PathFunc.pas 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540
  1. unit PathFunc;
  2. {
  3. Inno Setup
  4. Copyright (C) 1997-2025 Jordan Russell
  5. Portions by Martijn Laan
  6. For conditions of distribution and use, see LICENSE.TXT.
  7. This unit provides some path-related functions.
  8. }
  9. interface
  10. function AddBackslash(const S: String): String;
  11. function PathChangeExt(const Filename, Extension: String): String;
  12. function PathCharCompare(const S1, S2: PChar): Boolean;
  13. function PathCharIsSlash(const C: Char): Boolean;
  14. function PathCharIsTrailByte(const S: String; const Index: Integer): Boolean;
  15. function PathCharLength(const S: String; const Index: Integer): Integer;
  16. function PathCombine(const Dir, Filename: String): String;
  17. function PathCompare(const S1, S2: String): Integer;
  18. function PathDrivePartLength(const Filename: String): Integer;
  19. function PathDrivePartLengthEx(const Filename: String;
  20. const IncludeSignificantSlash: Boolean): Integer;
  21. function PathExpand(const Filename: String): String; overload;
  22. function PathExpand(const Filename: String; out ExpandedFilename: String): Boolean; overload;
  23. function PathExtensionPos(const Filename: String): Integer;
  24. function PathExtractDir(const Filename: String): String;
  25. function PathExtractDrive(const Filename: String): String;
  26. function PathExtractExt(const Filename: String): String;
  27. function PathExtractName(const Filename: String): String;
  28. function PathExtractPath(const Filename: String): String;
  29. function PathIsRooted(const Filename: String): Boolean;
  30. function PathLastChar(const S: String): PChar;
  31. function PathLastDelimiter(const Delimiters, S: string): Integer;
  32. function PathLowercase(const S: String): String;
  33. function PathNormalizeSlashes(const S: String): String;
  34. function PathPathPartLength(const Filename: String;
  35. const IncludeSlashesAfterPath: Boolean): Integer;
  36. function PathPos(Ch: Char; const S: String): Integer;
  37. function PathStartsWith(const S, AStartsWith: String): Boolean;
  38. function PathStrNextChar(const S: PChar): PChar;
  39. function PathStrPrevChar(const Start, Current: PChar): PChar;
  40. function PathStrScan(const S: PChar; const C: Char): PChar;
  41. function RemoveBackslash(const S: String): String;
  42. function RemoveBackslashUnlessRoot(const S: String): String;
  43. implementation
  44. {$ZEROBASEDSTRINGS OFF}
  45. uses
  46. Windows, SysUtils;
  47. function AddBackslash(const S: String): String;
  48. { Returns S plus a trailing backslash, unless S is an empty string or already
  49. ends in a backslash/slash. }
  50. begin
  51. if (S <> '') and not PathCharIsSlash(PathLastChar(S)^) then
  52. Result := S + '\'
  53. else
  54. Result := S;
  55. end;
  56. function PathCharLength(const S: String; const Index: Integer): Integer;
  57. { Returns the length in characters of the character at Index in S. }
  58. begin
  59. Result := 1;
  60. end;
  61. function PathCharIsSlash(const C: Char): Boolean;
  62. { Returns True if C is a backslash or slash. }
  63. begin
  64. Result := (C = '\') or (C = '/');
  65. end;
  66. function PathCharIsTrailByte(const S: String; const Index: Integer): Boolean;
  67. { Returns False if S[Index] is a single byte character or a lead byte.
  68. Returns True otherwise (i.e. it must be a trail byte). }
  69. var
  70. I: Integer;
  71. begin
  72. I := 1;
  73. while I <= Index do begin
  74. if I = Index then begin
  75. Result := False;
  76. Exit;
  77. end;
  78. Inc(I, PathCharLength(S, I));
  79. end;
  80. Result := True;
  81. end;
  82. function PathCharCompare(const S1, S2: PChar): Boolean;
  83. { Compares two first characters, and returns True if they are equal. }
  84. var
  85. N, I: Integer;
  86. begin
  87. N := PathStrNextChar(S1) - S1;
  88. if N = PathStrNextChar(S2) - S2 then begin
  89. for I := 0 to N-1 do begin
  90. if S1[I] <> S2[I] then begin
  91. Result := False;
  92. Exit;
  93. end;
  94. end;
  95. Result := True;
  96. end else
  97. Result := False;
  98. end;
  99. function PathChangeExt(const Filename, Extension: String): String;
  100. { Takes Filename, removes any existing extension, then adds the extension
  101. specified by Extension and returns the resulting string. }
  102. var
  103. I: Integer;
  104. begin
  105. I := PathExtensionPos(Filename);
  106. if I = 0 then
  107. Result := Filename + Extension
  108. else
  109. Result := Copy(Filename, 1, I - 1) + Extension;
  110. end;
  111. function PathCombine(const Dir, Filename: String): String;
  112. { Combines a directory and filename into a path.
  113. If Dir is empty, it just returns Filename.
  114. If Filename is empty, it returns an empty string (ignoring Dir).
  115. If Filename begins with a drive letter or slash, it returns Filename
  116. (ignoring Dir).
  117. If Dir specifies only a drive letter and colon ('c:'), it returns
  118. Dir + Filename.
  119. Otherwise, it returns the equivalent of AddBackslash(Dir) + Filename. }
  120. var
  121. I: Integer;
  122. begin
  123. if (Dir = '') or (Filename = '') or PathIsRooted(Filename) then
  124. Result := Filename
  125. else begin
  126. I := PathCharLength(Dir, 1) + 1;
  127. if ((I = Length(Dir)) and (Dir[I] = ':')) or
  128. PathCharIsSlash(PathLastChar(Dir)^) then
  129. Result := Dir + Filename
  130. else
  131. Result := Dir + '\' + Filename;
  132. end;
  133. end;
  134. function PathCompare(const S1, S2: String): Integer;
  135. { Compares two filenames, and returns 0 if they are equal. }
  136. begin
  137. Result := CompareStr(PathLowercase(S1), PathLowercase(S2));
  138. end;
  139. function PathDrivePartLength(const Filename: String): Integer;
  140. begin
  141. Result := PathDrivePartLengthEx(Filename, False);
  142. end;
  143. function PathDrivePartLengthEx(const Filename: String;
  144. const IncludeSignificantSlash: Boolean): Integer;
  145. { Returns length of the drive portion of Filename, or 0 if there is no drive
  146. portion.
  147. If IncludeSignificantSlash is True, the drive portion can include a trailing
  148. slash if it is significant to the meaning of the path (i.e. 'x:' and 'x:\'
  149. are not equivalent, nor are '\' and '').
  150. If IncludeSignificantSlash is False, the function works as follows:
  151. 'x:file' -> 2 ('x:')
  152. 'x:\file' -> 2 ('x:')
  153. '\\server\share\file' -> 14 ('\\server\share')
  154. '\file' -> 0 ('')
  155. If IncludeSignificantSlash is True, the function works as follows:
  156. 'x:file' -> 2 ('x:')
  157. 'x:\file' -> 3 ('x:\')
  158. '\\server\share\file' -> 14 ('\\server\share')
  159. '\file' -> 1 ('\')
  160. }
  161. var
  162. Len, I, C: Integer;
  163. begin
  164. Len := Length(Filename);
  165. { \\server\share }
  166. if (Len >= 2) and PathCharIsSlash(Filename[1]) and PathCharIsSlash(Filename[2]) then begin
  167. I := 3;
  168. C := 0;
  169. while I <= Len do begin
  170. if PathCharIsSlash(Filename[I]) then begin
  171. Inc(C);
  172. if C >= 2 then
  173. Break;
  174. repeat
  175. Inc(I);
  176. { And skip any additional consecutive slashes: }
  177. until (I > Len) or not PathCharIsSlash(Filename[I]);
  178. end
  179. else
  180. Inc(I, PathCharLength(Filename, I));
  181. end;
  182. Result := I - 1;
  183. Exit;
  184. end;
  185. { \ }
  186. { Note: Test this before 'x:' since '\:stream' means access stream 'stream'
  187. on the root directory of the current drive, not access drive '\:' }
  188. if (Len >= 1) and PathCharIsSlash(Filename[1]) then begin
  189. if IncludeSignificantSlash then
  190. Result := 1
  191. else
  192. Result := 0;
  193. Exit;
  194. end;
  195. { x: }
  196. if Len > 0 then begin
  197. I := PathCharLength(Filename, 1) + 1;
  198. if (I <= Len) and (Filename[I] = ':') then begin
  199. if IncludeSignificantSlash and (I < Len) and PathCharIsSlash(Filename[I+1]) then
  200. Result := I+1
  201. else
  202. Result := I;
  203. Exit;
  204. end;
  205. end;
  206. Result := 0;
  207. end;
  208. function PathIsRooted(const Filename: String): Boolean;
  209. { Returns True if Filename begins with a slash or drive ('x:').
  210. Equivalent to: PathDrivePartLengthEx(Filename, True) <> 0 }
  211. var
  212. Len, I: Integer;
  213. begin
  214. Result := False;
  215. Len := Length(Filename);
  216. if Len > 0 then begin
  217. { \ or \\ }
  218. if PathCharIsSlash(Filename[1]) then
  219. Result := True
  220. else begin
  221. { x: }
  222. I := PathCharLength(Filename, 1) + 1;
  223. if (I <= Len) and (Filename[I] = ':') then
  224. Result := True;
  225. end;
  226. end;
  227. end;
  228. function PathPathPartLength(const Filename: String;
  229. const IncludeSlashesAfterPath: Boolean): Integer;
  230. { Returns length of the path portion of Filename, or 0 if there is no path
  231. portion.
  232. Note these differences from Delphi's ExtractFilePath function:
  233. - The result will never be less than what PathDrivePartLength returns.
  234. If you pass a UNC root path, e.g. '\\server\share', it will return the
  235. length of the entire string, NOT the length of '\\server\'.
  236. - If you pass in a filename with a reference to an NTFS alternate data
  237. stream, e.g. 'abc:def', it will return the length of the entire string,
  238. NOT the length of 'abc:'. }
  239. var
  240. LastCharToKeep, Len, I: Integer;
  241. begin
  242. Result := PathDrivePartLengthEx(Filename, True);
  243. LastCharToKeep := Result;
  244. Len := Length(Filename);
  245. I := Result + 1;
  246. while I <= Len do begin
  247. if PathCharIsSlash(Filename[I]) then begin
  248. if IncludeSlashesAfterPath then
  249. Result := I
  250. else
  251. Result := LastCharToKeep;
  252. Inc(I);
  253. end
  254. else begin
  255. Inc(I, PathCharLength(Filename, I));
  256. LastCharToKeep := I-1;
  257. end;
  258. end;
  259. end;
  260. function PathExpand(const Filename: String; out ExpandedFilename: String): Boolean;
  261. { Like Delphi's ExpandFileName, but does proper error checking. }
  262. var
  263. Res: Integer;
  264. FilePart: PChar;
  265. Buf: array[0..4095] of Char;
  266. begin
  267. DWORD(Res) := GetFullPathName(PChar(Filename), SizeOf(Buf) div SizeOf(Buf[0]),
  268. Buf, FilePart);
  269. Result := (Res > 0) and (Res < SizeOf(Buf) div SizeOf(Buf[0]));
  270. if Result then
  271. SetString(ExpandedFilename, Buf, Res)
  272. end;
  273. function PathExpand(const Filename: String): String;
  274. begin
  275. if not PathExpand(Filename, Result) then
  276. Result := Filename;
  277. end;
  278. function PathExtensionPos(const Filename: String): Integer;
  279. { Returns index of the last '.' character in the filename portion of Filename,
  280. or 0 if there is no '.' in the filename portion.
  281. Note: Filename is assumed to NOT include an NTFS alternate data stream name
  282. (i.e. 'filename:stream'). }
  283. var
  284. Len, I: Integer;
  285. begin
  286. Result := 0;
  287. Len := Length(Filename);
  288. I := PathPathPartLength(Filename, True) + 1;
  289. while I <= Len do begin
  290. if Filename[I] = '.' then begin
  291. Result := I;
  292. Inc(I);
  293. end
  294. else
  295. Inc(I, PathCharLength(Filename, I));
  296. end;
  297. end;
  298. function PathExtractDir(const Filename: String): String;
  299. { Like PathExtractPath, but strips any trailing slashes, unless the resulting
  300. path is the root directory of a drive (i.e. 'C:\' or '\'). }
  301. var
  302. I: Integer;
  303. begin
  304. I := PathPathPartLength(Filename, False);
  305. Result := Copy(Filename, 1, I);
  306. end;
  307. function PathExtractDrive(const Filename: String): String;
  308. { Returns the drive portion of Filename (either 'x:' or '\\server\share'),
  309. or an empty string if there is no drive portion. }
  310. var
  311. L: Integer;
  312. begin
  313. L := PathDrivePartLength(Filename);
  314. if L = 0 then
  315. Result := ''
  316. else
  317. Result := Copy(Filename, 1, L);
  318. end;
  319. function PathExtractExt(const Filename: String): String;
  320. { Returns the extension portion of the last component of Filename (e.g. '.txt')
  321. or an empty string if there is no extension. }
  322. var
  323. I: Integer;
  324. begin
  325. I := PathExtensionPos(Filename);
  326. if I = 0 then
  327. Result := ''
  328. else
  329. Result := Copy(Filename, I, Maxint);
  330. end;
  331. function PathExtractName(const Filename: String): String;
  332. { Returns the filename portion of Filename (e.g. 'filename.txt'). If Filename
  333. ends in a slash or consists only of a drive part, the result will be an empty
  334. string.
  335. This function is essentially the opposite of PathExtractPath. }
  336. var
  337. I: Integer;
  338. begin
  339. I := PathPathPartLength(Filename, True);
  340. Result := Copy(Filename, I + 1, Maxint);
  341. end;
  342. function PathExtractPath(const Filename: String): String;
  343. { Returns the path portion of Filename (e.g. 'c:\dir\'). If Filename contains
  344. no drive part or slash, the result will be an empty string.
  345. This function is essentially the opposite of PathExtractName. }
  346. var
  347. I: Integer;
  348. begin
  349. I := PathPathPartLength(Filename, True);
  350. Result := Copy(Filename, 1, I);
  351. end;
  352. function PathLastChar(const S: String): PChar;
  353. { Returns pointer to last character in the string. Returns nil if the string is
  354. empty. }
  355. begin
  356. if S = '' then
  357. Result := nil
  358. else
  359. Result := @S[High(S)];
  360. end;
  361. function PathLastDelimiter(const Delimiters, S: string): Integer;
  362. { Returns the index of the last occurrence in S of one of the characters in
  363. Delimiters, or 0 if none were found.
  364. Note: S is allowed to contain null characters. }
  365. var
  366. P, E: PChar;
  367. begin
  368. Result := 0;
  369. if (S = '') or (Delimiters = '') then
  370. Exit;
  371. P := Pointer(S);
  372. E := @P[Length(S)];
  373. while P < E do begin
  374. if P^ <> #0 then begin
  375. if StrScan(PChar(Pointer(Delimiters)), P^) <> nil then
  376. Result := (P - PChar(Pointer(S))) + 1;
  377. P := PathStrNextChar(P);
  378. end
  379. else
  380. Inc(P);
  381. end;
  382. end;
  383. function PathLowercase(const S: String): String;
  384. { Converts the specified path name to lowercase }
  385. begin
  386. Result := AnsiLowerCase(S);
  387. end;
  388. function PathPos(Ch: Char; const S: String): Integer;
  389. var
  390. Len, I: Integer;
  391. begin
  392. Len := Length(S);
  393. I := 1;
  394. while I <= Len do begin
  395. if S[I] = Ch then begin
  396. Result := I;
  397. Exit;
  398. end;
  399. Inc(I, PathCharLength(S, I));
  400. end;
  401. Result := 0;
  402. end;
  403. function PathNormalizeSlashes(const S: String): String;
  404. { Returns S minus any superfluous slashes, and with any forward slashes
  405. converted to backslashes. For example, if S is 'C:\\\some//path', it returns
  406. 'C:\some\path'. Does not remove a double backslash at the beginning of the
  407. string, since that signifies a UNC path. }
  408. var
  409. Len, I: Integer;
  410. begin
  411. Result := S;
  412. Len := Length(Result);
  413. I := 1;
  414. while I <= Len do begin
  415. if Result[I] = '/' then
  416. Result[I] := '\';
  417. Inc(I, PathCharLength(Result, I));
  418. end;
  419. I := 1;
  420. while I < Length(Result) do begin
  421. if (Result[I] = '\') and (Result[I+1] = '\') and (I > 1) then
  422. Delete(Result, I+1, 1)
  423. else
  424. Inc(I, PathCharLength(Result, I));
  425. end;
  426. end;
  427. function PathStartsWith(const S, AStartsWith: String): Boolean;
  428. { Returns True if S starts with (or is equal to) AStartsWith. Uses path casing
  429. rules. }
  430. var
  431. AStartsWithLen: Integer;
  432. begin
  433. AStartsWithLen := Length(AStartsWith);
  434. if Length(S) = AStartsWithLen then
  435. Result := (PathCompare(S, AStartsWith) = 0)
  436. else if (Length(S) > AStartsWithLen) and not PathCharIsTrailByte(S, AStartsWithLen+1) then
  437. Result := (PathCompare(Copy(S, 1, AStartsWithLen), AStartsWith) = 0)
  438. else
  439. Result := False;
  440. end;
  441. function PathStrNextChar(const S: PChar): PChar;
  442. { Returns pointer to the character after S, unless S points to a null (#0). }
  443. begin
  444. Result := S;
  445. if Result^ <> #0 then
  446. Inc(Result);
  447. end;
  448. function PathStrPrevChar(const Start, Current: PChar): PChar;
  449. { Returns pointer to the character before Current, unless Current = Start. }
  450. begin
  451. Result := Current;
  452. if Result > Start then
  453. Dec(Result);
  454. end;
  455. function PathStrScan(const S: PChar; const C: Char): PChar;
  456. { Returns pointer to first occurrence of C in S, or nil if there are no
  457. occurrences. As with StrScan, specifying #0 for the search character is legal. }
  458. begin
  459. Result := S;
  460. while Result^ <> C do begin
  461. if Result^ = #0 then begin
  462. Result := nil;
  463. Break;
  464. end;
  465. Result := PathStrNextChar(Result);
  466. end;
  467. end;
  468. function RemoveBackslash(const S: String): String;
  469. { Returns S minus any trailing slashes. Use of this function is discouraged;
  470. use RemoveBackslashUnlessRoot instead when working with file system paths. }
  471. var
  472. I: Integer;
  473. begin
  474. I := Length(S);
  475. while (I > 0) and PathCharIsSlash(PathStrPrevChar(Pointer(S), @S[I+1])^) do
  476. Dec(I);
  477. if I = Length(S) then
  478. Result := S
  479. else
  480. Result := Copy(S, 1, I);
  481. end;
  482. function RemoveBackslashUnlessRoot(const S: String): String;
  483. { Returns S minus any trailing slashes, unless S specifies the root directory
  484. of a drive (i.e. 'C:\' or '\'), in which case it leaves 1 slash. }
  485. var
  486. DrivePartLen, I: Integer;
  487. begin
  488. DrivePartLen := PathDrivePartLengthEx(S, True);
  489. I := Length(S);
  490. while (I > DrivePartLen) and PathCharIsSlash(PathStrPrevChar(Pointer(S), @S[I+1])^) do
  491. Dec(I);
  492. if I = Length(S) then
  493. Result := S
  494. else
  495. Result := Copy(S, 1, I);
  496. end;
  497. end.