Wednesday, December 1, 2010

New Delphi RTL symlink functions

Symbolic links (symlinks) have been around on *nix for some time but only recently introduced to Windows in Vista with CreateSymbolicLink. Windows 2000 had introduced junction points which is half way there by allowing a link to directory, also known as a soft link (if the file moves the link points nowhere) but does not allow a link to a file.

Windows has a few idiosyncrasies that everyone should be made aware of. Here is a link to Microsoft symlink documentation but there are a few key differences between Windows and *nix. Symlinks in *nix are simply a file with a link target. The target file can be either a directory or a file and could actually change. Windows on the other hand has symlinks to files and symlinks to directories. They are different and cannot be interchanged as they can with *nix.

In the Delphi RTL the goal was to do what made the most sense for the developer. In most cases this is treating the symlink as a file but in other cases as a directory. The only real problem comes with POSIX when checking if a DirectoryExists and trying to find out information on the symlink. The following are notes that I made of the changes to the Delphi RTL functions in SysUtils.pas and IOUtils.pas.

IsRelativePath\TDirectory.IsRelativePath (new)

IsRelativePath returns a boolean value that indicates whether the specified
path is a relative path.


FileCreateSymLink\TFile.CreateSymLink (new, Windows Vista and above)

FileCreateSymLink creates a symbolic link. The parameter Link is the name of
the symbolic link created and Target is the string contained in the symbolic
link. On Windows the target directory must exist at the time of calling FileCreateSymLink.


FileGetSymLinkTarget\TFile.GetSymLinkTarget (new, Windows Vista and above)

FileGetSymLinkTarget reads the contents of a symbolic link. The result is
returned in the symbolic link record given by SymLinkRec.

Note: 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"


FileGetSymLinkTarget\TFile.GetSymLinkTarget (new, Windows Vista and above) overload that takes a string

FileGetSymLinkTarget returns the target of a symbolic link.


FileAge

FileAge retrieves the date-and-time stamp of the specified file as a
TDateTime. This version supports all valid NTFS date-and-time stamps
and returns a boolean value that indicates whether the specified
file exists. If the specified file is a symlink the function is performed on
the target file. If FollowLink is false then the date-and-time of the
symlink file is returned.


FileExists

FileExists returns a boolean value that indicates whether the specified
file exists. If the specified file is a symlink the function is performed on
the target file. If FollowLink is false then the symlink file is used
regardless if the link is broken.

files

case 1: file.txt FollowLink = true returns true
case 2: file.txt FollowLink = false returns true
case 3: [does not exist] FollowLink = true returns false
case 4: [does not exist] FollowLink = false returns false

symlink to file

case 5: link.txt -> file.txt FollowLink = true returns true
case 6: link.txt -> file.txt FollowLink = false returns true
case 7: link.txt -> [does not exist] FollowLink = true returns false
case 8: link.txt -> [does not exist] FollowLink = false returns true

directories

case 9: dir FollowLink = true returns false
case 10: dir FollowLink = false returns false

symlink to directories

case 11: link -> dir FollowLink = true returns false
case 12: link -> dir FollowLink = false returns true
case 13: link -> [does not exist] FollowLink = true returns false
case 14: link -> [does not exist] FollowLink = false returns true


DirectoryExists

DirectoryExists returns a boolean value that indicates whether the
specified directory exists (and is actually a directory). If the specified
file is a symlink the function is performed on the target file. If FollowLink
is false then the symlink file is used. If the link is broken DirectoryExists
will always return false.

Notes:
On Windows there are directory symlinks and file symlinks. On POSIX a symlink is created
as a file that just happens to point to a directory.

directories

case 1: dir FollowLink = true returns true
case 2: dir FollowLink = false returns true
case 3: [does not exist] FollowLink = true returns false
case 4: [does not exist] FollowLink = false returns false

symlink to directories

case 5: link -> dir FollowLink = true returns true
case 6: link -> dir FollowLink = false returns true
case 7: link -> [does not exist] FollowLink = true returns false
case 8: link -> [does not exist] FollowLink = false returns true

files

case 9: file.txt FollowLink = true returns false
case 10 file.txt FollowLink = false returns false
case 11: link -> file.txt FollowLink = true returns false
case 12: link -> file.txt FollowLink = false returns false
case 13: file -> [does not exist] FollowLink = true returns false
case 14: file -> [does not exist] FollowLink = false returns true


FileGetAttr

FileGetAttr returns the file attributes of the file given by FileName. The
attributes can be examined by AND-ing with the faXXXX constants defined
above. A return value of -1 indicates that an error occurred. If the
specified file is a symlink then the function is performed on the target file.
If FollowLink is false then the symlink file is used.


FileSetAttr (Windows only)

FileSetAttr sets the file attributes of the file given by FileName to the
value given by Attr. The attribute value is formed by OR-ing the
appropriate faXXXX constants. The return value is zero if the function was
successful. Otherwise the return value is a system error code. If the
specified file is a symlink then the function is performed on the target file.
If FollowLink is false then the symlink file is used.

Note: It is suggested to use TFile.SetAttributes because it is cross platform.


DeleteFile

DeleteFile deletes the file given by FileName. The return value is True if
the file was successfully deleted, or False if an error occurred. DeleteFile
can delete a symlinks and symlinks to directories.


RemoveDir

RemoveDir deletes an existing empty directory. The return value is
True if the directory was successfully deleted, or False if an error
occurred. If the given directory is a symlink to a directory then the
symlink is deleted. On Windows the link can be broken and the symlink
can still be verified to be a symlink.


FileIsReadOnly

FileIsReadOnly tests whether a given file is read-only for the current
process and effective user id. If the file does not exist, the
function returns False. (Check FileExists before calling FileIsReadOnly)
This function is platform portable. If the file specified is a symlink
then the function is performed on the target file.


FileSetReadOnly

FileSetReadOnly sets the read only state of a file. The file must
exist and the current effective user id must be the owner of the file.
On Unix systems, FileSetReadOnly attempts to set or remove
all three (user, group, and other) write permissions on the file.
If you want to grant partial permissions (writeable for owner but not
for others), use platform specific functions such as chmod.
The function returns True if the file was successfully modified,
False if there was an error. This function is platform portable. If the
specified file is a symlink then the function is performed on the target
file.


FileSetDate

FileSetDate sets the OS date-and-time stamp of the file given by FileName
to the value given by Age. The DateTimeToFileDate function can be used to
convert a TDateTime value to an OS date-and-time stamp. The return value
is zero if the function was successful. Otherwise the return value is a
system error code. If the specified file is a symlink then the function is
performed on the target file. If FollowLink is false then the symlink file
is used.


RenameFile

RenameFile renames the file given by OldName to the name given by NewName.
The return value is True if the file was successfully renamed, or False if
an error occurred. If the file specified is a symlink then the function is
performed on the symlink.


FileGetDateTime (new)

Returns all file times associated with a file. If the specified file is a
symlink then the function is performed on the target file. If FollowLink is
false then the symlink file is used.


TSearchRec = record (updated)

Added TimeStamp property


TSymLinkRec = record (new)


TDirectory.Exists

Added FollowLink parameter


TDirectory.GetAttributes

Added FollowLink parameter


TPath.GetAttributes

Added FollowLink parameter


TFile.Exists

Added FollowLink parameter


TFile.GetAttributes

Added FollowLink parameter


FileGetDateTime (new)

Returns the TDateTime of the file's creation time, last access time and last write time.


FileGetDateTimeInfo

FileGetDateTimeInfo returns the date-and-time stamp of the specified file
and supports all valid NTFS date-and-time stamps. A boolena value is returned
indicating whether the specified file exists. If the specified file is a
symlink the function is performed on the target file. If FollowLink is false
then the date-and-time of the symlink file is returned.


TDateTimeInfoRec = record

Used by FileGetDateTimeInfo to return the various date times on demand. It is a public record but only consumable.

4 comments:

Craig Peterson said...

CreateSymbolicLink is useless for a lot of Delphi apps since Microsoft decided to require administrator privileges to use it. Inconsistently, creating junction points doesn't, even though they both use DeviceIoControl(FSCTL_SET_REPARSE_POINT) on the backend.

BTW, was there a reason why FileGetSymLinkTarget only returns the fully qualified target, and doesn't also include the raw path like readlink returns?

Chris Bensen said...

@Craig - Agreed, when I found out the administrator requirement I was pretty bummed.

There were a few reasons for returning the fully qualified target rather than exactly what the symlink contains. Basically it can be summed up as usability and consistency. But a more in depth explanation is we found the most common use case would be to fix up the path for the target file if it was a relative path. So we put that logic into FileGetSymLinkTarget to reduce the amount of code developers need to write in most cases. There are also some issues on Windows when providing relative paths in certain cases so sticking with fully qualified paths seemed more reasonable to avoid confusion and consistency between platforms. Plus the raw path on Windows contains the device information which is why ExpandVolumeName is called.

Cobaia said...

Can you make test, with files that belong to the system (owner SYSTEM)?

Chris Bensen said...

@Cobaia, I'm not sure exactly what your question is?

Post a Comment