PathFunc.pas 21 KB

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