4

I have found that on Windows 7 64 bit, on a machine with a domain name, GetUserNameEx( 3, .... ) which should get the extended name format DisplayName (==3), into a buffer, works fine.

However, it does not work on Windows 7 32 bit, vm that is on a workgroup, rather than on a domain, it returns ERROR_NONE_MAPPED.

How do you read the person's friendly name "Fred Smith", for example, in a way that works on Windows? GetUserNameEx is manifestly broken. Actually, not broken, I'm told, just not intended to work for users who are not on a domain. Why not, I wonder, since the local SAM information exists? And there appears to be no other direct API to do this.

If Windows gives you ERROR_NONE_MAPPED, you are out of luck, and probably not on a domain. So this is not exactly a friendly area of the API.

[It is possible, it looks like, to call NetUserGetInfo, to read the local SAM info, when not on a domain, but you need to know the user name and password first, and then it will maybe look up the friendly name.]

RElated Question: does not mention the problem here

Community
  • 1
  • 1
Warren P
  • 65,725
  • 40
  • 181
  • 316
  • My answer works for Delphi, Matt's answer works for C#. I'll retag this as Visual C++ if someone ports this code snippet to C++. – Warren P Jan 16 '13 at 18:11

2 Answers2

7

Here is Warren's solution ported to C#. I added retrieval of a domain controller's IP from the domain name because at least on my domain, just using \\<domain> as the server name didn't work.

using System;
using System.Text;
using System.Net;
using System.Runtime.InteropServices;
using System.DirectoryServices.ActiveDirectory;

[DllImport("secur32.dll", CharSet = CharSet.Auto)]
private static extern int GetUserNameEx (int nameFormat, StringBuilder userName, ref uint userNameSize);

[DllImport("netapi32.dll", CharSet = CharSet.Unicode, ExactSpelling = true)]
private static extern int NetUserGetInfo ([MarshalAs(UnmanagedType.LPWStr)] string serverName,
                                          [MarshalAs(UnmanagedType.LPWStr)] string userName,
                                          int level, out IntPtr bufPtr);

[DllImport("netapi32.dll", CharSet = CharSet.Unicode, ExactSpelling = true)]
private static extern long NetApiBufferFree (out IntPtr bufPtr);

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct USER_INFO_10
{
    [MarshalAs(UnmanagedType.LPWStr)] public string usri10_name;
    [MarshalAs(UnmanagedType.LPWStr)] public string usri10_comment;
    [MarshalAs(UnmanagedType.LPWStr)] public string usri10_usr_comment;
    [MarshalAs(UnmanagedType.LPWStr)] public string usri10_full_name;
}

private string getUserDisplayName ()
{
    var username = new StringBuilder(1024);
    uint userNameSize = (uint) username.Capacity;

    // try to get display name and convert from "Last, First" to "First Last" if necessary
    if (0 != GetUserNameEx(3, username, ref userNameSize))
        return Regex.Replace(username.ToString(), @"(\S+), (\S+)", "$2 $1");

    // get SAM compatible name <server/machine>\\<username>
    if (0 != GetUserNameEx(2, username, ref userNameSize))
    {
        IntPtr bufPtr;
        try
        {
            string domain = Regex.Replace(username.ToString(), @"(.+)\\.+", @"$1");
            DirectoryContext context = new DirectoryContext(DirectoryContextType.Domain, domain);
            DomainController dc = DomainController.FindOne(context);

            if (0 == NetUserGetInfo(dc.IPAddress,
                                    Regex.Replace(username.ToString(), @".+\\(.+)", "$1"),
                                    10, out bufPtr))
            {
                var userInfo = (USER_INFO_10) Marshal.PtrToStructure(bufPtr, typeof (USER_INFO_10));
                return Regex.Replace(userInfo.usri10_full_name, @"(\S+), (\S+)", "$2 $1");
            }
        }
        finally
        {
            NetApiBufferFree(out bufPtr);
        }
    }

    return String.Empty;
}
David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
Matt Chambers
  • 2,229
  • 1
  • 25
  • 43
  • Would be more relevant if it was C++ (as the OP tagged `VisualC++` on the question tags).... – Fabricio Araujo Nov 09 '11 at 16:06
  • Exactly. C# is not exactly relevant here at this Delphi question that is not tagged C#. – Warren P Jul 24 '12 at 13:33
  • 3
    Since you'd already answered your own question to your satisfaction, I posted an alternate language version (which would incidentally be easier to port to C++) since I expected it would help somebody else in the future when they find your question for the same reason I did. – Matt Chambers Jul 31 '12 at 14:44
  • 1
    I've retagged the question `C#` so people will also find your work, thanks Matt. The philosophy on SO is exactly as Matt says; Think of other people, not just about the OP (Warren). – Warren P Jan 16 '13 at 18:10
3

I have a solution that appears to work, which in general means:

  1. if the GetUserNameEx(3,...) function from secur32.dll works, use that value.
  2. fall back to a combination of GetUserNameEx(2,...) calls and NetUserGetInfo calls imported from netapi32.dll
  3. The problem with invoking NetUserGetInfo first, is that it fails on domain names, or at least, the implementation below on NetUserGetInfo works only when reading SAM information from a local machine name, on a non-domain/non-ActiveDirectory user namespace.

Sample code (in Delphi), ported to C# below in Matt's answer:

type
EProcError = class( Exception );
TGetUserNameExWProc = function( FormatType : Integer; Buffer : PWideChar; var BufSize : Integer ) : DWORD;      stdcall;
var
  _GetUserNameExW : TGetUserNameExWProc;
procedure GetProcedureAddress( var P : Pointer; const ModuleName, ProcName : string );
var
  ModuleHandle : HMODULE;
begin
  if not Assigned( P ) then
  begin
    ModuleHandle := GetModuleHandle( pChar( ModuleName ) );
    if ModuleHandle = 0 then
    begin
      ModuleHandle := SafeLoadLibrary( pChar( ModuleName ) );
      if ModuleHandle = 0 then
        raise EProcError.Create( 'Unable to load module' );
    end;
    P := GetProcAddress( ModuleHandle, pChar( ProcName ) );
    if not Assigned( P ) then
      raise EProcError.Create( 'Unable to get proc address' );
  end;
end;
function MyGetUserNameEx( aFormat : Integer ) : string;
var
  sz : Integer;
  sz2 : Integer;
  ret : Integer;
begin
  if not Assigned( _GetUserNameExW ) then
    GetProcedureAddress( Pointer( @_GetUserNameExW ), 'secur32.dll', 'GetUserNameExW' );
  if Assigned( _GetUserNameExW ) then
  begin
    sz := 2000;
    SetLength( Result, sz );
    Result[ 1 ] := Chr( 0 );
    ret := _GetUserNameExW( { 3=NameDisplay } aFormat, PWideChar( Result ), sz );
    if ret <> 0 then
    begin
      sz2 := StrLen( PWideChar( Result ) ); // workaround WinXP API bug
      if sz2 < sz then // WinXP bug.
        sz := sz2;
      SetLength( Result, sz )
    end
    else
    begin
      ret := GetLastError;
      if ret = ERROR_NONE_MAPPED then
        Result := ''
      else
        Result := 'E' + IntToStr( ret );
    end;
  end;
end;
function MyNetUserGetInfo : string;
const
  netapi32 = 'netapi32.dll';
type
  TNetUserGetInfo = function( servername, username : LPCWSTR; level : DWORD; var bufptr : PByte ) : DWORD; stdcall;
  TNetApiBufferFree = function( Buffer : PByte ) : DWORD; stdcall;
  USER_INFO_10 = record
    usri10_name : PWideChar;
    usri10_comment : PWideChar;
    usri10_usr_comment : PWideChar;
    usri10_full_name : PWideChar;
  end;
  P_USER_INFO_10 = ^USER_INFO_10;
var
  _NetUserGetInfo : TNetUserGetInfo;
  _NetApiBufferFree : TNetApiBufferFree;
  ret : DWORD;
  servername : string;
  username : string;
  level : Cardinal;
  info : P_USER_INFO_10;
  pbuf : PByte;
  pwuser : PWideChar;
  n : Integer;
begin
  ret := 0;
  _NetUserGetInfo := nil;
  GetProcedureAddress( Pointer( @_NetUserGetInfo ), netapi32, 'NetUserGetInfo' ); // raises EProcError
  if not Assigned( _NetUserGetInfo ) then
    Result := 'FunctionNotFound'
  else
  begin
    // usernamesize := 200;
    username := MyGetUserNameEx( 2 );
    if username = '' then
    begin
      Result := 'CanNotGetUserName';
      Exit;
    end;
    n := Pos( '\', username );      //' recover SO code formatting
    if n > 0 then
    begin
      servername := '\\' + Copy( username, 1, n - 1 );
      username := Copy( username, n + 1, Length( username ) );
    end;
    level := 10;
    pbuf := nil;
    pwuser := PWideChar( username );
    info := nil;
    if servername = '' then
      ret := _NetUserGetInfo( { servername } nil, pwuser, level, pbuf )
    else
      ret := _NetUserGetInfo( PWideChar( servername ), pwuser, level, pbuf );
    if ret = 0 then
    begin
      info := P_USER_INFO_10( pbuf );
      if Assigned( info ) then
        Result := info.usri10_full_name;
      GetProcedureAddress( Pointer( @_NetApiBufferFree ), netapi32, 'NetApiBufferFree' );
      if Assigned( info ) and Assigned( _NetApiBufferFree ) then
        _NetApiBufferFree( pbuf );
    end
    else
    begin
      if ret = 2221 then
        Result := 'Error_USER ' + username
      else if ret = 1722 then
        Result := 'Error_RPC ' + servername
      else
        Result := 'E' + IntToStr( ret );
    end;
  end;
end;

Edit: Nov 2011; Removed dead link.

Warren P
  • 65,725
  • 40
  • 181
  • 316
  • 2
    How long will that link be active? In the interest of posterity, I think it would be useful to also include a paraphrased code sample rather than just function names. – mskfisher Sep 10 '10 at 19:44
  • 1
    For the benefit of SO readers, just paste the code in your answer right here! – Andreas Rejbrand Sep 10 '10 at 21:37
  • +1 to both the above. Answers really need to include the full answer: the web as a whole suffers from link-rot. If you post a long code sample, SO automatically formats it with scrollbars, so there isn't really any such thing as 'too long' as far as I'm aware. – David Sep 13 '10 at 01:37
  • 1
    Oh yes there is. Whitespace really breaks SO's code feature. I had to go through the hundreds of lines and carefully reformat it to work with SO. This took me a long time. I wish that when you clicked "code sample" around a block of code, it didn't break up into different sections when it feels I've got a big whitespace break. – Warren P Sep 14 '10 at 12:47
  • 1
    Another option is to use a WMI query: `SELECT FullName FROM Win32_UserAccount WHERE DOMAIN = '...' and Name = '...'`, where `Name` and `Domain` can be obtained from `GetUserName()` and `LookupAccountName()`, respectively. – Remy Lebeau May 19 '15 at 18:30