2
0

PathFunc.pas 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776
  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. The string comparison functions (including PathCompare, PathEndsWith, PathHasSubstringAt,
  9. PathSame, PathStartsWith, PathStrCompare, PathStrFind) all ignore case by
  10. default, and use a locale-independent "ordinal" comparison, which is important
  11. when comparing filenames/paths. Despite the "Path" prefix, however, the
  12. functions can be used to compare any kind of text, not just filenames/paths.
  13. }
  14. interface
  15. function AddBackslash(const S: String): String;
  16. function PathChangeExt(const Filename, Extension: String): String;
  17. function PathCharCompare(const S1, S2: PChar): Boolean;
  18. function PathCharIsSlash(const C: Char): Boolean;
  19. function PathCharIsTrailByte(const S: String; const Index: Integer): Boolean;
  20. function PathCharLength(const S: String; const Index: Integer): Integer;
  21. function PathCombine(const Dir, Filename: String): String;
  22. function PathCompare(const S1, S2: String; const IgnoreCase: Boolean = True): Integer;
  23. function PathDrivePartLength(const Filename: String): Integer;
  24. function PathDrivePartLengthEx(const Filename: String;
  25. const IncludeSignificantSlash: Boolean): Integer;
  26. function PathEndsWith(const S, AEndsWith: String;
  27. const IgnoreCase: Boolean = True): Boolean;
  28. function PathExpand(const Filename: String): String; overload;
  29. function PathExpand(const Filename: String; out ExpandedFilename: String): Boolean; overload;
  30. function PathExtensionPos(const Filename: String): Integer;
  31. function PathExtractDir(const Filename: String): String;
  32. function PathExtractDrive(const Filename: String): String;
  33. function PathExtractExt(const Filename: String): String;
  34. function PathExtractName(const Filename: String): String;
  35. function PathExtractPath(const Filename: String): String;
  36. function PathHasInvalidCharacters(const S: String;
  37. const AllowDriveLetterColon: Boolean): Boolean;
  38. function PathHasSubstringAt(const S, Substring: String; const Offset: Integer;
  39. const IgnoreCase: Boolean = True): Boolean;
  40. function PathIsRooted(const Filename: String): Boolean;
  41. function PathLastChar(const S: String): PChar;
  42. function PathLastDelimiter(const Delimiters, S: string): Integer;
  43. function PathLowercase(const S: String): String;
  44. function PathNormalizeSlashes(S: String): String;
  45. function PathPathPartLength(const Filename: String;
  46. const IncludeSlashesAfterPath: Boolean): Integer;
  47. function PathPos(Ch: Char; const S: String): Integer;
  48. function PathSame(const S1, S2: String): Boolean;
  49. function PathStartsWith(const S, AStartsWith: String;
  50. const IgnoreCase: Boolean = True): Boolean;
  51. function PathStrCompare(const S1: PChar; const S1Length: Integer;
  52. const S2: PChar; const S2Length: Integer;
  53. const IgnoreCase: Boolean = True): Integer;
  54. function PathStrFind(const SSource: PChar; const SSourceLength: Integer;
  55. const SValue: PChar; const SValueLength: Integer;
  56. const IgnoreCase: Boolean = True): Integer;
  57. function PathStrNextChar(const S: PChar): PChar;
  58. function PathStrPrevChar(const Start, Current: PChar): PChar;
  59. function PathStrScan(const S: PChar; const C: Char): PChar;
  60. function RemoveBackslash(const S: String): String;
  61. function RemoveBackslashUnlessRoot(const S: String): String;
  62. function ValidateAndCombinePath(const ADestDir, AFilename: String;
  63. out AResultingPath: String): Boolean; overload;
  64. function ValidateAndCombinePath(const ADestDir, AFilename: String): Boolean; overload;
  65. implementation
  66. {$ZEROBASEDSTRINGS OFF}
  67. uses
  68. Windows, SysUtils;
  69. function AddBackslash(const S: String): String;
  70. { Returns S plus a trailing backslash, unless S is an empty string or already
  71. ends in a backslash/slash. }
  72. begin
  73. if (S <> '') and not PathCharIsSlash(PathLastChar(S)^) then
  74. Result := S + '\'
  75. else
  76. Result := S;
  77. end;
  78. function PathCharLength(const S: String; const Index: Integer): Integer;
  79. { Returns the length in characters of the character at Index in S. }
  80. begin
  81. Result := 1;
  82. end;
  83. function PathCharIsSlash(const C: Char): Boolean;
  84. { Returns True if C is a backslash or slash. }
  85. begin
  86. Result := (C = '\') or (C = '/');
  87. end;
  88. function PathCharIsTrailByte(const S: String; const Index: Integer): Boolean;
  89. { Returns False if S[Index] is a single byte character or a lead byte.
  90. Returns True otherwise (i.e. it must be a trail byte). }
  91. var
  92. I: Integer;
  93. begin
  94. I := 1;
  95. while I <= Index do begin
  96. if I = Index then begin
  97. Result := False;
  98. Exit;
  99. end;
  100. Inc(I, PathCharLength(S, I));
  101. end;
  102. Result := True;
  103. end;
  104. function PathCharCompare(const S1, S2: PChar): Boolean;
  105. { Compares two first characters, and returns True if they are equal. }
  106. begin
  107. const N = PathStrNextChar(S1) - S1;
  108. if N = PathStrNextChar(S2) - S2 then begin
  109. for var I := 0 to N-1 do begin
  110. if S1[I] <> S2[I] then begin
  111. Result := False;
  112. Exit;
  113. end;
  114. end;
  115. Result := True;
  116. end else
  117. Result := False;
  118. end;
  119. function PathChangeExt(const Filename, Extension: String): String;
  120. { Takes Filename, removes any existing extension, then adds the extension
  121. specified by Extension and returns the resulting string. }
  122. var
  123. I: Integer;
  124. begin
  125. I := PathExtensionPos(Filename);
  126. if I = 0 then
  127. Result := Filename + Extension
  128. else
  129. Result := Copy(Filename, 1, I - 1) + Extension;
  130. end;
  131. function PathCombine(const Dir, Filename: String): String;
  132. { Combines a directory and filename into a path.
  133. If Dir is empty, it just returns Filename.
  134. If Filename is empty, it returns an empty string (ignoring Dir).
  135. If Filename begins with a drive letter or slash, it returns Filename
  136. (ignoring Dir).
  137. If Dir specifies only a drive letter and colon ('c:'), it returns
  138. Dir + Filename.
  139. Otherwise, it returns the equivalent of AddBackslash(Dir) + Filename. }
  140. var
  141. I: Integer;
  142. begin
  143. if (Dir = '') or (Filename = '') or PathIsRooted(Filename) then
  144. Result := Filename
  145. else begin
  146. I := PathCharLength(Dir, 1) + 1;
  147. if ((I = Length(Dir)) and (Dir[I] = ':')) or
  148. PathCharIsSlash(PathLastChar(Dir)^) then
  149. Result := Dir + Filename
  150. else
  151. Result := Dir + '\' + Filename;
  152. end;
  153. end;
  154. function PathCompare(const S1, S2: String; const IgnoreCase: Boolean = True): Integer;
  155. { Compares two strings (typically filenames, but they don't have to be) and
  156. returns 0 for "equal", <0 for "less than", or >0 for "greater than".
  157. An ordinal comparison is used, ignoring case by default. }
  158. begin
  159. Result := PathStrCompare(PChar(S1), Length(S1), PChar(S2), Length(S2),
  160. IgnoreCase);
  161. end;
  162. function PathDrivePartLength(const Filename: String): Integer;
  163. begin
  164. Result := PathDrivePartLengthEx(Filename, False);
  165. end;
  166. function PathDrivePartLengthEx(const Filename: String;
  167. const IncludeSignificantSlash: Boolean): Integer;
  168. { Returns length of the drive portion of Filename, or 0 if there is no drive
  169. portion.
  170. If IncludeSignificantSlash is True, the drive portion can include a trailing
  171. slash if it is significant to the meaning of the path (i.e. 'x:' and 'x:\'
  172. are not equivalent, nor are '\' and '').
  173. If IncludeSignificantSlash is False, the function works as follows:
  174. 'x:file' -> 2 ('x:')
  175. 'x:\file' -> 2 ('x:')
  176. '\\server\share\file' -> 14 ('\\server\share')
  177. '\file' -> 0 ('')
  178. If IncludeSignificantSlash is True, the function works as follows:
  179. 'x:file' -> 2 ('x:')
  180. 'x:\file' -> 3 ('x:\')
  181. '\\server\share\file' -> 14 ('\\server\share')
  182. '\file' -> 1 ('\')
  183. }
  184. var
  185. Len, I, C: Integer;
  186. begin
  187. Len := Length(Filename);
  188. { \\server\share }
  189. if (Len >= 2) and PathCharIsSlash(Filename[1]) and PathCharIsSlash(Filename[2]) then begin
  190. I := 3;
  191. C := 0;
  192. while I <= Len do begin
  193. if PathCharIsSlash(Filename[I]) then begin
  194. Inc(C);
  195. if C >= 2 then
  196. Break;
  197. repeat
  198. Inc(I);
  199. { And skip any additional consecutive slashes: }
  200. until (I > Len) or not PathCharIsSlash(Filename[I]);
  201. end
  202. else
  203. Inc(I, PathCharLength(Filename, I));
  204. end;
  205. Result := I - 1;
  206. Exit;
  207. end;
  208. { \ }
  209. { Note: Test this before 'x:' since '\:stream' means access stream 'stream'
  210. on the root directory of the current drive, not access drive '\:' }
  211. if (Len >= 1) and PathCharIsSlash(Filename[1]) then begin
  212. if IncludeSignificantSlash then
  213. Result := 1
  214. else
  215. Result := 0;
  216. Exit;
  217. end;
  218. { x: }
  219. if Len > 0 then begin
  220. I := PathCharLength(Filename, 1) + 1;
  221. if (I <= Len) and (Filename[I] = ':') then begin
  222. if IncludeSignificantSlash and (I < Len) and PathCharIsSlash(Filename[I+1]) then
  223. Result := I+1
  224. else
  225. Result := I;
  226. Exit;
  227. end;
  228. end;
  229. Result := 0;
  230. end;
  231. function PathIsRooted(const Filename: String): Boolean;
  232. { Returns True if Filename begins with a slash or drive ('x:').
  233. Equivalent to: PathDrivePartLengthEx(Filename, True) <> 0 }
  234. var
  235. Len, I: Integer;
  236. begin
  237. Result := False;
  238. Len := Length(Filename);
  239. if Len > 0 then begin
  240. { \ or \\ }
  241. if PathCharIsSlash(Filename[1]) then
  242. Result := True
  243. else begin
  244. { x: }
  245. I := PathCharLength(Filename, 1) + 1;
  246. if (I <= Len) and (Filename[I] = ':') then
  247. Result := True;
  248. end;
  249. end;
  250. end;
  251. function PathPathPartLength(const Filename: String;
  252. const IncludeSlashesAfterPath: Boolean): Integer;
  253. { Returns length of the path portion of Filename, or 0 if there is no path
  254. portion.
  255. Note these differences from Delphi's ExtractFilePath function:
  256. - The result will never be less than what PathDrivePartLength returns.
  257. If you pass a UNC root path, e.g. '\\server\share', it will return the
  258. length of the entire string, NOT the length of '\\server\'.
  259. - If you pass in a filename with a reference to an NTFS alternate data
  260. stream, e.g. 'abc:def', it will return the length of the entire string,
  261. NOT the length of 'abc:'. }
  262. var
  263. LastCharToKeep, Len, I: Integer;
  264. begin
  265. Result := PathDrivePartLengthEx(Filename, True);
  266. LastCharToKeep := Result;
  267. Len := Length(Filename);
  268. I := Result + 1;
  269. while I <= Len do begin
  270. if PathCharIsSlash(Filename[I]) then begin
  271. if IncludeSlashesAfterPath then
  272. Result := I
  273. else
  274. Result := LastCharToKeep;
  275. Inc(I);
  276. end
  277. else begin
  278. Inc(I, PathCharLength(Filename, I));
  279. LastCharToKeep := I-1;
  280. end;
  281. end;
  282. end;
  283. function PathEndsWith(const S, AEndsWith: String;
  284. const IgnoreCase: Boolean = True): Boolean;
  285. { Returns True if S ends with (or is equal to) AEndsWith.
  286. An ordinal comparison is used, ignoring case by default. }
  287. begin
  288. Result := PathHasSubstringAt(S, AEndsWith, Length(S) - Length(AEndsWith),
  289. IgnoreCase);
  290. end;
  291. function PathExpand(const Filename: String; out ExpandedFilename: String): Boolean;
  292. { Like Delphi's ExpandFileName, but does proper error checking. }
  293. var
  294. Res: Integer;
  295. FilePart: PChar;
  296. Buf: array[0..4095] of Char;
  297. begin
  298. DWORD(Res) := GetFullPathName(PChar(Filename), SizeOf(Buf) div SizeOf(Buf[0]),
  299. Buf, FilePart);
  300. Result := (Res > 0) and (Res < SizeOf(Buf) div SizeOf(Buf[0]));
  301. if Result then begin
  302. SetString(ExpandedFilename, Buf, Res);
  303. { Memory usage optimization: Most of the time, no changes are made to the
  304. path. When that is the case, return a reference to the passed-in string
  305. so that there aren't two identical strings on the heap. }
  306. if ExpandedFilename = Filename then
  307. ExpandedFilename := Filename;
  308. end;
  309. end;
  310. function PathExpand(const Filename: String): String;
  311. begin
  312. if not PathExpand(Filename, Result) then
  313. Result := Filename;
  314. end;
  315. function PathExtensionPos(const Filename: String): Integer;
  316. { Returns index of the last '.' character in the filename portion of Filename,
  317. or 0 if there is no '.' in the filename portion.
  318. Note: Filename is assumed to NOT include an NTFS alternate data stream name
  319. (i.e. 'filename:stream'). }
  320. var
  321. Len, I: Integer;
  322. begin
  323. Result := 0;
  324. Len := Length(Filename);
  325. I := PathPathPartLength(Filename, True) + 1;
  326. while I <= Len do begin
  327. if Filename[I] = '.' then begin
  328. Result := I;
  329. Inc(I);
  330. end
  331. else
  332. Inc(I, PathCharLength(Filename, I));
  333. end;
  334. end;
  335. function PathExtractDir(const Filename: String): String;
  336. { Like PathExtractPath, but strips any trailing slashes, unless the resulting
  337. path is the root directory of a drive (i.e. 'C:\' or '\'). }
  338. var
  339. I: Integer;
  340. begin
  341. I := PathPathPartLength(Filename, False);
  342. Result := Copy(Filename, 1, I);
  343. end;
  344. function PathExtractDrive(const Filename: String): String;
  345. { Returns the drive portion of Filename (either 'x:' or '\\server\share'),
  346. or an empty string if there is no drive portion. }
  347. var
  348. L: Integer;
  349. begin
  350. L := PathDrivePartLength(Filename);
  351. if L = 0 then
  352. Result := ''
  353. else
  354. Result := Copy(Filename, 1, L);
  355. end;
  356. function PathExtractExt(const Filename: String): String;
  357. { Returns the extension portion of the last component of Filename (e.g. '.txt')
  358. or an empty string if there is no extension. }
  359. var
  360. I: Integer;
  361. begin
  362. I := PathExtensionPos(Filename);
  363. if I = 0 then
  364. Result := ''
  365. else
  366. Result := Copy(Filename, I, Maxint);
  367. end;
  368. function PathExtractName(const Filename: String): String;
  369. { Returns the filename portion of Filename (e.g. 'filename.txt'). If Filename
  370. ends in a slash or consists only of a drive part or is empty, the result will
  371. be an empty string.
  372. This function is essentially the opposite of PathExtractPath. }
  373. var
  374. I: Integer;
  375. begin
  376. I := PathPathPartLength(Filename, True);
  377. Result := Copy(Filename, I + 1, Maxint);
  378. end;
  379. function PathExtractPath(const Filename: String): String;
  380. { Returns the path portion of Filename (e.g. 'c:\dir\'). If Filename contains
  381. no drive part or slash, the result will be an empty string.
  382. This function is essentially the opposite of PathExtractName. }
  383. var
  384. I: Integer;
  385. begin
  386. I := PathPathPartLength(Filename, True);
  387. Result := Copy(Filename, 1, I);
  388. end;
  389. function PathHasInvalidCharacters(const S: String;
  390. const AllowDriveLetterColon: Boolean): Boolean;
  391. { Checks the specified path for characters that are never allowed in paths,
  392. or characters and path components that are accepted by the system but might
  393. present a security problem (such as '..' and sometimes ':').
  394. Specifically, True is returned if S includes any of the following:
  395. - Control characters (0-31)
  396. - One of these characters: /*?"<>|
  397. (This means forward slashes and the prefixes '\\?\' and '\??\' are never
  398. allowed.)
  399. - Colons (':'), except when AllowDriveLetterColon=True and the string's
  400. first character is a letter and the second character is the only colon.
  401. (This blocks NTFS alternate data stream names.)
  402. - A component with a trailing dot or space
  403. Due to the last rule above, '.' and '..' components are never allowed, nor
  404. are components like these:
  405. 'file '
  406. 'file.'
  407. 'file. . .'
  408. 'file . . '
  409. When expanding paths (with no '\\?\' prefix used), Windows 11 23H2 silently
  410. removes all trailing dots and spaces from the end of the string. Therefore,
  411. if used at the end of a path, all of the above cases yield just 'file'.
  412. On preceding components of the path, nothing is done with spaces; if there
  413. is exactly one dot at the end, it is removed (e.g., 'dir.\file' becomes
  414. 'dir\file'), while multiple dots are left untouched ('dir..\file' doesn't
  415. change).
  416. By rejecting trailing dots and spaces up front, we avoid all that weirdness
  417. and the problems that could arise from it.
  418. Since ':' is considered invalid (except in the one case noted above), it's
  419. not possible to sneak in disallowed dots/spaces by including an NTFS
  420. alternate data stream name. The function will return True in these cases:
  421. '..:streamname'
  422. 'file :streamname'
  423. }
  424. begin
  425. Result := True;
  426. for var I := Low(S) to High(S) do begin
  427. var C := S[I];
  428. if Ord(C) < 32 then
  429. Exit;
  430. case C of
  431. #32, '.':
  432. begin
  433. if (I = High(S)) or PathCharIsSlash(S[I+1]) then
  434. Exit;
  435. end;
  436. ':':
  437. begin
  438. { The A-Z check ensures that '.:streamname', ' :streamname', and
  439. '\:streamname' are disallowed. }
  440. if not AllowDriveLetterColon or (I <> Low(S)+1) or
  441. not CharInSet(S[Low(S)], ['A'..'Z', 'a'..'z']) then
  442. Exit;
  443. end;
  444. '/', '*', '?', '"', '<', '>', '|': Exit;
  445. end;
  446. end;
  447. Result := False;
  448. end;
  449. function PathHasSubstringAt(const S, Substring: String; const Offset: Integer;
  450. const IgnoreCase: Boolean = True): Boolean;
  451. { Returns True if Substring exists in S at the specified zero-based offset
  452. from the beginning of S.
  453. An ordinal comparison is used, ignoring case by default.
  454. Passing an out-of-range Offset value is allowed/safe. False is returned if
  455. Offset is negative or if checking for Substring at Offset would go beyond
  456. the end of S (partially or fully).
  457. If Substring is empty and Offset = Length(S), then True is returned because
  458. that is not considered going *beyond* the end of S. }
  459. begin
  460. if Offset < 0 then
  461. Exit(False);
  462. const SubstringLen = Length(Substring);
  463. if Offset > Length(S) - SubstringLen then
  464. Exit(False);
  465. Result := (PathStrCompare(PChar(S) + Offset, SubstringLen, PChar(Substring),
  466. SubstringLen, IgnoreCase) = 0);
  467. end;
  468. function PathLastChar(const S: String): PChar;
  469. { Returns pointer to last character in the string. Returns nil if the string is
  470. empty. }
  471. begin
  472. if S = '' then
  473. Result := nil
  474. else
  475. Result := @S[High(S)];
  476. end;
  477. function PathLastDelimiter(const Delimiters, S: string): Integer;
  478. { Returns the index of the last occurrence in S of one of the characters in
  479. Delimiters, or 0 if none were found.
  480. Note: S is allowed to contain null characters. }
  481. var
  482. P, E: PChar;
  483. begin
  484. Result := 0;
  485. if (S = '') or (Delimiters = '') then
  486. Exit;
  487. P := Pointer(S);
  488. E := P + Length(S);
  489. while P < E do begin
  490. if P^ <> #0 then begin
  491. if StrScan(PChar(Pointer(Delimiters)), P^) <> nil then
  492. Result := Integer((P - PChar(Pointer(S))) + 1);
  493. P := PathStrNextChar(P);
  494. end
  495. else
  496. Inc(P);
  497. end;
  498. end;
  499. function PathLowercase(const S: String): String;
  500. { Converts the specified path name to lowercase }
  501. begin
  502. Result := AnsiLowerCase(S);
  503. end;
  504. function PathPos(Ch: Char; const S: String): Integer;
  505. var
  506. Len, I: Integer;
  507. begin
  508. Len := Length(S);
  509. I := 1;
  510. while I <= Len do begin
  511. if S[I] = Ch then begin
  512. Result := I;
  513. Exit;
  514. end;
  515. Inc(I, PathCharLength(S, I));
  516. end;
  517. Result := 0;
  518. end;
  519. function PathNormalizeSlashes(S: String): String;
  520. { Returns S minus any superfluous slashes, and with any forward slashes
  521. converted to backslashes. For example, if S is 'C:\\\some//path', it returns
  522. 'C:\some\path'.
  523. If the string starts with two slashes ('\\') then those two characters are
  524. ignored when collapsing repeated slashes. So:
  525. \\server\share -> \\server\share (unchanged)
  526. \\\server\share -> \\\server\share (unchanged)
  527. \\\\server\share -> \\\server\share (one backslash removed)
  528. Note that paths with 3+ leading slashes don't actually work. But Windows'
  529. GetFullPathName function, used by PathExpand, collapses slashes the same
  530. way. Best to be consistent. }
  531. begin
  532. const Len = Length(S);
  533. var I: Integer;
  534. for I := 1 to Len do
  535. if S[I] = '/' then
  536. S[I] := '\';
  537. var EndIndex := 2;
  538. if (Len >= 2) and (S[1] = '\') and (S[2] = '\') then
  539. Inc(EndIndex, 2);
  540. for I := Len downto EndIndex do
  541. if (S[I] = '\') and (S[I-1] = '\') then
  542. Delete(S, I, 1);
  543. Result := S;
  544. end;
  545. function PathSame(const S1, S2: String): Boolean;
  546. { Returns True if the specified strings (typically filenames) are equal.
  547. An ordinal comparison is used, ignoring case.
  548. Like PathCompare, but faster for checking equality as it returns False
  549. immediately if the strings are different lengths. }
  550. begin
  551. Result := (Length(S1) = Length(S2)) and (PathCompare(S1, S2) = 0);
  552. end;
  553. function PathStartsWith(const S, AStartsWith: String;
  554. const IgnoreCase: Boolean = True): Boolean;
  555. { Returns True if S starts with (or is equal to) AStartsWith.
  556. An ordinal comparison is used, ignoring case by default. }
  557. begin
  558. Result := PathHasSubstringAt(S, AStartsWith, 0, IgnoreCase);
  559. end;
  560. { Use our own CompareStringOrdinal declaration. The one in the Windows unit is
  561. "delayload" (yuck), and the bIgnoreCase parameter type differs between
  562. Delphi 11 and 12 (BOOL vs. DWORD). }
  563. function CompareStringOrdinal_static(lpString1: LPCWSTR; cchCount1: Integer;
  564. lpString2: LPCWSTR; cchCount2: Integer; bIgnoreCase: BOOL): Integer; stdcall;
  565. external kernel32 name 'CompareStringOrdinal';
  566. function PathStrCompare(const S1: PChar; const S1Length: Integer;
  567. const S2: PChar; const S2Length: Integer;
  568. const IgnoreCase: Boolean = True): Integer;
  569. { Compares two strings and returns 0 for "equal", <0 for "less than", or
  570. >0 for "greater than".
  571. An ordinal comparison is used, ignoring case by default.
  572. A length of -1 may be passed if a string is null-terminated; in that case,
  573. the length is determined automatically. }
  574. begin
  575. { As documented, CompareStringOrdinal only allows 1 for TRUE in the
  576. bIgnoreCase parameter. "BOOL(Byte(IgnoreCase))" ensures we pass 1, not the
  577. usual -1 Delphi passes when a Boolean is implicitly converted to BOOL. }
  578. const CompareResult = CompareStringOrdinal_static(S1, S1Length, S2, S2Length,
  579. BOOL(Byte(IgnoreCase)));
  580. case CompareResult of
  581. 0: raise Exception.CreateFmt('PathStrCompare: CompareStringOrdinal failed (%u)',
  582. [GetLastError]);
  583. 1..3: ;
  584. else
  585. raise Exception.CreateFmt('PathStrCompare: CompareStringOrdinal result invalid (%d)',
  586. [CompareResult]);
  587. end;
  588. Result := CompareResult - 2;
  589. end;
  590. { Use our own FindStringOrdinal declaration. The one in the Windows unit is
  591. "delayload" (yuck). }
  592. function FindStringOrdinal_static(dwFindStringOrdinalFlags: DWORD;
  593. lpStringSource: LPCWSTR; cchSource: Integer;
  594. lpStringValue: LPCWSTR; cchValue: Integer; bIgnoreCase: BOOL): Integer; stdcall;
  595. external kernel32 name 'FindStringOrdinal';
  596. function PathStrFind(const SSource: PChar; const SSourceLength: Integer;
  597. const SValue: PChar; const SValueLength: Integer;
  598. const IgnoreCase: Boolean = True): Integer;
  599. { Locates a value in a string, starting with the first character of the string.
  600. Returns a 0-based index if found, and -1 otherwise.
  601. An ordinal comparison is used, ignoring case by default.
  602. A length of -1 may be passed if a string is null-terminated; in that case,
  603. the length is determined automatically. }
  604. begin
  605. { This is not documented for FindStringOrdinal, but like CompareStringOrdinal
  606. it only allows 1 for TRUE in the bIgnoreCase parameter. See above. }
  607. const CompareResult = FindStringOrdinal_static(FIND_FROMSTART, SSource, SSourceLength,
  608. SValue, SValueLength, BOOL(Byte(IgnoreCase)));
  609. if CompareResult = -1 then begin
  610. const LastError = GetLastError;
  611. if LastError <> ERROR_SUCCESS then
  612. raise Exception.CreateFmt('PathStrFind: FindStringOrdinal failed (%u)',
  613. [LastError]);
  614. end else if not ((CompareResult >= 0) and (CompareResult < SSourceLength)) then
  615. raise Exception.CreateFmt('PathStrFind: FindStringOrdinal result invalid (%d)',
  616. [CompareResult]);
  617. Result := CompareResult;
  618. end;
  619. function PathStrNextChar(const S: PChar): PChar;
  620. { Returns pointer to the character after S, unless S points to a null (#0). }
  621. begin
  622. Result := S;
  623. if Result^ <> #0 then
  624. Inc(Result);
  625. end;
  626. function PathStrPrevChar(const Start, Current: PChar): PChar;
  627. { Returns pointer to the character before Current, unless Current = Start. }
  628. begin
  629. Result := Current;
  630. if Result > Start then
  631. Dec(Result);
  632. end;
  633. function PathStrScan(const S: PChar; const C: Char): PChar;
  634. { Returns pointer to first occurrence of C in S, or nil if there are no
  635. occurrences. As with StrScan, specifying #0 for the search character is legal. }
  636. begin
  637. Result := S;
  638. while Result^ <> C do begin
  639. if Result^ = #0 then begin
  640. Result := nil;
  641. Break;
  642. end;
  643. Result := PathStrNextChar(Result);
  644. end;
  645. end;
  646. function RemoveBackslash(const S: String): String;
  647. { Returns S minus any trailing slashes. Use of this function is discouraged;
  648. use RemoveBackslashUnlessRoot instead when working with file system paths. }
  649. var
  650. I: Integer;
  651. begin
  652. I := Length(S);
  653. while (I > 0) and PathCharIsSlash(PathStrPrevChar(Pointer(S), @S[I+1])^) do
  654. Dec(I);
  655. if I = Length(S) then
  656. Result := S
  657. else
  658. Result := Copy(S, 1, I);
  659. end;
  660. function RemoveBackslashUnlessRoot(const S: String): String;
  661. { Returns S minus any trailing slashes, unless S specifies the root directory
  662. of a drive (i.e. 'C:\' or '\'), in which case it leaves 1 slash. }
  663. var
  664. DrivePartLen, I: Integer;
  665. begin
  666. DrivePartLen := PathDrivePartLengthEx(S, True);
  667. I := Length(S);
  668. while (I > DrivePartLen) and PathCharIsSlash(PathStrPrevChar(Pointer(S), @S[I+1])^) do
  669. Dec(I);
  670. if I = Length(S) then
  671. Result := S
  672. else
  673. Result := Copy(S, 1, I);
  674. end;
  675. function ValidateAndCombinePath(const ADestDir, AFilename: String;
  676. out AResultingPath: String): Boolean;
  677. { Combines ADestDir and AFilename without allowing a result outside of
  678. ADestDir and without allowing other security problems.
  679. Returns True if all security checks pass, with the combination of ADestDir
  680. and AFilename in AResultingPath.
  681. ADestDir is assumed to be normalized already and have a trailing backslash.
  682. AFilename may be a file or directory name. }
  683. begin
  684. { - Don't allow empty names
  685. - Don't allow forward slashes or repeated slashes
  686. - Don't allow rooted (non-relative to current directory) names
  687. - Don't allow trailing slash
  688. - Don't allow invalid characters/dots/spaces (this catches '..') }
  689. Result := False;
  690. if (AFilename <> '') and
  691. (AFilename = PathNormalizeSlashes(AFilename)) and
  692. not PathIsRooted(AFilename) and
  693. not PathCharIsSlash(AFilename[High(AFilename)]) and
  694. not PathHasInvalidCharacters(AFilename, False) then begin
  695. { Our validity checks passed. Now pass the combined path to PathExpand
  696. (GetFullPathName) to see if it thinks the path needs normalization.
  697. If the returned path isn't exactly what was passed in, then consider
  698. the name invalid.
  699. One way that can happen is if the path ends in an MS-DOS device name:
  700. PathExpand('c:\path\NUL') returns '\\.\NUL'. Obviously we don't want
  701. devices being opened, so that must be rejected. }
  702. var CombinedPath := ADestDir + AFilename;
  703. var TestExpandedPath: String;
  704. if PathExpand(CombinedPath, TestExpandedPath) and
  705. (CombinedPath = TestExpandedPath) then begin
  706. AResultingPath := CombinedPath;
  707. Result := True;
  708. end;
  709. end;
  710. end;
  711. function ValidateAndCombinePath(const ADestDir, AFilename: String): Boolean;
  712. begin
  713. var ResultingPath: String;
  714. Result := ValidateAndCombinePath(ADestDir, AFilename, ResultingPath);
  715. end;
  716. end.