PathFunc.pas 21 KB

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