PathFunc.pas 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587
  1. unit PathFunc;
  2. {
  3. Inno Setup
  4. Copyright (C) 1997-2010 Jordan Russell
  5. Portions by Martijn Laan
  6. For conditions of distribution and use, see LICENSE.TXT.
  7. This unit provides some path-related, MBCS-aware functions.
  8. These functions should always be used in lieu of their SysUtils counterparts
  9. since they aren't MBCS-aware on Delphi 2, and sometimes not MBCS-aware on
  10. Delphi 6 and 7 either (see QC#5096).
  11. $jrsoftware: issrc/Components/PathFunc.pas,v 1.43 2010/04/19 21:43:01 jr Exp $
  12. }
  13. interface
  14. function AddBackslash(const S: String): String;
  15. function PathChangeExt(const Filename, Extension: String): String;
  16. function PathCharCompare(const S1, S2: PChar): Boolean;
  17. function PathCharIsSlash(const C: Char): Boolean;
  18. function PathCharIsTrailByte(const S: String; const Index: Integer): Boolean;
  19. function PathCharLength(const S: String; const Index: Integer): Integer;
  20. function PathCombine(const Dir, Filename: String): String;
  21. function PathCompare(const S1, S2: String): Integer;
  22. function PathDrivePartLength(const Filename: String): Integer;
  23. function PathDrivePartLengthEx(const Filename: String;
  24. const IncludeSignificantSlash: Boolean): Integer;
  25. function PathExpand(const Filename: String): String;
  26. function PathExtensionPos(const Filename: String): Integer;
  27. function PathExtractDir(const Filename: String): String;
  28. function PathExtractDrive(const Filename: String): String;
  29. function PathExtractExt(const Filename: String): String;
  30. function PathExtractName(const Filename: String): String;
  31. function PathExtractPath(const Filename: String): String;
  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. implementation
  47. uses
  48. Windows, SysUtils;
  49. function AddBackslash(const S: String): String;
  50. { Returns S plus a trailing backslash, unless S is an empty string or already
  51. ends in a backslash/slash. }
  52. begin
  53. if (S <> '') and not PathCharIsSlash(PathLastChar(S)^) then
  54. Result := S + '\'
  55. else
  56. Result := S;
  57. end;
  58. function PathCharLength(const S: String; const Index: Integer): Integer;
  59. { Returns the length in bytes of the character at Index in S.
  60. Notes:
  61. 1. If Index specifies the last character in S, 1 will always be returned,
  62. even if the last character is a lead byte.
  63. 2. If a lead byte is followed by a null character (e.g. #131#0), 2 will be
  64. returned. This mimics the behavior of MultiByteToWideChar and CharPrev,
  65. but not CharNext(P)-P, which would stop on the null. }
  66. begin
  67. {$IFNDEF UNICODE}
  68. if IsDBCSLeadByte(Ord(S[Index])) and (Index < Length(S)) then
  69. Result := 2
  70. else
  71. {$ENDIF}
  72. Result := 1;
  73. end;
  74. function PathCharIsSlash(const C: Char): Boolean;
  75. { Returns True if C is a backslash or slash. }
  76. begin
  77. Result := (C = '\') or (C = '/');
  78. end;
  79. function PathCharIsTrailByte(const S: String; const Index: Integer): Boolean;
  80. { Returns False if S[Index] is a single byte character or a lead byte.
  81. Returns True otherwise (i.e. it must be a trail byte). }
  82. var
  83. I: Integer;
  84. begin
  85. I := 1;
  86. while I <= Index do begin
  87. if I = Index then begin
  88. Result := False;
  89. Exit;
  90. end;
  91. Inc(I, PathCharLength(S, I));
  92. end;
  93. Result := True;
  94. end;
  95. function PathCharCompare(const S1, S2: PChar): Boolean;
  96. { Compares two first characters, and returns True if they are equal. }
  97. var
  98. N, I: Integer;
  99. begin
  100. N := PathStrNextChar(S1) - S1;
  101. if N = PathStrNextChar(S2) - S2 then begin
  102. for I := 0 to N-1 do begin
  103. if S1[I] <> S2[I] then begin
  104. Result := False;
  105. Exit;
  106. end;
  107. end;
  108. Result := True;
  109. end else
  110. Result := False;
  111. end;
  112. function PathChangeExt(const Filename, Extension: String): String;
  113. { Takes Filename, removes any existing extension, then adds the extension
  114. specified by Extension and returns the resulting string. }
  115. var
  116. I: Integer;
  117. begin
  118. I := PathExtensionPos(Filename);
  119. if I = 0 then
  120. Result := Filename + Extension
  121. else
  122. Result := Copy(Filename, 1, I - 1) + Extension;
  123. end;
  124. function PathCombine(const Dir, Filename: String): String;
  125. { Combines a directory and filename into a path.
  126. If Dir is empty, it just returns Filename.
  127. If Filename is empty, it returns an empty string (ignoring Dir).
  128. If Filename begins with a drive letter or slash, it returns Filename
  129. (ignoring Dir).
  130. If Dir specifies only a drive letter and colon ('c:'), it returns
  131. Dir + Filename.
  132. Otherwise, it returns the equivalent of AddBackslash(Dir) + Filename. }
  133. var
  134. I: Integer;
  135. begin
  136. if (Dir = '') or (Filename = '') or PathIsRooted(Filename) then
  137. Result := Filename
  138. else begin
  139. I := PathCharLength(Dir, 1) + 1;
  140. if ((I = Length(Dir)) and (Dir[I] = ':')) or
  141. PathCharIsSlash(PathLastChar(Dir)^) then
  142. Result := Dir + Filename
  143. else
  144. Result := Dir + '\' + Filename;
  145. end;
  146. end;
  147. function PathCompare(const S1, S2: String): Integer;
  148. { Compares two filenames, and returns 0 if they are equal. }
  149. begin
  150. Result := CompareStr(PathLowercase(S1), PathLowercase(S2));
  151. end;
  152. function PathDrivePartLength(const Filename: String): Integer;
  153. begin
  154. Result := PathDrivePartLengthEx(Filename, False);
  155. end;
  156. function PathDrivePartLengthEx(const Filename: String;
  157. const IncludeSignificantSlash: Boolean): Integer;
  158. { Returns length of the drive portion of Filename, or 0 if there is no drive
  159. portion.
  160. If IncludeSignificantSlash is True, the drive portion can include a trailing
  161. slash if it is significant to the meaning of the path (i.e. 'x:' and 'x:\'
  162. are not equivalent, nor are '\' and '').
  163. If IncludeSignificantSlash is False, the function works as follows:
  164. 'x:file' -> 2 ('x:')
  165. 'x:\file' -> 2 ('x:')
  166. '\\server\share\file' -> 14 ('\\server\share')
  167. '\file' -> 0 ('')
  168. If IncludeSignificantSlash is True, the function works as follows:
  169. 'x:file' -> 2 ('x:')
  170. 'x:\file' -> 3 ('x:\')
  171. '\\server\share\file' -> 14 ('\\server\share')
  172. '\file' -> 1 ('\')
  173. Note: This is MBCS-safe, unlike the Delphi's ExtractFileDrive function.
  174. (Computer and share names can include multi-byte characters!) }
  175. var
  176. Len, I, C: Integer;
  177. begin
  178. Len := Length(Filename);
  179. { \\server\share }
  180. if (Len >= 2) and PathCharIsSlash(Filename[1]) and PathCharIsSlash(Filename[2]) then begin
  181. I := 3;
  182. C := 0;
  183. while I <= Len do begin
  184. if PathCharIsSlash(Filename[I]) then begin
  185. Inc(C);
  186. if C >= 2 then
  187. Break;
  188. repeat
  189. Inc(I);
  190. { And skip any additional consecutive slashes: }
  191. until (I > Len) or not PathCharIsSlash(Filename[I]);
  192. end
  193. else
  194. Inc(I, PathCharLength(Filename, I));
  195. end;
  196. Result := I - 1;
  197. Exit;
  198. end;
  199. { \ }
  200. { Note: Test this before 'x:' since '\:stream' means access stream 'stream'
  201. on the root directory of the current drive, not access drive '\:' }
  202. if (Len >= 1) and PathCharIsSlash(Filename[1]) then begin
  203. if IncludeSignificantSlash then
  204. Result := 1
  205. else
  206. Result := 0;
  207. Exit;
  208. end;
  209. { x: }
  210. if Len > 0 then begin
  211. I := PathCharLength(Filename, 1) + 1;
  212. if (I <= Len) and (Filename[I] = ':') then begin
  213. if IncludeSignificantSlash and (I < Len) and PathCharIsSlash(Filename[I+1]) then
  214. Result := I+1
  215. else
  216. Result := I;
  217. Exit;
  218. end;
  219. end;
  220. Result := 0;
  221. end;
  222. function PathIsRooted(const Filename: String): Boolean;
  223. { Returns True if Filename begins with a slash or drive ('x:').
  224. Equivalent to: PathDrivePartLengthEx(Filename, True) <> 0 }
  225. var
  226. Len, I: Integer;
  227. begin
  228. Result := False;
  229. Len := Length(Filename);
  230. if Len > 0 then begin
  231. { \ or \\ }
  232. if PathCharIsSlash(Filename[1]) then
  233. Result := True
  234. else begin
  235. { x: }
  236. I := PathCharLength(Filename, 1) + 1;
  237. if (I <= Len) and (Filename[I] = ':') then
  238. Result := True;
  239. end;
  240. end;
  241. end;
  242. function PathPathPartLength(const Filename: String;
  243. const IncludeSlashesAfterPath: Boolean): Integer;
  244. { Returns length of the path portion of Filename, or 0 if there is no path
  245. portion.
  246. Note these differences from Delphi's ExtractFilePath function:
  247. - The result will never be less than what PathDrivePartLength returns.
  248. If you pass a UNC root path, e.g. '\\server\share', it will return the
  249. length of the entire string, NOT the length of '\\server\'.
  250. - If you pass in a filename with a reference to an NTFS alternate data
  251. stream, e.g. 'abc:def', it will return the length of the entire string,
  252. NOT the length of 'abc:'. }
  253. var
  254. LastCharToKeep, Len, I: Integer;
  255. begin
  256. Result := PathDrivePartLengthEx(Filename, True);
  257. LastCharToKeep := Result;
  258. Len := Length(Filename);
  259. I := Result + 1;
  260. while I <= Len do begin
  261. if PathCharIsSlash(Filename[I]) then begin
  262. if IncludeSlashesAfterPath then
  263. Result := I
  264. else
  265. Result := LastCharToKeep;
  266. Inc(I);
  267. end
  268. else begin
  269. Inc(I, PathCharLength(Filename, I));
  270. LastCharToKeep := I-1;
  271. end;
  272. end;
  273. end;
  274. function PathExpand(const Filename: String): String;
  275. { Like Delphi's ExpandFileName, but does proper error checking. }
  276. var
  277. Res: Integer;
  278. FilePart: PChar;
  279. Buf: array[0..4095] of Char;
  280. begin
  281. DWORD(Res) := GetFullPathName(PChar(Filename), SizeOf(Buf) div SizeOf(Buf[0]),
  282. Buf, FilePart);
  283. if (Res > 0) and (Res < SizeOf(Buf) div SizeOf(Buf[0])) then
  284. SetString(Result, Buf, Res)
  285. else
  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, the result will be an empty
  344. 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 PathLastChar(const S: String): PChar;
  363. { Returns pointer to last character in the string. Is MBCS-aware. Returns nil
  364. if the string is empty. }
  365. begin
  366. if S = '' then
  367. Result := nil
  368. else
  369. Result := PathStrPrevChar(Pointer(S), @S[Length(S)+1]);
  370. end;
  371. function PathLastDelimiter(const Delimiters, S: string): Integer;
  372. { Returns the index of the last occurrence in S of one of the characters in
  373. Delimiters, or 0 if none were found.
  374. Note: S is allowed to contain null characters. }
  375. var
  376. P, E: PChar;
  377. begin
  378. Result := 0;
  379. if (S = '') or (Delimiters = '') then
  380. Exit;
  381. P := Pointer(S);
  382. E := @P[Length(S)];
  383. while P < E do begin
  384. if P^ <> #0 then begin
  385. if StrScan(PChar(Pointer(Delimiters)), P^) <> nil then
  386. Result := (P - PChar(Pointer(S))) + 1;
  387. P := PathStrNextChar(P);
  388. end
  389. else
  390. Inc(P);
  391. end;
  392. end;
  393. function PathLowercase(const S: String): String;
  394. { Converts the specified path name to lowercase }
  395. {$IFNDEF UNICODE}
  396. var
  397. I, L: Integer;
  398. {$ENDIF}
  399. begin
  400. {$IFNDEF UNICODE}
  401. if (Win32Platform <> VER_PLATFORM_WIN32_NT) and
  402. (GetSystemMetrics(SM_DBCSENABLED) <> 0) then begin
  403. { Japanese Windows 98's handling of double-byte Roman characters in
  404. filenames is case sensitive, so we can't change the case of double-byte
  405. characters. (Japanese Windows NT/2000 is case insensitive, on both FAT
  406. and NTFS, in my tests.) Based on code from AnsiLowerCaseFileName. }
  407. Result := S;
  408. L := Length(Result);
  409. I := 1;
  410. while I <= L do begin
  411. if Result[I] in ['A'..'Z'] then begin
  412. Inc(Byte(Result[I]), 32);
  413. Inc(I);
  414. end
  415. else
  416. Inc(I, PathCharLength(Result, I));
  417. end;
  418. end
  419. else
  420. {$ENDIF}
  421. Result := AnsiLowerCase(S);
  422. end;
  423. function PathPos(Ch: Char; const S: String): Integer;
  424. { This is an MBCS-aware Pos function. }
  425. var
  426. Len, I: Integer;
  427. begin
  428. Len := Length(S);
  429. I := 1;
  430. while I <= Len do begin
  431. if S[I] = Ch then begin
  432. Result := I;
  433. Exit;
  434. end;
  435. Inc(I, PathCharLength(S, I));
  436. end;
  437. Result := 0;
  438. end;
  439. function PathNormalizeSlashes(const S: String): String;
  440. { Returns S minus any superfluous slashes, and with any forward slashes
  441. converted to backslashes. For example, if S is 'C:\\\some//path', it returns
  442. 'C:\some\path'. Does not remove a double backslash at the beginning of the
  443. string, since that signifies a UNC path. }
  444. var
  445. Len, I: Integer;
  446. begin
  447. Result := S;
  448. Len := Length(Result);
  449. I := 1;
  450. while I <= Len do begin
  451. if Result[I] = '/' then
  452. Result[I] := '\';
  453. Inc(I, PathCharLength(Result, I));
  454. end;
  455. I := 1;
  456. while I < Length(Result) do begin
  457. if (Result[I] = '\') and (Result[I+1] = '\') and (I > 1) then
  458. Delete(Result, I+1, 1)
  459. else
  460. Inc(I, PathCharLength(Result, I));
  461. end;
  462. end;
  463. function PathStartsWith(const S, AStartsWith: String): Boolean;
  464. { Returns True if S starts with (or is equal to) AStartsWith. Uses path casing
  465. rules, and is MBCS-aware. }
  466. var
  467. AStartsWithLen: Integer;
  468. begin
  469. AStartsWithLen := Length(AStartsWith);
  470. if Length(S) = AStartsWithLen then
  471. Result := (PathCompare(S, AStartsWith) = 0)
  472. else if (Length(S) > AStartsWithLen) and not PathCharIsTrailByte(S, AStartsWithLen+1) then
  473. Result := (PathCompare(Copy(S, 1, AStartsWithLen), AStartsWith) = 0)
  474. else
  475. Result := False;
  476. end;
  477. function PathStrNextChar(const S: PChar): PChar;
  478. { Returns pointer to the character after S, unless S points to a null (#0).
  479. Is MBCS-aware. }
  480. begin
  481. {$IFNDEF UNICODE}
  482. Result := CharNext(S);
  483. {$ELSE}
  484. Result := S;
  485. if Result^ <> #0 then
  486. Inc(Result);
  487. {$ENDIF}
  488. end;
  489. function PathStrPrevChar(const Start, Current: PChar): PChar;
  490. { Returns pointer to the character before Current, unless Current = Start.
  491. Is MBCS-aware. }
  492. begin
  493. {$IFNDEF UNICODE}
  494. Result := CharPrev(Start, Current);
  495. {$ELSE}
  496. Result := Current;
  497. if Result > Start then
  498. Dec(Result);
  499. {$ENDIF}
  500. end;
  501. function PathStrScan(const S: PChar; const C: Char): PChar;
  502. { Returns pointer to first occurrence of C in S, or nil if there are no
  503. occurrences. Like StrScan, but MBCS-aware.
  504. Note: As with StrScan, specifying #0 for the search character is legal. }
  505. begin
  506. Result := S;
  507. while Result^ <> C do begin
  508. if Result^ = #0 then begin
  509. Result := nil;
  510. Break;
  511. end;
  512. Result := PathStrNextChar(Result);
  513. end;
  514. end;
  515. function RemoveBackslash(const S: String): String;
  516. { Returns S minus any trailing slashes. Use of this function is discouraged;
  517. use RemoveBackslashUnlessRoot instead when working with file system paths. }
  518. var
  519. I: Integer;
  520. begin
  521. I := Length(S);
  522. while (I > 0) and PathCharIsSlash(PathStrPrevChar(Pointer(S), @S[I+1])^) do
  523. Dec(I);
  524. if I = Length(S) then
  525. Result := S
  526. else
  527. Result := Copy(S, 1, I);
  528. end;
  529. function RemoveBackslashUnlessRoot(const S: String): String;
  530. { Returns S minus any trailing slashes, unless S specifies the root directory
  531. of a drive (i.e. 'C:\' or '\'), in which case it leaves 1 slash. }
  532. var
  533. DrivePartLen, I: Integer;
  534. begin
  535. DrivePartLen := PathDrivePartLengthEx(S, True);
  536. I := Length(S);
  537. while (I > DrivePartLen) and PathCharIsSlash(PathStrPrevChar(Pointer(S), @S[I+1])^) do
  538. Dec(I);
  539. if I = Length(S) then
  540. Result := S
  541. else
  542. Result := Copy(S, 1, I);
  543. end;
  544. end.