PathFunc.pas 21 KB

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