3

I am using Delphi XE5 and XE6 and creating a number of directory links programmatically by shelling a process that prompts elevation and uses the Delphi function:

FileCreateSymLink( sLinkPath, sTargetPath )

From this point on, my use of sLinkPath is automatically vectored to sTarget path by the file system. This all works fine. At other times I also need to interrogate such a link to see (a) if it is a link and (b) where it points. To do this I call the Delphi function

function FileGetSymLinkTarget(const FileName: string; var TargetName: string): Boolean;

With a successfully created link that points to another folder on my local hard disk, this works fine. However when I create a link that points to a network location such as

\\SERVER\Working\scratch\BJF\test

the link works perfectly at the file system level but Delphi calls to FileGetSymLinkTarget return false and a null target string. Stepping into SysUtils.pas reveals a call to “InternalGetFileNameFromSymLink” which in turn reveals a lot of hand waving to 'try' various calls to get sensible target information. I notice that inside this routine the one attempt that succeeds is the call to

GetObjectInfoName(Handle)

which reutrns

\Device\Mup\SERVER\Working\scratch\BJF\test

(close!) but which is then obliterated by ExpandVolumeName, probably because of the prefix, to a null string.

So, my question is:

Is this likely to be a bug in XE5 and XE6? Are there other ways of reading a link's target without using SysUtils?

LATER ADDITION BASED ON ACCEPTED ANSWER:

I have created a routine as follows based on Sertac's example that returns correct symlink paths for local drives and network paths. Although I am now calling this routine in place of SysUtils.FileGetSymLinkTarget, it may be useful to call SysUtils.FileGetSymLinkTarget first and only use my routine if the returned target is empty, perhaps to cope with redirections that I have not tried.

  function MyFileGetSymLinkTarget( const APathToLink : string; var ATarget : string ) : boolean;
  var
    LinkHandle: THandle;
    TargetName: array [0..OFS_MAXPATHNAME-1] of Char;
  begin
    ATarget := '';
    LinkHandle := CreateFile( PChar(APathToLink), 0, FILE_SHARE_READ, nil,
        OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0);
    Win32Check(LinkHandle <> INVALID_HANDLE_VALUE);
    try
      Result := GetFinalPathNameByHandle(LinkHandle, TargetName, OFS_MAXPATHNAME, FILE_NAME_NORMALIZED) > 0;
      if Result then
        begin
        ATarget := TargetName;
        if Pos( '\\?\UNC\', ATarget ) = 1 then
           begin
           Delete( ATarget, 1, 8 );
           Insert( '\\', ATarget, 1 );
           end
          else
          if Pos( '\\?\', ATarget ) = 1 then
             Delete( ATarget, 1, 4 );
        end;
    finally
      CloseHandle(LinkHandle);
    end;
  end;
Sertac Akyuz
  • 54,131
  • 4
  • 102
  • 169
Brian Frost
  • 13,334
  • 11
  • 80
  • 154
  • `ExpandVolumeName()` translates an object prefix into a local drive letter. It returns a blank string since your target is not mapped to a local drive letter but to a UNC path instead. `FileGetSymLinkTarget()` is resorting to `NTQueryObject()` with the undocumented `ObjectNameInformation` flag to get the object name, which is why you are getting a system-defined name. – Remy Lebeau May 01 '14 at 19:22
  • 1
    Symbolic Links are implemented as [Reparse Points](http://msdn.microsoft.com/en-us/library/windows/desktop/aa365503.aspx) on Windows, and Microsoft has APIs for working with Reparse Points, such as [`FSCTL_GET_REPARSE_POINT`](http://msdn.microsoft.com/en-us/library/windows/desktop/aa364571.aspx). I am surprised that the RTL resorted to manual hacks instead of using them. – Remy Lebeau May 01 '14 at 19:23
  • Thanks for your comments Remy. It does seem strange that the RTL operates this way. I will raise an issue with EMB, perhaps someone will look at it....?! – Brian Frost May 02 '14 at 08:43
  • Have a look at `(Ansi)StartsText()` instead of using `Pos()=1`. And when removing `\\?\UNC\ `, use `Delete(ATarget, 3, 6)` by itself instead of using `Delete()` and `Insert()` separately. – Remy Lebeau May 02 '14 at 18:15

1 Answers1

3

The following works in my test setup, which use GetFinalPathNameByHandle.

var
  LinkHandle: THandle;
  TargetName: array [0..512] of Char;
begin
  LinkHandle := CreateFile('[path to sym link]', 0, FILE_SHARE_READ, nil,
      OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0);
  Win32Check(LinkHandle <> INVALID_HANDLE_VALUE);
  try
    if GetFinalPathNameByHandle(LinkHandle, TargetName, 512,
        FILE_NAME_NORMALIZED) > 0 then
      ShowMessage(TargetName)
    else
      RaiseLastOSError;
  finally
    CloseHandle(LinkHandle);
  end;

end;

The target path is displayed as \\?\UNC\Server\Share\Folder\SubFolder\. You can test the left-most side agains \\?\UNC and replace it with \ if you like.

You can also substitute VOLUME_NAME_NONE in place of FILE_NAME_NORMALIZED to have the target path as \Server\Share\Folder\SubFolder\.

The RTL use the same function in one of its tries with VOLUME_NAME_NT as result type which returns a path like Device\Mup\.., and then tries to match the starting part of the string with one of the local logical volumes (GetLogicalDriveStrings). When there's no match, since the path points to a network drive, it returns an empty string as you have noted.


Do note the comment in the RTL sources regarding symbolic links across machine boundary though:

The access rights of symlinks are unpredictable over network drives. It is therefore not recommended to create symlinks over a network drive. To enable remote access of symlinks under Windows Vista and Windows 7 use the command: "fsutil behavior set SymlinkEvaluation R2R:1 R2L:1"

Sertac Akyuz
  • 54,131
  • 4
  • 102
  • 169
  • This works perfectly thanks. I've edited my question to show my final solution based your code above and which now works for local and network drives. I did see the RTL note about symlinks and network drives but I'm only ever creating them locally and (possibly) pointing to a network location so I assumed that this was ok for me. – Brian Frost May 02 '14 at 08:36
  • @Brian - You are welcome. You are probably right that the comment does not apply to your circumstances. If ever you'd need to change the behavior in your case, the parameters you'd need to use are then `L2R` and `L2L` as specified in fsutil's [documentation](http://technet.microsoft.com/en-us/library/cc785435%28WS.10%29.aspx). – Sertac Akyuz May 02 '14 at 09:08