Compression.SevenZipDecoder.pas 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. unit Compression.SevenZipDecoder;
  2. {
  3. Inno Setup
  4. Copyright (C) 1997-2024 Jordan Russell
  5. Portions by Martijn Laan
  6. For conditions of distribution and use, see LICENSE.TXT.
  7. Interface to the 7-Zip Decoder OBJ in Compression.SevenZipDecoder\7ZipDecode,
  8. used by Setup.
  9. }
  10. interface
  11. type
  12. TOnExtractionProgress = function(const ArchiveName, FileName: string; const Progress, ProgressMax: Int64): Boolean of object;
  13. procedure Extract7ZipArchive(const ArchiveFileName, DestDir: String;
  14. const FullPaths: Boolean; const OnExtractionProgress: TOnExtractionProgress);
  15. implementation
  16. uses
  17. Windows, SysUtils, Forms,
  18. PathFunc,
  19. Shared.SetupMessageIDs, SetupLdrAndSetup.Messages, Setup.LoggingFunc, Setup.MainFunc, Setup.InstFunc;
  20. type
  21. TSevenZipDecodeState = record
  22. ExpandedArchiveFileName, ExpandedDestDir: String;
  23. LogBuffer: AnsiString;
  24. ExtractedArchiveName: String;
  25. OnExtractionProgress: TOnExtractionProgress;
  26. LastReportedProgress, LastReportedProgressMax: UInt64;
  27. Aborted: Boolean;
  28. end;
  29. var
  30. State: TSevenZipDecodeState;
  31. { Compiled by Visual Studio 2022 using compile.bat
  32. To enable source debugging recompile using compile-bcc32c.bat and turn off the VISUALSTUDIO define below
  33. Note that in a speed test the code produced by bcc32c was about 33% slower }
  34. {$L Src\Compression.SevenZipDecoder\7zDecode\IS7zDec.obj}
  35. {$DEFINE VISUALSTUDIO}
  36. function IS_7zDec(const fileName: PChar; const fullPaths: Bool): Integer; cdecl; external name '_IS_7zDec';
  37. function ValidateAndCombinePath(const ADestDir, AFilename: String;
  38. out AResultingPath: String): Boolean;
  39. { Filenames read from archives aren't validated at all by the SDK's 7zMain.c,
  40. so we have to handle that ourself. Most importantly, we need to make sure a
  41. malicious archive cannot create files outside of the destination directory.
  42. Returns True if all security checks pass, with the combination of ADestDir
  43. and AFilename in AResultingPath.
  44. ADestDir is assumed to be normalized already and have a trailing backslash.
  45. AFilename may be a file or directory name. }
  46. begin
  47. { - Don't allow empty names
  48. - Don't allow forward slashes or repeated slashes
  49. (archives use '/' on disk but 7zMain.c changes them to '\')
  50. - Don't allow rooted (non-relative to current directory) names
  51. - Don't allow trailing slash
  52. - Don't allow invalid characters/dots/spaces (this catches '..') }
  53. Result := False;
  54. if (AFilename <> '') and
  55. (AFilename = PathNormalizeSlashes(AFilename)) and
  56. not PathIsRooted(AFilename) and
  57. not PathCharIsSlash(AFilename[High(AFilename)]) and
  58. not PathHasInvalidCharacters(AFilename, False) then begin
  59. { Our validity checks passed. Now pass the combined path to PathExpand
  60. (GetFullPathName) to see if it thinks the path needs normalization.
  61. If the returned path isn't exactly what was passed in, then consider
  62. the name invalid.
  63. One way that can happen is if the path ends in an MS-DOS device name:
  64. PathExpand('c:\path\NUL') returns '\\.\NUL'. Obviously we don't want
  65. devices being opened, so that must be rejected. }
  66. var CombinedPath := ADestDir + AFilename;
  67. var TestExpandedPath: String;
  68. if PathExpand(CombinedPath, TestExpandedPath) and
  69. (CombinedPath = TestExpandedPath) then begin
  70. AResultingPath := CombinedPath;
  71. Result := True;
  72. end;
  73. end;
  74. end;
  75. function __CreateDirectoryW(lpPathName: LPCWSTR;
  76. lpSecurityAttributes: PSecurityAttributes): BOOL; cdecl;
  77. begin
  78. var ExpandedDir: String;
  79. if ValidateAndCombinePath(State.ExpandedDestDir, lpPathName, ExpandedDir) then
  80. Result := CreateDirectoryW(PChar(ExpandedDir), lpSecurityAttributes)
  81. else begin
  82. Result := False;
  83. SetLastError(ERROR_ACCESS_DENIED);
  84. end;
  85. end;
  86. { Never actually called but still required by the linker }
  87. function __CreateFileA(lpFileName: LPCSTR; dwDesiredAccess, dwShareMode: DWORD;
  88. lpSecurityAttributes: PSecurityAttributes; dwCreationDisposition, dwFlagsAndAttributes: DWORD;
  89. hTemplateFile: THandle): THandle; cdecl;
  90. begin
  91. { Return an error if we do ever get called which is unwanted because it should
  92. use CreateFileW and not CreateFileA }
  93. Result := INVALID_HANDLE_VALUE;
  94. SetLastError(ERROR_INVALID_FUNCTION);
  95. end;
  96. function __CreateFileW(lpFileName: LPCWSTR; dwDesiredAccess, dwShareMode: DWORD;
  97. lpSecurityAttributes: PSecurityAttributes; dwCreationDisposition, dwFlagsAndAttributes: DWORD;
  98. hTemplateFile: THandle): THandle; cdecl;
  99. begin
  100. var ExpandedFileName: String;
  101. if ((dwDesiredAccess = GENERIC_READ) and
  102. PathExpand(lpFileName, ExpandedFileName) and
  103. (PathCompare(ExpandedFileName, State.ExpandedArchiveFileName) = 0)) or
  104. ((dwDesiredAccess = GENERIC_WRITE) and
  105. ValidateAndCombinePath(State.ExpandedDestDir, lpFileName, ExpandedFileName)) then
  106. Result := CreateFileW(PChar(ExpandedFileName), dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile)
  107. else begin
  108. Result := INVALID_HANDLE_VALUE;
  109. SetLastError(ERROR_ACCESS_DENIED);
  110. end;
  111. end;
  112. {$IFDEF VISUALSTUDIO}
  113. function __FileTimeToLocalFileTime(lpFileTime: PFileTime; var lpLocalFileTime: TFileTime): BOOL; cdecl;
  114. begin
  115. Result := FileTimeToLocalFileTime(lpFileTime, lpLocalFileTime);
  116. end;
  117. { Never actually called but still required by the linker }
  118. function __GetFileSize(hFile: THandle; lpFileSizeHigh: Pointer): DWORD; cdecl;
  119. begin
  120. Result := GetFileSize(hFile, lpFileSizeHigh);
  121. end;
  122. function __ReadFile(hFile: THandle; var Buffer; nNumberOfBytesToRead: DWORD;
  123. var lpNumberOfBytesRead: DWORD; lpOverlapped: POverlapped): BOOL; cdecl;
  124. begin
  125. Result := ReadFile(hFile, Buffer, nNumberOfBytesToRead, lpNumberOfBytesRead, lpOverlapped);
  126. end;
  127. function __SetFileAttributesW(lpFileName: LPCWSTR; dwFileAttributes: DWORD): BOOL; cdecl;
  128. begin
  129. Result := SetFileAttributesW(lpFileName, dwFileAttributes);
  130. end;
  131. function __SetFilePointer(hFile: THandle; lDistanceToMove: Longint;
  132. lpDistanceToMoveHigh: Pointer; dwMoveMethod: DWORD): DWORD; cdecl;
  133. begin
  134. Result := SetFilePointer(hFile, lDistanceToMove, lpDistanceToMoveHigh, dwMoveMethod);
  135. end;
  136. function __SetFileTime(hFile: THandle;
  137. lpCreationTime, lpLastAccessTime, lpLastWriteTime: PFileTime): BOOL; cdecl;
  138. begin
  139. Result := SetFileTime(hFile, lpCreationTime, lpLastAccessTime, lpLastWriteTime);
  140. end;
  141. function __WriteFile(hFile: THandle; const Buffer; nNumberOfBytesToWrite: DWORD;
  142. var lpNumberOfBytesWritten: DWORD; lpOverlapped: POverlapped): BOOL; cdecl;
  143. begin
  144. Result := WriteFile(hFile, Buffer, nNumberOfBytesToWrite, lpNumberOfBytesWritten, lpOverlapped);
  145. end;
  146. function __CloseHandle(hObject: THandle): BOOL; cdecl;
  147. begin
  148. Result := CloseHandle(hObject);
  149. end;
  150. function __GetLastError: DWORD; cdecl;
  151. begin
  152. Result := GetLastError;
  153. end;
  154. function __LocalFree(hMem: HLOCAL): HLOCAL; cdecl;
  155. begin
  156. Result := LocalFree(hMem);
  157. end;
  158. function __FormatMessageA(dwFlags: DWORD; lpSource: Pointer; dwMessageId: DWORD; dwLanguageId: DWORD;
  159. lpBuffer: LPSTR; nSize: DWORD; Arguments: Pointer): DWORD; cdecl;
  160. begin
  161. Result := FormatMessageA(dwFlags, lpSource, dwMessageId, dwLanguageId, lpBuffer, nSize, Arguments);
  162. end;
  163. function __WideCharToMultiByte(CodePage: UINT; dwFlags: DWORD;
  164. lpWideCharStr: LPWSTR; cchWideChar: Integer; lpMultiByteStr: LPSTR;
  165. cchMultiByte: Integer; lpDefaultChar: LPCSTR; lpUsedDefaultChar: PBOOL): Integer; cdecl;
  166. begin
  167. Result := WideCharToMultiByte(CodePage, dwFlags, lpWideCharStr, cchWideChar, lpMultiByteStr, cchMultiByte, lpDefaultChar, lpUsedDefaultChar);
  168. end;
  169. //https://github.com/rust-lang/compiler-builtins/issues/403
  170. procedure __allshl; register; external 'ntdll.dll' name '_allshl';
  171. procedure __aullshr; register; external 'ntdll.dll' name '_aullshr';
  172. {$ELSE}
  173. procedure __aullrem; stdcall; external 'ntdll.dll' name '_aullrem';
  174. procedure __aulldiv; stdcall; external 'ntdll.dll' name '_aulldiv';
  175. {$ENDIF}
  176. function _memcpy(dest, src: Pointer; n: Cardinal): Pointer; cdecl;
  177. begin
  178. Move(src^, dest^, n);
  179. Result := dest;
  180. end;
  181. function _memset(dest: Pointer; c: Integer; n: Cardinal): Pointer; cdecl;
  182. begin
  183. FillChar(dest^, n, c);
  184. Result := dest;
  185. end;
  186. function _malloc(size: NativeUInt): Pointer; cdecl;
  187. begin
  188. if size > NativeUInt(High(NativeInt)) then
  189. Result := nil
  190. else begin
  191. try
  192. GetMem(Result, NativeInt(size));
  193. except
  194. on EOutOfMemory do
  195. Result := nil;
  196. end;
  197. end;
  198. end;
  199. procedure _free(address: Pointer); cdecl;
  200. begin
  201. FreeMem(address);
  202. end;
  203. function _wcscmp(string1, string2: PChar): Integer; cdecl;
  204. begin
  205. Result := StrComp(string1, string2);
  206. end;
  207. procedure Log(const S: AnsiString);
  208. begin
  209. if S <> '' then
  210. Setup.LoggingFunc.Log(UTF8ToString(S));
  211. end;
  212. function __fputs(str: PAnsiChar; unused: Pointer): Integer; cdecl;
  213. function FindNewLine(const S: AnsiString): Integer;
  214. begin
  215. { 7zMain.c always sends #10 as newline but its call to FormatMessage can cause #13#10 anyway }
  216. var N := Length(S);
  217. for var I := 1 to N do
  218. if CharInSet(S[I], [#13, #10]) then
  219. Exit(I);
  220. Result := 0;
  221. end;
  222. begin
  223. try
  224. State.LogBuffer := State.LogBuffer + str;
  225. var P := FindNewLine(State.LogBuffer);
  226. while P <> 0 do begin
  227. Log(Copy(State.LogBuffer, 1, P-1));
  228. if (State.LogBuffer[P] = #13) and (P < Length(State.LogBuffer)) and (State.LogBuffer[P+1] = #10) then
  229. Inc(P);
  230. Delete(State.LogBuffer, 1, P);
  231. P := FindNewLine(State.LogBuffer);
  232. end;
  233. Result := 0;
  234. except
  235. Result := -1; { EOF }
  236. end;
  237. end;
  238. procedure _ReportProgress(const FileName: PChar; const Progress, ProgressMax: UInt64; var Abort: Bool); cdecl;
  239. begin
  240. if Assigned(State.OnExtractionProgress) then begin
  241. { Make sure script isn't called crazy often because that would slow the download significantly. Only report:
  242. -At start or finish
  243. -Or if somehow Progress decreased or Max changed
  244. -Or if at least 512 KB progress was made since last report
  245. }
  246. if (Progress = 0) or (Progress = ProgressMax) or
  247. (Progress < State.LastReportedProgress) or (ProgressMax <> State.LastReportedProgressMax) or
  248. ((Progress - State.LastReportedProgress) > 524288) then begin
  249. try
  250. if not State.OnExtractionProgress(State.ExtractedArchiveName, FileName, Progress, ProgressMax) then
  251. Abort := True;
  252. finally
  253. State.LastReportedProgress := Progress;
  254. State.LastReportedProgressMax := ProgressMax;
  255. end;
  256. end;
  257. end;
  258. if not Abort and DownloadTemporaryFileOrExtract7ZipArchiveProcessMessages then
  259. Application.ProcessMessages;
  260. if Abort then
  261. State.Aborted := True;
  262. end;
  263. procedure Extract7ZipArchive(const ArchiveFileName, DestDir: String;
  264. const FullPaths: Boolean; const OnExtractionProgress: TOnExtractionProgress);
  265. begin
  266. if ArchiveFileName = '' then
  267. InternalError('Extract7ZipArchive: Invalid ArchiveFileName value');
  268. if DestDir = '' then
  269. InternalError('Extract7ZipArchive: Invalid DestDir value');
  270. LogFmt('Extracting 7-Zip archive %s to %s. Full paths? %s', [ArchiveFileName, DestDir, SYesNo[FullPaths]]);
  271. var SaveCurDir := GetCurrentDir;
  272. if not ForceDirectories(False, DestDir) or not SetCurrentDir(DestDir) then
  273. raise Exception.Create(FmtSetupMessage(msgErrorExtractionFailed, ['-1']));
  274. try
  275. State.ExpandedArchiveFileName := PathExpand(ArchiveFileName);
  276. State.ExpandedDestDir := AddBackslash(PathExpand(DestDir));
  277. State.LogBuffer := '';
  278. State.ExtractedArchiveName := PathExtractName(ArchiveFileName);
  279. State.OnExtractionProgress := OnExtractionProgress;
  280. State.LastReportedProgress := 0;
  281. State.LastReportedProgressMax := 0;
  282. State.Aborted := False;
  283. var Res := IS_7zDec(PChar(ArchiveFileName), FullPaths);
  284. if State.LogBuffer <> '' then
  285. Log(State.LogBuffer);
  286. if State.Aborted then
  287. raise Exception.Create(SetupMessages[msgErrorExtractionAborted])
  288. else if Res <> 0 then
  289. raise Exception.Create(FmtSetupMessage(msgErrorExtractionFailed, [Res.ToString]))
  290. finally
  291. SetCurrentDir(SaveCurDir);
  292. end;
  293. end;
  294. end.