Wednesday, June 16, 2010

GetFinalPathNameByHandle

Too bad GetFinalPathNameByHandle only works if the handle is a file that has something in it. For a directory or file with zero bytes it fails in a bad way. So you can't get the name of a directory or a zero byte file. Kind of a bummer especially since this is a new to Vista API and would be really nice to have when used in conjunction with CreateSymbolicLink. There are ways of getting around the odd failure by creating a file map, but the directory and zero byte file is a bit more difficult requiring some rather serious hoop jumping.

5 comments:

The Disciplined Engineer said...

Hi Chris, I had the same situation where I wanted to get the symbolic link target of a directory (from C#). I came across your post in my search. Eventually, I was able to solve my problem using the following code:

private const int FILE_SHARE_READ = 1;
private const int FILE_SHARE_WRITE = 2;

private const int CREATION_DISPOSITION_OPEN_EXISTING = 3;

private const int FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;

// http://msdn.microsoft.com/en-us/library/aa364962%28VS.85%29.aspx
[DllImport("kernel32.dll", EntryPoint = "GetFinalPathNameByHandleW", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern int GetFinalPathNameByHandle(IntPtr handle, [In, Out] StringBuilder path, int bufLen, int flags);

// http://msdn.microsoft.com/en-us/library/aa363858(VS.85).aspx
[DllImport("kernel32.dll", EntryPoint = "CreateFileW", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern SafeFileHandle CreateFile(string lpFileName, int dwDesiredAccess, int dwShareMode,
IntPtr SecurityAttributes, int dwCreationDisposition, int dwFlagsAndAttributes, IntPtr hTemplateFile);

public static string GetSymbolicLinkTarget(DirectoryInfo symlink)
{
SafeFileHandle directoryHandle = CreateFile(symlink.FullName, 0, 2, System.IntPtr.Zero, CREATION_DISPOSITION_OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, System.IntPtr.Zero);
if(directoryHandle.IsInvalid)
throw new Win32Exception(Marshal.GetLastWin32Error());

StringBuilder path = new StringBuilder(512);
int size = GetFinalPathNameByHandle(directoryHandle.DangerousGetHandle(), path, path.Capacity, 0);
if (size<0)
throw new Win32Exception(Marshal.GetLastWin32Error());
// The remarks section of GetFinalPathNameByHandle mentions the return being prefixed with "\\?\"
// More information about "\\?\" here -> http://msdn.microsoft.com/en-us/library/aa365247(v=VS.85).aspx
if (path[0] == '\\' && path[1] == '\\' && path[2] == '?' && path[3] == '\\')
return path.ToString().Substring(4);
else
return path.ToString();
}


I hope it helps you and whoever comes across this page :)

Chris Bensen said...

H, thanks for the comment. What about your code allows GetFinalPathNameByHandle to return the name of a directory? The last parameter to GetFinalPathNameByHandle is the only real choice when making the call but passing VOLUME_NAME_NT or VOLUME_NAME_DOS hasn't seemed to make any difference. Does your code work with zero byte files?

Olaf said...
This comment has been removed by the author.
Olaf said...

Here's a version that returns the proper values for symlinks to empty files and directories. Please use Delphi XE to compile or define missing function import etc.

I have noticed that one should always use the full path to a file when calling MKLINK, otherwise the call to "CreateFile" is going to fail (and DIR /AL as well).

Hope this helps,
Olaf

-------------------------------------------

{$WARN SYMBOL_PLATFORM OFF}

program FinalPathNameByHandle;

{$APPTYPE CONSOLE}

uses
SysUtils,
Windows;

(* ---- *)

var
Handle : THandle;
sLink, sTarget : String;
dwAttr, dwFlagsAndAttr : DWord;
uLen : UInt;
uResult : UInt = 1;

begin { FinalPathNameByHandle }
WriteLn;

if (ParamCount <> 1) then
begin
WriteLn ('FinalPathNameByHandle.exe [file or directory name]');
Halt ($FF);
end; { if }

sLink := ParamStr (1);

try
dwAttr := GetFileAttributes (PChar (sLink));

Win32Check (dwAttr <> INVALID_HANDLE_VALUE);

if ((dwAttr and FILE_ATTRIBUTE_REPARSE_POINT) <> 0) then
begin
WriteLn (Format ('"%s" is a Reparse Point', [sLink]));

if (FileExists (sLink)) then
dwFlagsAndAttr := FILE_ATTRIBUTE_NORMAL
else dwFlagsAndAttr := FILE_ATTRIBUTE_DIRECTORY or FILE_FLAG_BACKUP_SEMANTICS;

Handle := CreateFile (PChar (sLink), GENERIC_READ, FILE_SHARE_READ,
NIL, OPEN_EXISTING, dwFlagsAndAttr, 0);

if (Handle = INVALID_HANDLE_VALUE) and
(GetLastError = 5) then // Access denied
Handle := CreateFile (PChar (sLink), 0, FILE_SHARE_READ, NIL,
OPEN_EXISTING, dwFlagsAndAttr, 0);

Win32Check (Handle <> INVALID_HANDLE_VALUE);

try
SetLength (sTarget, MAX_PATH);

uLen := GetFinalPathNameByHandle (Handle, PChar (sTarget),
MAX_PATH,
FILE_NAME_NORMALIZED);
Win32Check (uLen > 0);

uResult := 0;
SetLength (sTarget, uLen);
WriteLn (Format ('File target: "%s"', [sTarget]));

finally
CloseHandle (Handle);
end; { try / finally }
end { if }
else WriteLn (Format ('"%s" is not a Reparse Point', [sLink]));

except
on E:Exception do
WriteLn (Format ('Exception %s: %s', [E.ClassName, E.Message]));
end; { try / except }

if (DebugHook > 0) then
begin
WriteLn;
Write ('Press [Enter] to continue ');
ReadLn;
end; { if }

Halt (uResult);
end.

Chris Bensen said...

@Olaf, thanks for investigating GetFinalPathNameByHandle. I'm on vacation at the moment but when I get back to my desk I take a look at what you've go here!

Post a Comment