dcntfslinks.pas 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  1. {
  2. Double Commander
  3. -------------------------------------------------------------------------
  4. This unit contains functions to work with hard and symbolic links
  5. on the NTFS file system.
  6. Copyright (C) 2012-2025 Alexander Koblov ([email protected])
  7. This library is free software; you can redistribute it and/or
  8. modify it under the terms of the GNU Lesser General Public
  9. License as published by the Free Software Foundation; either
  10. version 2.1 of the License, or (at your option) any later version.
  11. This library is distributed in the hope that it will be useful,
  12. but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. Lesser General Public License for more details.
  15. You should have received a copy of the GNU Lesser General Public
  16. License along with this library. If not, see <https://www.gnu.org/licenses/>.
  17. }
  18. unit DCNtfsLinks;
  19. {$mode delphi}
  20. interface
  21. uses
  22. Windows, SysUtils;
  23. const
  24. // CreateSymbolicLink flags
  25. SYMBOLIC_LINK_FLAG_FILE = 0;
  26. SYMBOLIC_LINK_FLAG_DIRECTORY = 1;
  27. // CreateFile flags
  28. FILE_FLAG_OPEN_REPARSE_POINT = $00200000;
  29. // DeviceIoControl control codes
  30. FSCTL_SET_REPARSE_POINT = $000900A4;
  31. FSCTL_GET_REPARSE_POINT = $000900A8;
  32. FSCTL_DELETE_REPARSE_POINT = $000900AC;
  33. // WSL and Cygwin symbolic link
  34. IO_REPARSE_TAG_LX_SYMLINK = $A000001D;
  35. const
  36. REPARSE_DATA_HEADER_SIZE = 8;
  37. MOUNT_POINT_HEADER_SIZE = 8;
  38. FILE_DOES_NOT_EXIST = DWORD(-1);
  39. wsLongFileNamePrefix = UnicodeString('\\?\');
  40. wsNativeFileNamePrefix = UnicodeString('\??\');
  41. wsNetworkFileNamePrefix = UnicodeString('\??\UNC\');
  42. type
  43. {$packrecords c}
  44. TSymbolicLinkReparseBuffer = record
  45. SubstituteNameOffset: USHORT;
  46. SubstituteNameLength: USHORT;
  47. PrintNameOffset: USHORT;
  48. PrintNameLength: USHORT;
  49. Flags: ULONG;
  50. PathBuffer: array[0..0] of WCHAR;
  51. end;
  52. TMountPointReparseBuffer = record
  53. SubstituteNameOffset: USHORT;
  54. SubstituteNameLength: USHORT;
  55. PrintNameOffset: USHORT;
  56. PrintNameLength: USHORT;
  57. PathBuffer: array[0..0] of WCHAR;
  58. end;
  59. TLxSymlinkReparseBuffer = record
  60. FileType: DWORD;
  61. PathBuffer: array[0..0] of AnsiChar;
  62. end;
  63. TGenericReparseBuffer = record
  64. DataBuffer: array[0..0] of UCHAR;
  65. end;
  66. REPARSE_DATA_BUFFER = record
  67. ReparseTag: ULONG;
  68. ReparseDataLength: USHORT;
  69. Reserved: USHORT;
  70. case Integer of
  71. 0: (SymbolicLinkReparseBuffer: TSymbolicLinkReparseBuffer);
  72. 1: (MountPointReparseBuffer: TMountPointReparseBuffer);
  73. 2: (LxSymlinkReparseBuffer: TLxSymlinkReparseBuffer);
  74. 3: (GenericReparseBuffer: TGenericReparseBuffer);
  75. end;
  76. TReparseDataBuffer = REPARSE_DATA_BUFFER;
  77. PReparseDataBuffer = ^REPARSE_DATA_BUFFER;
  78. {$packrecords default}
  79. {en
  80. Creates a symbolic link.
  81. This function is only supported on the NTFS file system.
  82. On Windows 2000/XP it works for directories only
  83. On Windows Vista/Seven it works for directories and files
  84. (for files it works only with Administrator rights)
  85. @param(AFileName The name of the existing file)
  86. @param(ALinkName The name of the symbolic link)
  87. @returns(The function returns @true if successful, @false otherwise)
  88. }
  89. function CreateSymLink(const ATargetName, ALinkName: UnicodeString; Attr: UInt32): Boolean;
  90. {en
  91. Established a hard link beetwen an existing file and new file. This function
  92. is only supported on the NTFS file system, and only for files, not directories.
  93. @param(AFileName The name of the existing file)
  94. @param(ALinkName The name of the new hard link)
  95. @returns(The function returns @true if successful, @false otherwise)
  96. }
  97. function CreateHardLink(const AFileName, ALinkName: UnicodeString): Boolean;
  98. {en
  99. Reads a symbolic link target.
  100. This function is only supported on the NTFS file system.
  101. @param(aSymlinkFileName The name of the symbolic link)
  102. @param(aTargetFileName The name of the target file/directory)
  103. @returns(The function returns @true if successful, @false otherwise)
  104. }
  105. function ReadSymLink(const aSymlinkFileName: UnicodeString; out aTargetFileName: UnicodeString): Boolean;
  106. implementation
  107. const
  108. ERROR_DIRECTORY_NOT_SUPPORTED = 336;
  109. type
  110. TCreateSymbolicLinkW = function(
  111. pwcSymlinkFileName,
  112. pwcTargetFileName: PWideChar;
  113. dwFlags: DWORD): BOOL; stdcall;
  114. TCreateHardLinkW = function (
  115. lpFileName,
  116. lpExistingFileName: LPCWSTR;
  117. lpSecurityAttributes: LPSECURITY_ATTRIBUTES): BOOL; stdcall;
  118. var
  119. HasNewApi: Boolean = False;
  120. MayCreateSymLink: Boolean = False;
  121. CreateHardLinkW: TCreateHardLinkW = nil;
  122. CreateSymbolicLinkW: TCreateSymbolicLinkW = nil;
  123. function _CreateHardLink_New(AFileName : UnicodeString; ALinkName: UnicodeString): Boolean;
  124. begin
  125. if Assigned(CreateHardLinkW) then
  126. Result:= CreateHardLinkW(PWideChar(ALinkName), PWideChar(AFileName), nil)
  127. else begin
  128. Result:= False;
  129. SetLastError(ERROR_NOT_SUPPORTED);
  130. end;
  131. end;
  132. function _CreateHardLink_Old(aExistingFileName, aFileName: UnicodeString): Boolean;
  133. var
  134. hFile: THandle;
  135. lpBuffer: TWin32StreamId;
  136. wcFileName: array[0..MAX_PATH] of WideChar;
  137. dwNumberOfBytesWritten: DWORD = 0;
  138. lpContext: LPVOID = nil;
  139. lpFilePart: LPWSTR = nil;
  140. begin
  141. Result:= GetFullPathNameW(PWideChar(aFileName), MAX_PATH, wcFileName, lpFilePart) > 0;
  142. if Result then
  143. begin
  144. hFile:= CreateFileW(PWideChar(aExistingFileName),
  145. GENERIC_READ or GENERIC_WRITE,
  146. FILE_SHARE_READ or FILE_SHARE_WRITE or FILE_SHARE_DELETE,
  147. nil, OPEN_EXISTING, 0, 0);
  148. Result:= (hFile <> INVALID_HANDLE_VALUE);
  149. end;
  150. if Result then
  151. try
  152. ZeroMemory(@lpBuffer, SizeOf(TWin32StreamId));
  153. with lpBuffer do
  154. begin
  155. dwStreamId:= BACKUP_LINK;
  156. Size.LowPart:= (Length(aFileName) + 1) * SizeOf(WideChar);
  157. end;
  158. // Write stream header
  159. Result:= BackupWrite(hFile,
  160. @lpBuffer,
  161. SizeOf(TWin32StreamId) - SizeOf(PWideChar),
  162. dwNumberOfBytesWritten,
  163. False,
  164. False,
  165. lpContext);
  166. if not Result then Exit;
  167. // Write file name buffer
  168. Result:= BackupWrite(hFile,
  169. @wcFileName,
  170. lpBuffer.Size.LowPart,
  171. dwNumberOfBytesWritten,
  172. False,
  173. False,
  174. lpContext);
  175. if not Result then Exit;
  176. // Finish write operation
  177. Result:= BackupWrite(hFile,
  178. nil,
  179. 0,
  180. dwNumberOfBytesWritten,
  181. True,
  182. False,
  183. lpContext);
  184. finally
  185. CloseHandle(hFile);
  186. end;
  187. end;
  188. function CreateHardLink(const AFileName, ALinkName: UnicodeString): Boolean;
  189. var
  190. dwAttributes: DWORD;
  191. begin
  192. dwAttributes := Windows.GetFileAttributesW(PWideChar(AFileName));
  193. if dwAttributes = FILE_DOES_NOT_EXIST then Exit(False);
  194. if (dwAttributes and FILE_ATTRIBUTE_DIRECTORY) <> 0 then
  195. begin
  196. SetLastError(ERROR_DIRECTORY_NOT_SUPPORTED);
  197. Exit(False);
  198. end;
  199. dwAttributes := Windows.GetFileAttributesW(PWideChar(ALinkName));
  200. if dwAttributes <> FILE_DOES_NOT_EXIST then
  201. begin
  202. SetLastError(ERROR_FILE_EXISTS);
  203. Exit(False);
  204. end;
  205. if HasNewApi then
  206. Result:= _CreateHardLink_New(AFileName, ALinkName)
  207. else
  208. Result:= _CreateHardLink_Old(AFileName, ALinkName)
  209. end;
  210. function _CreateSymLink_New(const ATargetFileName, ASymlinkFileName: UnicodeString; dwFlags: DWORD): Boolean;
  211. begin
  212. if not Assigned(CreateSymbolicLinkW) then
  213. begin
  214. Result:= False;
  215. SetLastError(ERROR_NOT_SUPPORTED);
  216. end
  217. // CreateSymbolicLinkW under Windows 10 1903 does not return error if user doesn't have
  218. // SeCreateSymbolicLinkPrivilege, so we make manual check and return error in this case
  219. else begin
  220. if MayCreateSymLink then
  221. Result:= CreateSymbolicLinkW(PWideChar(ASymlinkFileName), PWideChar(ATargetFileName), dwFlags)
  222. else begin
  223. Result:= False;
  224. SetLastError(ERROR_PRIVILEGE_NOT_HELD);
  225. end
  226. end;
  227. end;
  228. function _CreateSymLink_Old(aTargetFileName, aSymlinkFileName: UnicodeString): Boolean;
  229. var
  230. hDevice: THandle;
  231. lpInBuffer: PReparseDataBuffer;
  232. dwLastError,
  233. nInBufferSize,
  234. dwPathBufferSize: DWORD;
  235. wsNativeFileName: UnicodeString;
  236. lpBytesReturned: DWORD = 0;
  237. begin
  238. Result:= CreateDirectoryW(PWideChar(aSymlinkFileName), nil);
  239. if Result then
  240. try
  241. hDevice:= CreateFileW(PWideChar(aSymlinkFileName),
  242. GENERIC_WRITE, 0, nil, OPEN_EXISTING,
  243. FILE_FLAG_BACKUP_SEMANTICS or FILE_FLAG_OPEN_REPARSE_POINT, 0);
  244. if hDevice = INVALID_HANDLE_VALUE then
  245. begin
  246. dwLastError:= GetLastError;
  247. Exit(False);
  248. end;
  249. if Pos(wsLongFileNamePrefix, aTargetFileName) <> 1 then
  250. wsNativeFileName:= wsNativeFileNamePrefix + aTargetFileName
  251. else begin
  252. wsNativeFileName:= wsNativeFileNamePrefix + Copy(aTargetFileName, 5, MaxInt);
  253. end;
  254. // File name length with trailing zero and zero for empty PrintName
  255. dwPathBufferSize:= Length(wsNativeFileName) * SizeOf(WideChar) + 4;
  256. nInBufferSize:= REPARSE_DATA_HEADER_SIZE + MOUNT_POINT_HEADER_SIZE + dwPathBufferSize;
  257. lpInBuffer:= GetMem(nInBufferSize);
  258. ZeroMemory(lpInBuffer, nInBufferSize);
  259. with lpInBuffer^, lpInBuffer^.MountPointReparseBuffer do
  260. begin
  261. ReparseTag:= IO_REPARSE_TAG_MOUNT_POINT;
  262. ReparseDataLength:= MOUNT_POINT_HEADER_SIZE + dwPathBufferSize;
  263. SubstituteNameLength:= Length(wsNativeFileName) * SizeOf(WideChar);
  264. PrintNameOffset:= SubstituteNameOffset + SubstituteNameLength + SizeOf(WideChar);
  265. CopyMemory(@PathBuffer[0], @wsNativeFileName[1], SubstituteNameLength);
  266. end;
  267. Result:= DeviceIoControl(hDevice, // handle to file or directory
  268. FSCTL_SET_REPARSE_POINT, // dwIoControlCode
  269. lpInBuffer, // input buffer
  270. nInBufferSize, // size of input buffer
  271. nil, // lpOutBuffer
  272. 0, // nOutBufferSize
  273. lpBytesReturned, // lpBytesReturned
  274. nil); // OVERLAPPED structure
  275. if not Result then dwLastError:= GetLastError;
  276. FreeMem(lpInBuffer);
  277. CloseHandle(hDevice);
  278. finally
  279. if not Result then
  280. begin
  281. RemoveDirectoryW(PWideChar(aSymlinkFileName));
  282. SetLastError(dwLastError);
  283. end;
  284. end;
  285. end;
  286. function CreateSymLink(const ATargetName, ALinkName: UnicodeString; Attr: UInt32): Boolean;
  287. var
  288. dwAttributes: DWORD;
  289. lpFilePart: LPWSTR = nil;
  290. AFileName, AFullPathName: UnicodeString;
  291. begin
  292. Result:= False;
  293. if (Length(ATargetName) > 1) and CharInSet(ATargetName[2], [':', '\']) then
  294. AFullPathName:= ATargetName
  295. else begin
  296. SetLength(AFullPathName, MaxSmallint);
  297. AFileName:= ExtractFilePath(ALinkName) + ATargetName;
  298. dwAttributes:= GetFullPathNameW(PWideChar(AFileName), MaxSmallint, PWideChar(AFullPathName), lpFilePart);
  299. if dwAttributes > 0 then
  300. SetLength(AFullPathName, dwAttributes)
  301. else begin
  302. AFullPathName:= ATargetName;
  303. end;
  304. end;
  305. if (Attr <> FILE_DOES_NOT_EXIST) then
  306. dwAttributes:= Attr
  307. else begin
  308. dwAttributes:= Windows.GetFileAttributesW(PWideChar(AFullPathName));
  309. end;
  310. if dwAttributes = FILE_DOES_NOT_EXIST then Exit;
  311. if HasNewApi = False then
  312. begin
  313. if (dwAttributes and FILE_ATTRIBUTE_DIRECTORY) <> 0 then
  314. Result:= _CreateSymLink_Old(AFullPathName, ALinkName)
  315. else
  316. SetLastError(ERROR_NOT_SUPPORTED);
  317. end
  318. else begin
  319. if (dwAttributes and FILE_ATTRIBUTE_DIRECTORY) = 0 then
  320. Result:= _CreateSymLink_New(ATargetName, ALinkName, SYMBOLIC_LINK_FLAG_FILE)
  321. else begin
  322. if (not MayCreateSymLink) and (Pos('\\', AFullPathName) = 0) then
  323. Result:= _CreateSymLink_Old(AFullPathName, ALinkName)
  324. else begin
  325. Result:= _CreateSymLink_New(ATargetName, ALinkName, SYMBOLIC_LINK_FLAG_DIRECTORY);
  326. end;
  327. end;
  328. end;
  329. end;
  330. function ReadSymLink(const aSymlinkFileName: UnicodeString; out aTargetFileName: UnicodeString): Boolean;
  331. var
  332. L: Integer;
  333. hDevice: THandle;
  334. dwFileAttributes: DWORD;
  335. caOutBuffer: array[0..MaxSmallint] of Byte;
  336. lpOutBuffer: TReparseDataBuffer absolute caOutBuffer;
  337. pwcTargetFileName: PWideChar;
  338. lpBytesReturned: DWORD = 0;
  339. dwFlagsAndAttributes: DWORD;
  340. begin
  341. dwFileAttributes:= GetFileAttributesW(PWideChar(aSymlinkFileName));
  342. Result:= dwFileAttributes <> FILE_DOES_NOT_EXIST;
  343. if Result then
  344. begin
  345. if (dwFileAttributes and FILE_ATTRIBUTE_DIRECTORY) = 0 then
  346. dwFlagsAndAttributes:= FILE_FLAG_OPEN_REPARSE_POINT
  347. else
  348. dwFlagsAndAttributes:= FILE_FLAG_BACKUP_SEMANTICS or FILE_FLAG_OPEN_REPARSE_POINT;
  349. // Open reparse point
  350. hDevice:= CreateFileW(PWideChar(aSymlinkFileName),
  351. 0, FILE_SHARE_READ or FILE_SHARE_WRITE,
  352. nil, OPEN_EXISTING, dwFlagsAndAttributes, 0);
  353. Result:= hDevice <> INVALID_HANDLE_VALUE;
  354. if not Result then Exit;
  355. Result:= DeviceIoControl(hDevice, // handle to file or directory
  356. FSCTL_GET_REPARSE_POINT, // dwIoControlCode
  357. nil, // input buffer
  358. 0, // size of input buffer
  359. @caOutBuffer, // lpOutBuffer
  360. SizeOf(caOutBuffer), // nOutBufferSize
  361. lpBytesReturned, // lpBytesReturned
  362. nil); // OVERLAPPED structure
  363. CloseHandle(hDevice);
  364. if Result then
  365. begin
  366. case lpOutBuffer.ReparseTag of
  367. IO_REPARSE_TAG_SYMLINK:
  368. with lpOutBuffer.SymbolicLinkReparseBuffer do
  369. begin
  370. pwcTargetFileName:= @PathBuffer[0];
  371. pwcTargetFileName:= pwcTargetFileName + SubstituteNameOffset div SizeOf(WideChar);
  372. SetLength(aTargetFileName, SubstituteNameLength div SizeOf(WideChar));
  373. CopyMemory(PWideChar(aTargetFileName), pwcTargetFileName, SubstituteNameLength);
  374. end;
  375. IO_REPARSE_TAG_MOUNT_POINT:
  376. with lpOutBuffer.MountPointReparseBuffer do
  377. begin
  378. pwcTargetFileName:= @PathBuffer[0];
  379. pwcTargetFileName:= pwcTargetFileName + SubstituteNameOffset div SizeOf(WideChar);
  380. SetLength(aTargetFileName, SubstituteNameLength div SizeOf(WideChar));
  381. CopyMemory(PWideChar(aTargetFileName), pwcTargetFileName, SubstituteNameLength);
  382. end;
  383. IO_REPARSE_TAG_LX_SYMLINK:
  384. with lpOutBuffer.LxSymlinkReparseBuffer do
  385. begin
  386. L:= lpOutBuffer.ReparseDataLength - SizeOf(FileType);
  387. SetLength(aTargetFileName, L + 1);
  388. SetLength(aTargetFileName, MultiByteToWideChar(CP_UTF8, 0, @PathBuffer[0], L, PWideChar(aTargetFileName), L + 1));
  389. end;
  390. end;
  391. if Pos(wsNetworkFileNamePrefix, aTargetFileName) = 1 then
  392. Delete(aTargetFileName, 2, Length(wsNetworkFileNamePrefix) - 2)
  393. else if Pos(wsNativeFileNamePrefix, aTargetFileName) = 1 then
  394. Delete(aTargetFileName, 1, Length(wsNativeFileNamePrefix));
  395. end;
  396. end;
  397. end;
  398. function MayCreateSymbolicLink: Boolean;
  399. const
  400. SE_CREATE_SYMBOLIC_LINK_NAME = 'SeCreateSymbolicLinkPrivilege';
  401. var
  402. I: Integer;
  403. hProcess: HANDLE;
  404. dwLength: DWORD = 0;
  405. seCreateSymbolicLink: LUID = 0;
  406. TokenInformation: array [0..1023] of Byte;
  407. Privileges: TTokenPrivileges absolute TokenInformation;
  408. begin
  409. hProcess:= GetCurrentProcess();
  410. if (OpenProcessToken(hProcess, TOKEN_READ, hProcess)) then
  411. try
  412. if (LookupPrivilegeValueW(nil, SE_CREATE_SYMBOLIC_LINK_NAME, seCreateSymbolicLink)) then
  413. begin
  414. if (GetTokenInformation(hProcess, TokenPrivileges, @Privileges, SizeOf(TokenInformation), dwLength)) then
  415. begin
  416. {$PUSH}{$R-}
  417. for I:= 0 to Int32(Privileges.PrivilegeCount) - 1 do
  418. begin
  419. if Privileges.Privileges[I].Luid = seCreateSymbolicLink then
  420. Exit(True);
  421. end;
  422. {$POP}
  423. end;
  424. end;
  425. finally
  426. CloseHandle(hProcess);
  427. end;
  428. Result:= False;
  429. end;
  430. procedure Initialize;
  431. var
  432. AHandle: HMODULE;
  433. begin
  434. MayCreateSymLink:= MayCreateSymbolicLink;
  435. HasNewApi:= (Win32Platform = VER_PLATFORM_WIN32_NT) and (Win32MajorVersion >= 6);
  436. if HasNewApi then begin
  437. AHandle:= GetModuleHandle('kernel32.dll');
  438. CreateHardLinkW:= TCreateHardLinkW(GetProcAddress(AHandle, 'CreateHardLinkW'));
  439. CreateSymbolicLinkW:= TCreateSymbolicLinkW(GetProcAddress(AHandle, 'CreateSymbolicLinkW'));
  440. end;
  441. end;
  442. initialization
  443. Initialize;
  444. end.