doublecmd/components/doublecmd/dcntfslinks.pas
2026-01-17 16:32:34 +03:00

535 lines
19 KiB
ObjectPascal

{
Double Commander
-------------------------------------------------------------------------
This unit contains functions to work with hard and symbolic links
on the NTFS file system.
Copyright (C) 2012-2025 Alexander Koblov (alexx2000@mail.ru)
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library. If not, see <https://www.gnu.org/licenses/>.
}
unit DCNtfsLinks;
{$mode delphi}
interface
uses
Windows, SysUtils;
const
// CreateSymbolicLink flags
SYMBOLIC_LINK_FLAG_FILE = 0;
SYMBOLIC_LINK_FLAG_DIRECTORY = 1;
// CreateFile flags
FILE_FLAG_OPEN_REPARSE_POINT = $00200000;
// DeviceIoControl control codes
FSCTL_SET_REPARSE_POINT = $000900A4;
FSCTL_GET_REPARSE_POINT = $000900A8;
FSCTL_DELETE_REPARSE_POINT = $000900AC;
// WSL and Cygwin symbolic link
IO_REPARSE_TAG_LX_SYMLINK = $A000001D;
const
LX_SYMLINK_HEADER_SIZE = 4;
REPARSE_DATA_HEADER_SIZE = 8;
MOUNT_POINT_HEADER_SIZE = 8;
FILE_DOES_NOT_EXIST = DWORD(-1);
wsLongFileNamePrefix = UnicodeString('\\?\');
wsNativeFileNamePrefix = UnicodeString('\??\');
wsNetworkFileNamePrefix = UnicodeString('\??\UNC\');
type
{$packrecords c}
TSymbolicLinkReparseBuffer = record
SubstituteNameOffset: USHORT;
SubstituteNameLength: USHORT;
PrintNameOffset: USHORT;
PrintNameLength: USHORT;
Flags: ULONG;
PathBuffer: array[0..0] of WCHAR;
end;
TMountPointReparseBuffer = record
SubstituteNameOffset: USHORT;
SubstituteNameLength: USHORT;
PrintNameOffset: USHORT;
PrintNameLength: USHORT;
PathBuffer: array[0..0] of WCHAR;
end;
TLxSymlinkReparseBuffer = record
FileType: DWORD;
PathBuffer: array[0..0] of AnsiChar;
end;
TGenericReparseBuffer = record
DataBuffer: array[0..0] of UCHAR;
end;
REPARSE_DATA_BUFFER = record
ReparseTag: ULONG;
ReparseDataLength: USHORT;
Reserved: USHORT;
case Integer of
0: (SymbolicLinkReparseBuffer: TSymbolicLinkReparseBuffer);
1: (MountPointReparseBuffer: TMountPointReparseBuffer);
2: (LxSymlinkReparseBuffer: TLxSymlinkReparseBuffer);
3: (GenericReparseBuffer: TGenericReparseBuffer);
end;
TReparseDataBuffer = REPARSE_DATA_BUFFER;
PReparseDataBuffer = ^REPARSE_DATA_BUFFER;
{$packrecords default}
{en
Creates a symbolic link.
This function is only supported on the NTFS file system.
On Windows 2000/XP it works for directories only
On Windows Vista/Seven it works for directories and files
(for files it works only with Administrator rights)
@param(AFileName The name of the existing file)
@param(ALinkName The name of the symbolic link)
@returns(The function returns @true if successful, @false otherwise)
}
function CreateSymLink(const ATargetName, ALinkName: UnicodeString; Attr: UInt32): Boolean;
{en
Established a hard link beetwen an existing file and new file. This function
is only supported on the NTFS file system, and only for files, not directories.
@param(AFileName The name of the existing file)
@param(ALinkName The name of the new hard link)
@returns(The function returns @true if successful, @false otherwise)
}
function CreateHardLink(const AFileName, ALinkName: UnicodeString): Boolean;
{en
Reads a symbolic link target.
This function is only supported on the NTFS file system.
@param(aSymlinkFileName The name of the symbolic link)
@param(aTargetFileName The name of the target file/directory)
@returns(The function returns @true if successful, @false otherwise)
}
function ReadSymLink(const aSymlinkFileName: UnicodeString; out aTargetFileName: UnicodeString): Boolean;
{en
Creates a WSL/Cygwin symbolic link.
@param(aTargetFileName The name of the existing file)
@param(aSymlinkFileName The name of the symbolic link)
@returns(The function returns @true if successful, @false otherwise)
}
function CreateSymLinkUnix(const aTargetFileName: String; const aSymlinkFileName: UnicodeString): Boolean;
implementation
const
ERROR_DIRECTORY_NOT_SUPPORTED = 336;
type
TCreateSymbolicLinkW = function(
pwcSymlinkFileName,
pwcTargetFileName: PWideChar;
dwFlags: DWORD): BOOL; stdcall;
TCreateHardLinkW = function (
lpFileName,
lpExistingFileName: LPCWSTR;
lpSecurityAttributes: LPSECURITY_ATTRIBUTES): BOOL; stdcall;
var
HasNewApi: Boolean = False;
MayCreateSymLink: Boolean = False;
CreateHardLinkW: TCreateHardLinkW = nil;
CreateSymbolicLinkW: TCreateSymbolicLinkW = nil;
function _CreateHardLink_New(AFileName : UnicodeString; ALinkName: UnicodeString): Boolean;
begin
if Assigned(CreateHardLinkW) then
Result:= CreateHardLinkW(PWideChar(ALinkName), PWideChar(AFileName), nil)
else begin
Result:= False;
SetLastError(ERROR_NOT_SUPPORTED);
end;
end;
function _CreateHardLink_Old(aExistingFileName, aFileName: UnicodeString): Boolean;
var
hFile: THandle;
lpBuffer: TWin32StreamId;
wcFileName: array[0..MAX_PATH] of WideChar;
dwNumberOfBytesWritten: DWORD = 0;
lpContext: LPVOID = nil;
lpFilePart: LPWSTR = nil;
begin
Result:= GetFullPathNameW(PWideChar(aFileName), MAX_PATH, wcFileName, lpFilePart) > 0;
if Result then
begin
hFile:= CreateFileW(PWideChar(aExistingFileName),
GENERIC_READ or GENERIC_WRITE,
FILE_SHARE_READ or FILE_SHARE_WRITE or FILE_SHARE_DELETE,
nil, OPEN_EXISTING, 0, 0);
Result:= (hFile <> INVALID_HANDLE_VALUE);
end;
if Result then
try
ZeroMemory(@lpBuffer, SizeOf(TWin32StreamId));
with lpBuffer do
begin
dwStreamId:= BACKUP_LINK;
Size.LowPart:= (Length(aFileName) + 1) * SizeOf(WideChar);
end;
// Write stream header
Result:= BackupWrite(hFile,
@lpBuffer,
SizeOf(TWin32StreamId) - SizeOf(PWideChar),
dwNumberOfBytesWritten,
False,
False,
lpContext);
if not Result then Exit;
// Write file name buffer
Result:= BackupWrite(hFile,
@wcFileName,
lpBuffer.Size.LowPart,
dwNumberOfBytesWritten,
False,
False,
lpContext);
if not Result then Exit;
// Finish write operation
Result:= BackupWrite(hFile,
nil,
0,
dwNumberOfBytesWritten,
True,
False,
lpContext);
finally
CloseHandle(hFile);
end;
end;
function CreateHardLink(const AFileName, ALinkName: UnicodeString): Boolean;
var
dwAttributes: DWORD;
begin
dwAttributes := Windows.GetFileAttributesW(PWideChar(AFileName));
if dwAttributes = FILE_DOES_NOT_EXIST then Exit(False);
if (dwAttributes and FILE_ATTRIBUTE_DIRECTORY) <> 0 then
begin
SetLastError(ERROR_DIRECTORY_NOT_SUPPORTED);
Exit(False);
end;
dwAttributes := Windows.GetFileAttributesW(PWideChar(ALinkName));
if dwAttributes <> FILE_DOES_NOT_EXIST then
begin
SetLastError(ERROR_FILE_EXISTS);
Exit(False);
end;
if HasNewApi then
Result:= _CreateHardLink_New(AFileName, ALinkName)
else
Result:= _CreateHardLink_Old(AFileName, ALinkName)
end;
function _CreateSymLink_New(const ATargetFileName, ASymlinkFileName: UnicodeString; dwFlags: DWORD): Boolean;
begin
if not Assigned(CreateSymbolicLinkW) then
begin
Result:= False;
SetLastError(ERROR_NOT_SUPPORTED);
end
// CreateSymbolicLinkW under Windows 10 1903 does not return error if user doesn't have
// SeCreateSymbolicLinkPrivilege, so we make manual check and return error in this case
else begin
if MayCreateSymLink then
Result:= CreateSymbolicLinkW(PWideChar(ASymlinkFileName), PWideChar(ATargetFileName), dwFlags)
else begin
Result:= False;
SetLastError(ERROR_PRIVILEGE_NOT_HELD);
end
end;
end;
function _CreateSymLink_Old(aTargetFileName, aSymlinkFileName: UnicodeString): Boolean;
var
hDevice: THandle;
lpInBuffer: PReparseDataBuffer;
dwLastError,
nInBufferSize,
dwPathBufferSize: DWORD;
wsNativeFileName: UnicodeString;
lpBytesReturned: DWORD = 0;
begin
Result:= CreateDirectoryW(PWideChar(aSymlinkFileName), nil);
if Result then
try
hDevice:= CreateFileW(PWideChar(aSymlinkFileName),
GENERIC_WRITE, 0, nil, OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS or FILE_FLAG_OPEN_REPARSE_POINT, 0);
if hDevice = INVALID_HANDLE_VALUE then
begin
dwLastError:= GetLastError;
Exit(False);
end;
if Pos(wsLongFileNamePrefix, aTargetFileName) <> 1 then
wsNativeFileName:= wsNativeFileNamePrefix + aTargetFileName
else begin
wsNativeFileName:= wsNativeFileNamePrefix + Copy(aTargetFileName, 5, MaxInt);
end;
// File name length with trailing zero and zero for empty PrintName
dwPathBufferSize:= Length(wsNativeFileName) * SizeOf(WideChar) + 4;
nInBufferSize:= REPARSE_DATA_HEADER_SIZE + MOUNT_POINT_HEADER_SIZE + dwPathBufferSize;
lpInBuffer:= GetMem(nInBufferSize);
ZeroMemory(lpInBuffer, nInBufferSize);
with lpInBuffer^, lpInBuffer^.MountPointReparseBuffer do
begin
ReparseTag:= IO_REPARSE_TAG_MOUNT_POINT;
ReparseDataLength:= MOUNT_POINT_HEADER_SIZE + dwPathBufferSize;
SubstituteNameLength:= Length(wsNativeFileName) * SizeOf(WideChar);
PrintNameOffset:= SubstituteNameOffset + SubstituteNameLength + SizeOf(WideChar);
CopyMemory(@PathBuffer[0], @wsNativeFileName[1], SubstituteNameLength);
end;
Result:= DeviceIoControl(hDevice, // handle to file or directory
FSCTL_SET_REPARSE_POINT, // dwIoControlCode
lpInBuffer, // input buffer
nInBufferSize, // size of input buffer
nil, // lpOutBuffer
0, // nOutBufferSize
lpBytesReturned, // lpBytesReturned
nil); // OVERLAPPED structure
if not Result then dwLastError:= GetLastError;
FreeMem(lpInBuffer);
CloseHandle(hDevice);
finally
if not Result then
begin
RemoveDirectoryW(PWideChar(aSymlinkFileName));
SetLastError(dwLastError);
end;
end;
end;
function CreateSymLink(const ATargetName, ALinkName: UnicodeString; Attr: UInt32): Boolean;
var
dwAttributes: DWORD;
lpFilePart: LPWSTR = nil;
AFileName, AFullPathName: UnicodeString;
begin
Result:= False;
if (Length(ATargetName) > 1) and CharInSet(ATargetName[2], [':', '\']) then
AFullPathName:= ATargetName
else begin
SetLength(AFullPathName, MaxSmallint);
AFileName:= ExtractFilePath(ALinkName) + ATargetName;
dwAttributes:= GetFullPathNameW(PWideChar(AFileName), MaxSmallint, PWideChar(AFullPathName), lpFilePart);
if dwAttributes > 0 then
SetLength(AFullPathName, dwAttributes)
else begin
AFullPathName:= ATargetName;
end;
end;
if (Attr <> FILE_DOES_NOT_EXIST) then
dwAttributes:= Attr
else begin
dwAttributes:= Windows.GetFileAttributesW(PWideChar(AFullPathName));
end;
if dwAttributes = FILE_DOES_NOT_EXIST then Exit;
if HasNewApi = False then
begin
if (dwAttributes and FILE_ATTRIBUTE_DIRECTORY) <> 0 then
Result:= _CreateSymLink_Old(AFullPathName, ALinkName)
else
SetLastError(ERROR_NOT_SUPPORTED);
end
else begin
if (dwAttributes and FILE_ATTRIBUTE_DIRECTORY) = 0 then
Result:= _CreateSymLink_New(ATargetName, ALinkName, SYMBOLIC_LINK_FLAG_FILE)
else begin
if (not MayCreateSymLink) and (Pos('\\', AFullPathName) = 0) then
Result:= _CreateSymLink_Old(AFullPathName, ALinkName)
else begin
Result:= _CreateSymLink_New(ATargetName, ALinkName, SYMBOLIC_LINK_FLAG_DIRECTORY);
end;
end;
end;
end;
function CreateSymLinkUnix(const aTargetFileName: String; const aSymlinkFileName: UnicodeString): Boolean;
var
hDevice: THandle;
dwLastError: DWORD;
nInBufferSize: DWORD;
dwPathBufferSize: DWORD;
lpBytesReturned: DWORD = 0;
lpInBuffer: PReparseDataBuffer;
begin
hDevice:= CreateFileW(PWideChar(aSymlinkFileName),
GENERIC_WRITE, 0, nil, CREATE_NEW,
FILE_FLAG_OPEN_REPARSE_POINT, 0);
if hDevice = INVALID_HANDLE_VALUE then Exit(False);
dwPathBufferSize:= Length(aTargetFileName);
nInBufferSize:= REPARSE_DATA_HEADER_SIZE + LX_SYMLINK_HEADER_SIZE + dwPathBufferSize;
lpInBuffer:= GetMem(nInBufferSize);
ZeroMemory(lpInBuffer, nInBufferSize);
with lpInBuffer^, lpInBuffer^.LxSymlinkReparseBuffer do
begin
FileType:= 2; // symbolic link
ReparseTag:= IO_REPARSE_TAG_LX_SYMLINK;
ReparseDataLength:= LX_SYMLINK_HEADER_SIZE + dwPathBufferSize;
CopyMemory(@PathBuffer[0], @aTargetFileName[1], Length(aTargetFileName));
end;
Result:= DeviceIoControl(hDevice, // handle to file or directory
FSCTL_SET_REPARSE_POINT, // dwIoControlCode
lpInBuffer, // input buffer
nInBufferSize, // size of input buffer
nil, // lpOutBuffer
0, // nOutBufferSize
lpBytesReturned, // lpBytesReturned
nil); // OVERLAPPED structure
// File system does not support reparse points
// Create a normal file with the link target inside
if (not Result) and (GetLastError = ERROR_INVALID_FUNCTION) then
begin
Result:= (FileWrite(hDevice, aTargetFileName[1], dwPathBufferSize) = dwPathBufferSize);
if Result then SetFileAttributesW(PWideChar(aSymlinkFileName), FILE_ATTRIBUTE_SYSTEM);
end;
if not Result then dwLastError:= GetLastError;
FreeMem(lpInBuffer);
CloseHandle(hDevice);
if not Result then
begin
DeleteFileW(PWideChar(aSymlinkFileName));
SetLastError(dwLastError);
end;
end;
function ReadSymLink(const aSymlinkFileName: UnicodeString; out aTargetFileName: UnicodeString): Boolean;
var
L: Integer;
hDevice: THandle;
dwFileAttributes: DWORD;
caOutBuffer: array[0..MaxSmallint] of Byte;
lpOutBuffer: TReparseDataBuffer absolute caOutBuffer;
pwcTargetFileName: PWideChar;
lpBytesReturned: DWORD = 0;
dwFlagsAndAttributes: DWORD;
begin
dwFileAttributes:= GetFileAttributesW(PWideChar(aSymlinkFileName));
Result:= dwFileAttributes <> FILE_DOES_NOT_EXIST;
if Result then
begin
if (dwFileAttributes and FILE_ATTRIBUTE_DIRECTORY) = 0 then
dwFlagsAndAttributes:= FILE_FLAG_OPEN_REPARSE_POINT
else
dwFlagsAndAttributes:= FILE_FLAG_BACKUP_SEMANTICS or FILE_FLAG_OPEN_REPARSE_POINT;
// Open reparse point
hDevice:= CreateFileW(PWideChar(aSymlinkFileName),
0, FILE_SHARE_READ or FILE_SHARE_WRITE,
nil, OPEN_EXISTING, dwFlagsAndAttributes, 0);
Result:= hDevice <> INVALID_HANDLE_VALUE;
if not Result then Exit;
Result:= DeviceIoControl(hDevice, // handle to file or directory
FSCTL_GET_REPARSE_POINT, // dwIoControlCode
nil, // input buffer
0, // size of input buffer
@caOutBuffer, // lpOutBuffer
SizeOf(caOutBuffer), // nOutBufferSize
lpBytesReturned, // lpBytesReturned
nil); // OVERLAPPED structure
CloseHandle(hDevice);
if Result then
begin
case lpOutBuffer.ReparseTag of
IO_REPARSE_TAG_SYMLINK:
with lpOutBuffer.SymbolicLinkReparseBuffer do
begin
pwcTargetFileName:= @PathBuffer[0];
pwcTargetFileName:= pwcTargetFileName + SubstituteNameOffset div SizeOf(WideChar);
SetLength(aTargetFileName, SubstituteNameLength div SizeOf(WideChar));
CopyMemory(PWideChar(aTargetFileName), pwcTargetFileName, SubstituteNameLength);
end;
IO_REPARSE_TAG_MOUNT_POINT:
with lpOutBuffer.MountPointReparseBuffer do
begin
pwcTargetFileName:= @PathBuffer[0];
pwcTargetFileName:= pwcTargetFileName + SubstituteNameOffset div SizeOf(WideChar);
SetLength(aTargetFileName, SubstituteNameLength div SizeOf(WideChar));
CopyMemory(PWideChar(aTargetFileName), pwcTargetFileName, SubstituteNameLength);
end;
IO_REPARSE_TAG_LX_SYMLINK:
with lpOutBuffer.LxSymlinkReparseBuffer do
begin
L:= lpOutBuffer.ReparseDataLength - SizeOf(FileType);
SetLength(aTargetFileName, L + 1);
SetLength(aTargetFileName, MultiByteToWideChar(CP_UTF8, 0, @PathBuffer[0], L, PWideChar(aTargetFileName), L + 1));
end;
end;
if Pos(wsNetworkFileNamePrefix, aTargetFileName) = 1 then
Delete(aTargetFileName, 2, Length(wsNetworkFileNamePrefix) - 2)
else if Pos(wsNativeFileNamePrefix, aTargetFileName) = 1 then
Delete(aTargetFileName, 1, Length(wsNativeFileNamePrefix));
end;
end;
end;
function MayCreateSymbolicLink: Boolean;
const
SE_CREATE_SYMBOLIC_LINK_NAME = 'SeCreateSymbolicLinkPrivilege';
var
I: Integer;
hProcess: HANDLE;
dwLength: DWORD = 0;
seCreateSymbolicLink: LUID = 0;
TokenInformation: array [0..1023] of Byte;
Privileges: TTokenPrivileges absolute TokenInformation;
begin
hProcess:= GetCurrentProcess();
if (OpenProcessToken(hProcess, TOKEN_READ, hProcess)) then
try
if (LookupPrivilegeValueW(nil, SE_CREATE_SYMBOLIC_LINK_NAME, seCreateSymbolicLink)) then
begin
if (GetTokenInformation(hProcess, TokenPrivileges, @Privileges, SizeOf(TokenInformation), dwLength)) then
begin
{$PUSH}{$R-}
for I:= 0 to Int32(Privileges.PrivilegeCount) - 1 do
begin
if Privileges.Privileges[I].Luid = seCreateSymbolicLink then
Exit(True);
end;
{$POP}
end;
end;
finally
CloseHandle(hProcess);
end;
Result:= False;
end;
procedure Initialize;
var
AHandle: HMODULE;
begin
MayCreateSymLink:= MayCreateSymbolicLink;
HasNewApi:= (Win32Platform = VER_PLATFORM_WIN32_NT) and (Win32MajorVersion >= 6);
if HasNewApi then begin
AHandle:= GetModuleHandle('kernel32.dll');
CreateHardLinkW:= TCreateHardLinkW(GetProcAddress(AHandle, 'CreateHardLinkW'));
CreateSymbolicLinkW:= TCreateSymbolicLinkW(GetProcAddress(AHandle, 'CreateSymbolicLinkW'));
end;
end;
initialization
Initialize;
end.