Browse Source

Merge branch 'files-extractarchive'

Martijn Laan 2 months ago
parent
commit
7f6278c99b

+ 4 - 4
Components/ISSigFunc.pas

@@ -17,11 +17,11 @@ uses
 
 type
   TISSigVerifySignatureResult = (vsrSuccess, vsrMalformed, vsrKeyNotFound,
-    vsrBadSignature);
+    vsrBad);
   TISSigImportKeyResult = (ikrSuccess, ikrMalformed, ikrNotPrivateKey);
   TISSigVerifySignatureFileMissingErrorProc = reference to procedure(const Filename: String);
   TISSigVerifySignatureSigFileMissingErrorProc = reference to procedure(const Filename, SigFilename: String);
-  TISSigVerifySignatureVerificationFailedErrorProc = reference to procedure(const SigFilename: String; const VerifyResult: TISSigVerifySignatureResult);
+  TISSigVerifySignatureVerificationFailedErrorProc = reference to procedure(const Filename, SigFilename: String; const VerifyResult: TISSigVerifySignatureResult);
 
 { Preferred, hardened functions for loading/saving .issig and key file text }
 function ISSigLoadTextFromFile(const AFilename: String): String;
@@ -253,7 +253,7 @@ begin
     AFileHash := UnverifiedFileHash;
     Result := vsrSuccess;
   end else
-    Result := vsrBadSignature;
+    Result := vsrBad;
 end;
 
 function ISSigVerifySignatureText(const AAllowedKeys: array of TECDSAKey;
@@ -297,7 +297,7 @@ begin
     AExpectedFileSize, AExpectedFileHash, AKeyUsedID);
   Result := VerifyResult = vsrSuccess;
   if not Result and Assigned(AVerificationFailedErrorProc) then
-    AVerificationFailedErrorProc(SigFilename, VerifyResult);
+    AVerificationFailedErrorProc(AFilename, SigFilename, VerifyResult);
 end;
 
 function ISSigVerifySignature(const AFilename: String; const AAllowedKeys: array of TECDSAKey;

+ 3 - 1
Components/Lzma2/Util/7z/7zMain.c

@@ -5,7 +5,7 @@
    -Use CP_UTF8 in PrintString
    -Fix Utf16_To_Char to handle CP_UTF7 and CP_UTF8's special rules 
    -Change main to mainW to support Unicode archive names
-   -Add specific error text for SZ_ERROR_ARCHIVE, SZ_ERROR_NO_ARCHIVE, and SZ_ERROR_PROGRESS
+   -Add specific error text for SZ_ERROR_DATA, SZ_ERROR_ARCHIVE, SZ_ERROR_NO_ARCHIVE, and SZ_ERROR_PROGRESS
    -Return res on errors instead of always returning 1
    -Add optional progress reporting with abort option
    -Add optional output of SzArEx_Extract's output buffer sizes
@@ -967,6 +967,8 @@ int Z7_CDECL mainW(int numargs, WCHAR *args[])
     PrintError("decoder doesn't support this archive");
   else if (res == SZ_ERROR_MEM)
     PrintError("cannot allocate memory");
+  else if (res == SZ_ERROR_DATA)
+    PrintError("Data error");
   else if (res == SZ_ERROR_CRC)
     PrintError("CRC error");
   else if (res == SZ_ERROR_ARCHIVE)

+ 10 - 3
Components/PathFunc.pas

@@ -47,7 +47,8 @@ function PathStrScan(const S: PChar; const C: Char): PChar;
 function RemoveBackslash(const S: String): String;
 function RemoveBackslashUnlessRoot(const S: String): String;
 function ValidateAndCombinePath(const ADestDir, AFilename: String;
-  out AResultingPath: String): Boolean;
+  out AResultingPath: String): Boolean; overload;
+function ValidateAndCombinePath(const ADestDir, AFilename: String): Boolean; overload;
 
 implementation
 
@@ -364,8 +365,8 @@ end;
 
 function PathExtractName(const Filename: String): String;
 { Returns the filename portion of Filename (e.g. 'filename.txt'). If Filename
-  ends in a slash or consists only of a drive part, the result will be an empty
-  string.
+  ends in a slash or consists only of a drive part or is empty, the result will
+  be an empty string.
   This function is essentially the opposite of PathExtractPath. }
 var
   I: Integer;
@@ -641,4 +642,10 @@ begin
   end;
 end;
 
+function ValidateAndCombinePath(const ADestDir, AFilename: String): Boolean;
+begin
+  var ResultingPath: String;
+  Result := ValidateAndCombinePath(ADestDir, AFilename, ResultingPath);
+end;
+
 end.

+ 1 - 1
Components/TrustFunc.pas

@@ -93,7 +93,7 @@ begin
       begin
         raise Exception.CreateFmt('Signature file "%s" does not exist', [SigFileName]);
       end,
-      procedure(const SigFilename: String; const VerifyResult: TISSigVerifySignatureResult)
+      procedure(const Filename, SigFilename: String; const VerifyResult: TISSigVerifySignatureResult)
       begin
         raise Exception.CreateFmt('Signature file "%s" is not valid', [SigFileName]);
       end

+ 39 - 12
Examples/CodeDownloadFiles.iss

@@ -1,11 +1,13 @@
 ; -- CodeDownloadFiles.iss --
 ;
 ; This script shows how the CreateDownloadPage support function can be used to
-; download temporary files while showing the download progress to the user.
+; download temporary files and archives while showing the download and extraction
+; progress to the user.
 ;
 ; To verify the downloaded files, this script shows two methods:
-; -For innosetup-latest.exe: using the Inno Setup Signature Tool, the [ISSigKeys]
-;  section, and the issigverify flag
+; -For innosetup-latest.exe and MyProg-ExtraReadmes.7z: using Inno Setup
+;  Signature Tool, the [ISSigKeys] section, and the AddWithISSigVerify support
+;  function
 ; -For iscrypt.dll: using a simple SHA256 check
 ; Using the Inno Setup Signature Tool has the benefit that the script does not
 ; need to be changed when the downloaded file changes, so any installers built
@@ -19,20 +21,25 @@ DefaultDirName={autopf}\My Program
 DefaultGroupName=My Program
 UninstallDisplayIcon={app}\MyProg.exe
 OutputDir=userdocs:Inno Setup Examples Output
+;Use "ArchiveExtraction=enhanced" if your archive has a password
+;Use "ArchiveExtraction=full" if your archive is not a .7z file but for example a .zip file
+ArchiveExtraction=enhanced/nopassword
 
 [ISSigKeys]
-Name: "mykey"; \
+Name: "mykey"; RuntimeID: "def02"; \
   KeyID:   "def020edee3c4835fd54d85eff8b66d4d899b22a777353ca4a114b652e5e7a28"; \
   PublicX: "515dc7d6c16d4a46272ceb3d158c5630a96466ab4d948e72c2029d737c823097"; \
   PublicY: "f3c21f6b5156c52a35f6f28016ee3e31a3ded60c325b81fb7b1f88c221081a61"
 
 [Files]
 ; Place any regular files here
-Source: "MyProg.exe"; DestDir: "{app}";
-Source: "MyProg.chm"; DestDir: "{app}";
-Source: "Readme.txt"; DestDir: "{app}"; Flags: isreadme;
-; These files will be downloaded
+Source: "MyProg.exe"; DestDir: "{app}"
+Source: "MyProg.chm"; DestDir: "{app}"
+Source: "Readme.txt"; DestDir: "{app}"; Flags: isreadme
+; These files will be downloaded. If you include flag issigverify here the file will be verified
+; a second time while copying. Verification while copying is efficient, except for archives.
 Source: "{tmp}\innosetup-latest.exe"; DestDir: "{app}"; Flags: external ignoreversion issigverify
+Source: "{tmp}\MyProg-ExtraReadmes.7z"; DestDir: "{app}"; Flags: external extractarchive recursesubdirs ignoreversion
 Source: "{tmp}\ISCrypt.dll"; DestDir: "{app}"; Flags: external ignoreversion
 
 [Icons]
@@ -41,21 +48,41 @@ Name: "{group}\My Program"; Filename: "{app}\MyProg.exe"
 [Code]
 var
   DownloadPage: TDownloadWizardPage;
+  AllowedKeysRuntimeIDs: TStringList;
 
 procedure InitializeWizard;
 begin
   DownloadPage := CreateDownloadPage(SetupMessage(msgWizardPreparing), SetupMessage(msgPreparingDesc), nil);
   DownloadPage.ShowBaseNameInsteadOfUrl := True;
+  
+  // To allow all keys you can also just pass nil instead of this list to AddWithISSigVerify 
+  AllowedKeysRuntimeIDs := TStringList.Create;
+  AllowedKeysRuntimeIDs.Add('def02');
+end;
+
+procedure DeinitializeSetup;
+begin
+  if AllowedKeysRuntimeIDs <> nil then
+    AllowedKeysRuntimeIDs.Free;
 end;
 
 function NextButtonClick(CurPageID: Integer): Boolean;
 begin
   if CurPageID = wpReady then begin
     DownloadPage.Clear;
-    // Use AddEx to specify a username and password
-    DownloadPage.Add('https://jrsoftware.org/download.php/is.exe?dontcount=1', 'innosetup-latest.exe', '');
-    DownloadPage.Add('https://jrsoftware.org/download.php/is.exe.issig?dontcount=1', 'innosetup-latest.exe.issig', '');
-    DownloadPage.Add('https://jrsoftware.org/download.php/iscrypt.dll?dontcount=1', 'ISCrypt.dll', '2f6294f9aa09f59a574b5dcd33be54e16b39377984f3d5658cda44950fa0f8fc');
+    // Use AddEx or AddExWithISSigVerify to specify a username and password
+    DownloadPage.AddWithISSigVerify(
+      'https://jrsoftware.org/download.php/is.exe?dontcount=1',
+      'https://jrsoftware.org/download.php/is.exe.issig',
+      'innosetup-latest.exe', AllowedKeysRuntimeIDs);
+    DownloadPage.AddWithISSigVerify(
+      'https://jrsoftware.org/download.php/myprog-extrareadmes.7z',
+      'https://jrsoftware.org/download.php/myprog-extrareadmes.7z.issig',
+      'MyProg-ExtraReadmes.7z', AllowedKeysRuntimeIDs);
+    DownloadPage.Add(
+      'https://jrsoftware.org/download.php/iscrypt.dll?dontcount=1',
+      'ISCrypt.dll',
+      '2f6294f9aa09f59a574b5dcd33be54e16b39377984f3d5658cda44950fa0f8fc');
     DownloadPage.Show;
     try
       try

+ 0 - 101
Examples/CodeDownloadFiles2.iss

@@ -1,101 +0,0 @@
-; -- CodeDownloadFiles2.iss --
-; Same as CodeDownloadFiles1.iss but additionally downloads a 7-Zip archive and
-; shows how to verify and extract it.
-
-[Setup]
-AppName=My Program
-AppVersion=1.5
-WizardStyle=modern
-DefaultDirName={autopf}\My Program
-DefaultGroupName=My Program
-UninstallDisplayIcon={app}\MyProg.exe
-OutputDir=userdocs:Inno Setup Examples Output
-;Use "ArchiveExtraction=enhanced/nopassword" if your archive has large files
-;Use "ArchiveExtraction=enhanced" if your archive has large files *and* a password
-;Use "ArchiveExtraction=full" if your archive is not a .7z file but for example a .zip file
-ArchiveExtraction=basic
-
-[ISSigKeys]
-Name: "mykey"; \
-  KeyID:   "def020edee3c4835fd54d85eff8b66d4d899b22a777353ca4a114b652e5e7a28"; \
-  PublicX: "515dc7d6c16d4a46272ceb3d158c5630a96466ab4d948e72c2029d737c823097"; \
-  PublicY: "f3c21f6b5156c52a35f6f28016ee3e31a3ded60c325b81fb7b1f88c221081a61"
-
-[Files]
-; Place any regular files here
-Source: "MyProg.exe"; DestDir: "{app}";
-Source: "MyProg.chm"; DestDir: "{app}";
-Source: "Readme.txt"; DestDir: "{app}"; Flags: isreadme;
-; These files will be downloaded
-Source: "{tmp}\innosetup-latest.exe"; DestDir: "{app}"; Flags: external ignoreversion issigverify
-Source: "{tmp}\ISCrypt.dll"; DestDir: "{app}"; Flags: external ignoreversion
-Source: "{tmp}\MyProg-ExtraReadmes\*"; Excludes: "*.issig"; DestDir: "{app}"; Flags: external recursesubdirs ignoreversion issigverify
-
-[Icons]
-Name: "{group}\My Program"; Filename: "{app}\MyProg.exe"
-
-[Code]
-var
-  DownloadPage: TDownloadWizardPage;
-  ExtractionPage: TExtractionWizardPage;
-
-procedure InitializeWizard;
-begin
-  DownloadPage := CreateDownloadPage(SetupMessage(msgWizardPreparing), SetupMessage(msgPreparingDesc), nil);
-  DownloadPage.ShowBaseNameInsteadOfUrl := True;
-  ExtractionPage := CreateExtractionPage(SetupMessage(msgWizardPreparing), SetupMessage(msgPreparingDesc), nil);
-end;
-
-function NextButtonClick(CurPageID: Integer): Boolean;
-begin
-  if CurPageID = wpReady then begin
-    DownloadPage.Clear;
-    // Use AddEx to specify a username and password
-    DownloadPage.Add('https://jrsoftware.org/download.php/is.exe?dontcount=1', 'innosetup-latest.exe', '');
-    DownloadPage.Add('https://jrsoftware.org/download.php/is.exe.issig?dontcount=1', 'innosetup-latest.exe.issig', '');
-    DownloadPage.Add('https://jrsoftware.org/download.php/iscrypt.dll?dontcount=1', 'ISCrypt.dll', '2f6294f9aa09f59a574b5dcd33be54e16b39377984f3d5658cda44950fa0f8fc');
-    DownloadPage.Add('https://jrsoftware.org/download.php/myprog-extrareadmes.7z?dontcount=1', 'MyProg-ExtraReadmes.7z', '');
-    DownloadPage.Show;
-    try
-      try
-        // Downloads the files to {tmp}
-        DownloadPage.Download;
-        Result := True;
-      except
-        if DownloadPage.AbortedByUser then
-          Log('Aborted by user.')
-        else
-          SuppressibleMsgBox(AddPeriod(GetExceptionMessage), mbCriticalError, MB_OK, IDOK);
-        Result := False;
-      end;
-    finally
-      DownloadPage.Hide;
-    end;
-
-    if not Result then
-      Exit;
-
-    ExtractionPage.Clear;
-    // Use AddEx to specify a password
-    ExtractionPage.Add(ExpandConstant('{tmp}\MyProg-ExtraReadmes.7z'), ExpandConstant('{tmp}\MyProg-ExtraReadmes'), True);
-    ExtractionPage.Show;
-    try
-      try
-        // Extracts the archive to {tmp}\MyProg-ExtraReadmes
-        // Note that each file in the MyProg-ExtraReadmes.7z example archive comes with an .issig signature file
-        // These signature files are used by the [Files] section to verify the archive's content
-        ExtractionPage.Extract;
-        Result := True;
-      except
-        if ExtractionPage.AbortedByUser then
-          Log('Aborted by user.')
-        else
-          SuppressibleMsgBox(AddPeriod(GetExceptionMessage), mbCriticalError, MB_OK, IDOK);
-        Result := False;
-      end;
-    finally
-      ExtractionPage.Hide;
-    end;
-  end else
-    Result := True;
-end;

+ 14 - 2
Files/Default.isl

@@ -1,4 +1,4 @@
-; *** Inno Setup version 6.4.0+ English messages ***
+; *** Inno Setup version 6.5.0+ English messages ***
 ;
 ; To download user-contributed translations of this file, go to:
 ;   https://jrsoftware.org/files/istrans/
@@ -224,13 +224,18 @@ ErrorFileHash2=Invalid file hash: expected %1, found %2
 ErrorProgress=Invalid progress: %1 of %2
 ErrorFileSize=Invalid file size: expected %1, found %2
 
-; *** TExtractionWizardPage wizard page and Extract7ZipArchive
+; *** TExtractionWizardPage wizard page and ExtractArchive
 ExtractionLabel=Extracting additional files...
 ButtonStopExtraction=&Stop extraction
 StopExtraction=Are you sure you want to stop the extraction?
 ErrorExtractionAborted=Extraction aborted
 ErrorExtractionFailed=Extraction failed: %1
 
+; *** Archive extraction failure details
+ArchiveIncorrectPassword=The password is incorrect
+ArchiveIsCorrupted=The archive is corrupted
+ArchiveUnsupportedFormat=The archive format is unsupported
+
 ; *** "Preparing to Install" wizard page
 WizardPreparing=Preparing to Install
 PreparingDesc=Setup is preparing to install [name] on your computer.
@@ -309,6 +314,12 @@ FileAbortRetryIgnoreSkipNotRecommended=&Skip this file (not recommended)
 FileAbortRetryIgnoreIgnoreNotRecommended=&Ignore the error and continue (not recommended)
 SourceIsCorrupted=The source file is corrupted
 SourceDoesntExist=The source file "%1" does not exist
+SourceVerificationFailed=Verification of the source file failed: %1
+VerificationSignatureDoesntExist=The signature file "%1" does not exist
+VerificationSignatureInvalid=The signature file "%1" is invalid
+VerificationKeyNotFound=The signature file "%1" uses an unknown key
+VerificationFileSizeIncorrect=The size of the file is incorrect
+VerificationFileHashIncorrect=The hash of the file is incorrect
 ExistingFileReadOnly2=The existing file could not be replaced because it is marked read-only.
 ExistingFileReadOnlyRetry=&Remove the read-only attribute and try again
 ExistingFileReadOnlyKeepExisting=&Keep the existing file
@@ -327,6 +338,7 @@ ErrorChangingAttr=An error occurred while trying to change the attributes of the
 ErrorCreatingTemp=An error occurred while trying to create a file in the destination directory:
 ErrorReadingSource=An error occurred while trying to read the source file:
 ErrorCopying=An error occurred while trying to copy a file:
+ErrorExtracting=An error occurred while trying to extract an archive:
 ErrorReplacingExistingFile=An error occurred while trying to replace the existing file:
 ErrorRestartReplace=RestartReplace failed:
 ErrorRenamingTemp=An error occurred while trying to rename a file in the destination directory:

+ 13 - 8
Files/Languages/Dutch.isl

@@ -1,4 +1,4 @@
-; *** Inno Setup version 6.4.0+ Dutch messages ***
+; *** Inno Setup version 6.5.0+ Dutch messages ***
 ;
 ; This file is based on user-contributed translations by various authors
 ;
@@ -30,8 +30,8 @@ LdrCannotExecTemp=Kan een bestand in de tijdelijke map niet uitvoeren. Setup wor
 ; *** Startup error messages
 LastErrorMessage=%1.%n%nFout %2: %3
 SetupFileMissing=Het bestand %1 ontbreekt in de installatiemap. Corrigeer dit probleem of gebruik een andere kopie van het programma.
-SetupFileCorrupt=De installatiebestanden zijn beschadigd. Gebruik een andere kopie van het programma.
-SetupFileCorruptOrWrongVer=De installatiebestanden zijn beschadigd, of zijn niet compatibel met deze versie van Setup. Corrigeer dit probleem of gebruik een andere kopie van het programma.
+SetupFileCorrupt=De installatiebestanden zijn corrupt. Gebruik een andere kopie van het programma.
+SetupFileCorruptOrWrongVer=De installatiebestanden zijn corrupt, of zijn niet compatibel met deze versie van Setup. Corrigeer dit probleem of gebruik een andere kopie van het programma.
 InvalidParameter=Er werd een ongeldige schakeloptie opgegeven op de opdrachtregel:%n%n%1
 SetupAlreadyRunning=Setup is al gestart.
 WindowsVersionNotSupported=Dit programma ondersteunt de versie van Windows die u gebruikt niet.
@@ -67,7 +67,7 @@ AboutSetupMenuItem=&Over Setup...
 AboutSetupTitle=Over Setup
 AboutSetupMessage=%1 versie %2%n%3%n%n%1-homepage:%n%4
 AboutSetupNote=
-TranslatorNote=Dutch translation maintained by Martijn Laan (mlaan@jrsoftware.org)
+TranslatorNote=Dutch translation maintained by Martijn Laan (mlaan@innosetup.nl)
 
 ; *** Buttons
 ButtonBack=< Vo&rige
@@ -204,13 +204,18 @@ ErrorFileHash2=Ongeldige bestandshash: %1 verwacht, %2 gevonden
 ErrorProgress=Ongeldige voortgang: %1 van %2
 ErrorFileSize=Ongeldige bestandsgrootte: %1 verwacht, %2 gevonden
 
-; *** TExtractionWizardPage wizard page and Extract7ZipArchive
+; *** TExtractionWizardPage wizard page and ExtractArchive
 ExtractionLabel=Bezig met het uitpakken van extra bestanden...
 ButtonStopExtraction=&Stop uitpakken
 StopExtraction=Weet u zeker dat u het uitpakken wilt stoppen?
 ErrorExtractionAborted=Uitpakken gestopt
 ErrorExtractionFailed=Uitpakken mislukt: %1
 
+; *** Archive extraction failure details
+ArchiveIncorrectPassword=Het wachtwoord is niet correct
+ArchiveIsCorrupted=Het archief is corrupt
+ArchiveUnsupportedFormat=Het archief formaat wordt niet ondersteund
+
 ; *** "Preparing to Install" wizard page
 WizardPreparing=Bezig met het voorbereiden van de installatie
 PreparingDesc=Setup is bezig met het voorbereiden van de installatie van [name].
@@ -285,7 +290,7 @@ ErrorIniEntry=Fout bij het maken van een INI-instelling in bestand "%1".
 ; *** File copying errors
 FileAbortRetryIgnoreSkipNotRecommended=&Sla dit bestand over (niet aanbevolen)
 FileAbortRetryIgnoreIgnoreNotRecommended=&Negeer de fout en ga door (niet aanbevolen)
-SourceIsCorrupted=Het bronbestand is beschadigd
+SourceIsCorrupted=Het bronbestand is corrupt
 SourceDoesntExist=Het bronbestand "%1" bestaat niet
 ExistingFileReadOnly2=Het bestaande bestand kon niet vervangen worden omdat het een alleen-lezen markering heeft.
 ExistingFileReadOnlyRetry=&Verwijder de alleen-lezen markering en probeer het opnieuw
@@ -326,7 +331,7 @@ ErrorRestartingComputer=Setup kan de computer niet opnieuw opstarten. Doe dit ha
 
 ; *** Uninstaller messages
 UninstallNotFound=Bestand "%1" bestaat niet. Kan het programma niet verwijderen.
-UninstallUnsupportedVer=Het installatie-logbestand "%1" heeft een formaat dat niet herkend wordt door deze versie van het verwijderprogramma. Kan het programma niet verwijderen
+UninstallUnsupportedVer=Het installatie-logbestand "%1" heeft een formaat dat niet ondersteund wordt door deze versie van het verwijderprogramma. Kan het programma niet verwijderen
 UninstallUnknownEntry=Er is een onbekend gegeven (%1) aangetroffen in het installatie-logbestand
 ConfirmUninstall=Weet u zeker dat u %1 en alle bijbehorende componenten wilt verwijderen?
 UninstallOnlyOnWin64=Deze installatie kan alleen worden verwijderd onder 64-bit Windows.
@@ -336,7 +341,7 @@ UninstallOpenError=Bestand "%1" kon niet worden geopend. Kan het verwijderen nie
 UninstalledAll=%1 is met succes van deze computer verwijderd.
 UninstalledMost=Het verwijderen van %1 is voltooid.%n%nEnkele elementen konden niet verwijderd worden. Deze kunnen handmatig verwijderd worden.
 UninstalledAndNeedsRestart=Om het verwijderen van %1 te voltooien, moet uw computer opnieuw worden opgestart.%n%nWilt u nu opnieuw opstarten?
-UninstallDataCorrupted="%1" bestand is beschadigd. Kan verwijderen niet voltooien
+UninstallDataCorrupted="%1" bestand is corrupt. Kan verwijderen niet voltooien
 
 ; *** Uninstallation phase messages
 ConfirmDeleteSharedFileTitle=Gedeeld bestand verwijderen?

+ 15 - 4
ISHelp/isetup.xml

@@ -1689,7 +1689,7 @@ Instructs Setup to proceed to comparing time stamps (last write/modified time) i
 </flag>
 <flag name="createallsubdirs">
 <p>By default the compiler skips empty directories when it recurses subdirectories searching for the <tt>Source</tt> filename/wildcard. This flag causes these directories to be created at install time (just like if you created [Dirs] entries for them).</p>
-<p>Must be combined with <tt>recursesubdirs</tt>.</p>
+<p>This flag must be combined with <tt>recursesubdirs</tt>.</p>
 </flag>
 <flag name="deleteafterinstall">
 <p>Instructs Setup to install the file as usual, but then delete it once the installation is completed (or aborted). This can be useful for extracting temporary data needed by a program executed in the script's [Run] section.</p>
@@ -1704,7 +1704,14 @@ Instructs Setup to proceed to comparing time stamps (last write/modified time) i
 <p>Must be combined with <tt>nocompression</tt>.</p>
 </flag>
 <flag name="external">
-<p>This flag instructs Inno Setup not to statically compile the file specified by the <tt>Source</tt> parameter into the installation files, but instead copy from an existing file on the distribution media or the user's system. See the <tt>Source</tt> parameter description for more information.</p>
+<p>This flag instructs Inno Setup not to statically compile the file specified by the <tt>Source</tt> parameter into the installation files, but instead to copy from an existing file on the distribution media or the user's system. See the <tt>Source</tt> parameter description for more information.</p>
+</flag>
+<flag name="extractarchive">
+<p>This flag instructs Inno Setup not to copy an existing archive file, but instead to extract it.</p>
+<p>The supported archive formats, beyond .7z, and the support for password-protected archives, depend on the <link topic="setup_archiveextraction">ArchiveExtraction</link> [Setup] section directive, that must not be set to <tt>basic</tt>.</p>
+<p>This flag must be combined with the <tt>external</tt> and <tt>ignoreversion</tt> flags, meaning it should only be used on files private to your application, <i>never</i> on shared system files.</p>
+<p>This flag is usually combined with the <tt>recursesubdirs</tt> and <tt>createallsubdirs</tt> flags.</p>
+<p>Using a solid archive is not recommended; extraction performance may degrade depending on the solid block size.</p>
 </flag>
 <flag name="fontisnttruetype">
 <p>Specify this flag if the entry is installing a <i>non-TrueType</i> font with the <tt>FontInstall</tt> parameter.</p>
@@ -1717,6 +1724,7 @@ Instructs Setup to proceed to comparing time stamps (last write/modified time) i
 <flag name="ignoreversion">
 <p>Don't compare version info at all; replace existing files regardless of their version number.</p>
 <p>This flag should only be used on files private to your application, <i>never</i> on shared system files.</p>
+<p>This flag cannot be combined with <tt>replacesameversion</tt>.</p>
 </flag>
 <flag name="issigverify">
 <p>Instructs the compiler or Setup to verify the source file's signature using a key from the <link topic="issigkeyssection">[ISSigKeys] section</link>, allowing all keys by default. Use the <tt>ISSigAllowedKeys</tt> parameter to limit the allowed keys.</p>
@@ -1727,7 +1735,8 @@ Instructs Setup to proceed to comparing time stamps (last write/modified time) i
 <li><p>When used with the <tt>external</tt> flag, Setup will verify the source file during the installation process while it is being copied to the destination directory. Files are always created with temporary names (<tt>*.tmp</tt>) initially. If the verification fails, the temporary file will be deleted and a &quot;The source file is corrupted&quot; error message will be displayed to the user (with Skip, Try Again, and Cancel options) and a more detailed error is logged. If the verification succeeds, the temporary file will be renamed to the correct destination name.</p>
 <p>When a file entry with the <tt>external</tt> flag is skipped (i.e., not installed - for example because the <tt>ignoreversion</tt> flag wasn't used), the source file isn't copied anywhere, so no verification takes place.</p></li>
 </ul>
-<p>Since verification occurs while source files are being compressed/copied, and not in a separate pass, each file's contents are only read once. Thus, enabling verification has little performance impact; the only extra I/O comes from reading the tiny <tt>.issig</tt> files. This approach also ensures there is no Time-Of-Check to Time-Of-Use (TOCTOU) problem; each source file is kept open the entire time it is being compressed/copied and verified without allowing other processes write access.</p>
+<p>Since verification occurs while source files are being compressed/copied, and not in a separate pass, each file's contents are only read once. Thus, enabling verification has little performance impact; the only extra I/O comes from reading the tiny <tt>.issig</tt> files. Only archives and downloaded files are read twice.</p>
+<p>The verification process is protected against the Time-Of-Check to Time-Of-Use (TOCTOU) problem.</p>
 <p>This flag cannot be combined with the <tt>sign</tt> or <tt>signonce</tt> flags. Use <tt>signcheck</tt> instead.</p>
 </flag>
 <flag name="isreadme">
@@ -1771,6 +1780,8 @@ Instructs Setup to proceed to comparing time stamps (last write/modified time) i
 <flag name="replacesameversion">
 <p>When this flag is used and the file already exists on the user's system and it has the same version number as the file being installed, Setup will compare the files and replace the existing file if their contents differ.</p>
 <p>The default behavior (i.e. when this flag isn't used) is to not replace an existing file with the same version number.</p>
+<p>This flag has no effect if combined with the <tt>extractarchive</tt> flag, or if used for a file that lacks a version number.</p>
+<p>This flag cannot be combined with <tt>ignoreversion</tt>.</p>
 </flag>
 <flag name="restartreplace">
 <p>When an existing file needs to be replaced, and it is in use (locked) by another running process, Setup will by default display an error message. This flag tells Setup to instead register the file to be replaced the next time the system is restarted (by calling MoveFileEx or by creating an entry in WININIT.INI). When this happens, the user will be prompted to restart their computer at the end of the installation process.</p>
@@ -4729,7 +4740,7 @@ Name: portablemode; Description: "Portable Mode"</pre></example>
 <tt>full</tt></setupvalid>
 <setupdefault><tt>basic</tt></setupdefault>
 <body>
-<p>This specifies the method of archive extraction used by support functions <link topic="isxfunc_ExtractArchive">ExtractArchive</link> and <link topic="isxfunc_CreateExtractionPage">CreateExtractionPage</link>.</p>
+<p>This specifies the method of archive extraction used by [Files] section flag <link topic="filessection" anchor="extractarchive">extractarchive</link> and support functions <link topic="isxfunc_ExtractArchive">ExtractArchive</link> and <link topic="isxfunc_CreateExtractionPage">CreateExtractionPage</link>.</p>
 <p><tt>basic</tt> uses an embedded version of the "7z ANSI-C Decoder" from the LZMA SDK by Igor Pavlov, as-is, except that Unicode support and error messages were improved and that it outputs memory requirements. It only supports .7z archives that are not password-protected.</p>
 <p><tt>enhanced/nopassword</tt> internally uses 7zxr.dll from the 7-Zip source code by Igor Pavlov, as-is, except that it was recompiled, code-signed, and renamed to is7zxr.dll. Compared to <tt>basic</tt>, it has lower memory requirements for archives that contain large files but increases the size of the Setup file(s). It still only supports .7z archives that are not password-protected.</p>
 <p><tt>enhanced</tt> uses 7zxa.dll instead of 7zxr.dll, recompiled, code-signed, and renamed to is7zxa.dll. It still only supports .7z archives, but they may be password-protected.</p>

+ 6 - 4
ISHelp/isxclasses.pas

@@ -777,8 +777,10 @@ end;
 TDownloadWizardPage = class(TOutputProgressWizardPage)
   property AbortButton: TNewButton; read;
   property AbortedByUser: Boolean; read;
-  procedure Add(const Url, BaseName, RequiredSHA256OfFile: String);
-  procedure AddEx(const Url, BaseName, RequiredSHA256OfFile, UserName, Password: String);
+  function Add(const Url, BaseName, RequiredSHA256OfFile: String): Integer;
+  function AddWithISSigVerify(const Url, IssigUrl, BaseName: String; const AllowedKeysRuntimeIDs: TStringList): Integer;
+  function AddEx(const Url, BaseName, RequiredSHA256OfFile, UserName, Password: String): Integer;
+  function AddExWithISSigVerify(const Url, IssigUrl, BaseName, UserName, Password: String; const AllowedKeysRuntimeIDs: TStringList: Integer;
   procedure Clear;
   function Download: Int64;
   property ShowBaseNameInsteadOfUrl: Boolean; read write;
@@ -787,8 +789,8 @@ end;
 TExtractionWizardPage = class(TOutputProgressWizardPage)
   property AbortButton: TNewButton; read;
   property AbortedByUser: Boolean; read;
-  procedure Add(const ArchiveFileName, DestDir: String; const FullPaths: Boolean);
-  procedure AddEx(const ArchiveFileName, DestDir, Password: String; const FullPaths: Boolean);
+  function Add(const ArchiveFileName, DestDir: String; const FullPaths: Boolean): Integer;
+  function AddEx(const ArchiveFileName, DestDir, Password: String; const FullPaths: Boolean): Integer;
   procedure Clear;
   procedure Extract;
   property ShowArchiveInsteadOfFile: Boolean; read write;

+ 6 - 4
ISHelp/isxclasses_wordlists_generated.pas

@@ -78,11 +78,17 @@ var
     'function Add(APrompt, AFilter, ADefaultExtension: String): Integer;',
     'function Add(APrompt: String): Integer;',
     'function Add(APrompt: String; APassword: Boolean): Integer;',
+    'function Add(ArchiveFileName, DestDir: String; FullPaths: Boolean): Integer;',
     'function Add(S: String): Integer;',
+    'function Add(Url, BaseName, RequiredSHA256OfFile: String): Integer;',
     'function AddCheckBox(ACaption, ASubItem: String; ALevel: Byte; AChecked, AEnabled, AHasInternalChildren, ACheckWhenParentChecked: Boolean; AObject: TObject): Integer;',
     'function AddEx(ACaption: String; ALevel: Byte; AExclusive: Boolean): Integer;',
+    'function AddEx(ArchiveFileName, DestDir, Password: String; FullPaths: Boolean): Integer;',
+    'function AddEx(Url, BaseName, RequiredSHA256OfFile, UserName, Password: String): Integer;',
+    'function AddExWithISSigVerify(Url, IssigUrl, BaseName, UserName, Password: String; AllowedKeysRuntimeIDs: TStringList: Integer;',
     'function AddGroup(ACaption, ASubItem: String; ALevel: Byte; AObject: TObject): Integer;',
     'function AddRadioButton(ACaption, ASubItem: String; ALevel: Byte; AChecked, AEnabled: Boolean; AObject: TObject): Integer;',
+    'function AddWithISSigVerify(Url, IssigUrl, BaseName: String; AllowedKeysRuntimeIDs: TStringList): Integer;',
     'function AdjustHeight: Integer;',
     'function AdjustLabelHeight(ALabel: TNewStaticText): Integer;',
     'function AdjustLinkLabelHeight(ALinkLabel: TNewLinkLabel): Integer;',
@@ -104,10 +110,6 @@ var
     'function TextHeight(Text: String): Integer;',
     'function TextWidth(Text: String): Integer;',
     'function Write(Buffer: AnyString; ByteCount: Longint): Longint;',
-    'procedure Add(ArchiveFileName, DestDir: String; FullPaths: Boolean);',
-    'procedure Add(Url, BaseName, RequiredSHA256OfFile: String);',
-    'procedure AddEx(ArchiveFileName, DestDir, Password: String; FullPaths: Boolean);',
-    'procedure AddEx(Url, BaseName, RequiredSHA256OfFile, UserName, Password: String);',
     'procedure AddStrings(Strings: TStrings);',
     'procedure Animate;',
     'procedure Append(S: String);',

+ 4 - 4
ISHelp/isxfunc.xml

@@ -1884,7 +1884,7 @@ end;</pre>
 <p>An exception will be raised if there was an error.</p>
 <p>The supported archive formats, beyond .7z, and the support for password-protected archives, depend on the <link topic="setup_archiveextraction">ArchiveExtraction</link> [Setup] section directive.</p>
 <p>Set OnExtractionProgress to a function to be informed of progress, or <tt>nil</tt> otherwise.</p>
-<p>See <i>CodeDownloadFiles2.iss</i> for an example which uses <link topic="isxfunc_CreateExtractionPage">CreateExtractionPage</link> instead.</p></description>
+<p>See <i>CodeDownloadFiles.iss</i> for an example of archive extraction using just a [Files] entry instead.</p></description>
         <remarks><p>TOnExtractionProgress is defined as:</p>
 <p><tt>TOnExtractionProgress = function(const ArchiveName, FileName: String; const Progress, ProgressMax: Int64): Boolean;</tt></p>
 <p>Return True to allow the extraction to continue, False otherwise.</p></remarks>
@@ -1915,8 +1915,8 @@ end;</pre>
     <subcategory>
       <function>
         <name>ISSigVerify</name>
-        <prototype>function ISSigVerify(const AllowedKeysRuntimeIDs: TArrayOfString; const Filename: String; const KeepOpen: Boolean): TFileStream;</prototype>
-        <description><p>Verifies the signature of the specified file using the specified allowed keys, looked up using [ISSigKeys] section parameter <tt>RuntimeID</tt>. If no keys are specified then all keys are allowed. An exception will be raised upon failure.</p>
+        <prototype>function ISSigVerify(const AllowedKeysRuntimeIDs: TStringList; const Filename: String; const KeepOpen: Boolean): TFileStream;</prototype>
+        <description><p>Verifies the signature of the specified file using the specified allowed keys, looked up using [ISSigKeys] section parameter <tt>RuntimeID</tt>. To allow all keys set AllowedKeysRuntimeIDs to <tt>nil</tt>. An exception will be raised upon failure.</p>
 <p>Returns a handle to the still open file if True is specified in the KeepOpen parameter, <tt>nil</tt> otherwise. It is recommended that you always specify True if you plan to use the file for anything after verification. Otherwise, you risk creating a Time-Of-Check to Time-Of-Use (TOCTOU) problem.</p></description>
         <example><pre>var
   F: TFileStream;
@@ -2702,7 +2702,7 @@ Page := CreateOutputMsgMemoPage(wpWelcome,
 <p>To start the extraction, call the <tt>Extract</tt> method. An exception will be raised if there was an error. Otherwise, <tt>Extract</tt> returns the number of archives extracted.</p>
 <p>Set the <tt>ShowArchiveInsteadFile</tt> property to <tt>True</tt> to show the name of the archive which is being extracted to the user instead of the names of the files inside the archive.</p>
 <p>See <link topic="isxfunc_ExtractArchive">ExtractArchive</link> for the definition of <tt>TOnExtractionProgress</tt>.</p></remarks>
-        <example><p>See <i>CodeDownloadFiles2.iss</i> for an example.</p></example>
+        <example><p>See <i>CodeDownloadFiles.iss</i> for an example of <tt>CreateDownloadPage</tt> which works very similar to <tt>CreateExtractionPage</tt>, and an example of archive extraction using just a [Files] entry instead.</p></example>
         <seealso><p><link topic="scriptclasses" anchor="TExtractionWizardPage">TExtractionWizardPage</link><br />
 <link topic="isxfunc_ExtractArchive">ExtractArchive</link><br />
 <link topic="isxfunc_CreateOutputProgressPage">CreateOutputProgressPage</link></p></seealso>

+ 2 - 2
Projects/ISSigTool.dpr

@@ -220,10 +220,10 @@ begin
     begin
       PrintUnlessQuiet('MISSINGSIGFILE (Signature file does not exist)');
     end,
-    procedure(const SigFilename: String; const VerifyResult: TISSigVerifySignatureResult)
+    procedure(const Filename, SigFilename: String; const VerifyResult: TISSigVerifySignatureResult)
     begin
       case VerifyResult of
-        vsrMalformed, vsrBadSignature:
+        vsrMalformed, vsrBad:
           PrintUnlessQuiet('BADSIGFILE (Signature file is not valid)');
         vsrKeyNotFound:
           PrintUnlessQuiet('UNKNOWNKEY (Incorrect key ID)');

+ 10 - 10
Projects/Src/Compiler.Messages.pas

@@ -2,7 +2,7 @@ unit Compiler.Messages;
 
 {
   Inno Setup
-  Copyright (C) 1997-2024 Jordan Russell
+  Copyright (C) 1997-2025 Jordan Russell
   Portions by Martijn Laan
   For conditions of distribution and use, see LICENSE.TXT.
 
@@ -85,14 +85,13 @@ const
   SCompilerUnknownFilenamePrefix = 'Unknown filename prefix "%s"';
   SCompilerSourceFileDoesntExist = 'Source file "%s" does not exist';
   SCompilerSourceFileNotSigned = 'Source file "%s" is not signed';
-  SCompilerSourceFileISSigMissingFile = 'Signature file does not exist for source file "%s"';
-  SCompilerSourceFileISSigInvalidSignature1 = 'Signature file "%s" is not valid: %s';
-  SCompilerSourceFileISSigInvalidSignature2 = 'Signature for source file "%s" is not valid: %s';
-  SCompilerSourceFileISSigMalformedOrBadSignature = 'malformed or bad signature';
-  SCompilerSourceFileISSigKeyNotFound = 'no matching key found';
-  SCompilerSourceFileISSigUnknownVerifyResult = 'unknown verify result';
-  SCompilerSourceFileISSigFileSizeIncorrect = 'file size incorrect';
-  SCompilerSourceFileISSigFileHashIncorrect = 'file hash incorrect';
+  SCompilerSourceFileVerificationFailed = 'Verification of source file "%s" failed: %s';
+  SCompilerVerificationSignatureDoesntExist = 'The signature file "%1" does not exist';
+  SCompilerVerificationSignatureMalformed = 'The signature file "%1" is malformed';
+  SCompilerVerificationSignatureBad = 'The signature file "%1" is bad';
+  SCompilerVerificationKeyNotFound = 'The signature file "%1" uses an unknown key';
+  SCompilerVerificationFileSizeIncorrect = 'The size of the file is incorrect';
+  SCompilerVerificationFileHashIncorrect = 'The hash of the file is incorrect';
   SCompilerCopyError3a = 'Could not copy "%s" to "%s".' + SNewLine2 + 'Error %s';
   SCompilerCopyError3b = 'Could not copy "%s" to "%s".' + SNewLine2 + 'Error %d: %s';
   SCompilerReadError = 'Could not read "%s".' + SNewLine2 + 'Error: %s';
@@ -110,6 +109,7 @@ const
   SCompilerEntrySuperseded2 = 'The [%s] section directive "%s" has been superseded by "%s" in this version of Inno Setup.';
   SCompilerEntryMissing2 = 'Required [%s] section directive "%s" not specified';
   SCompilerEntryInvalid2 = 'Value of [%s] section directive "%s" is invalid';
+  SCompilerEntryValueUnsupported = 'Value of [%s] section directive "%s" must not be "%s" if flag "%s" is used.';
   SCompilerEntryAlreadySpecified = '[%s] section directive "%s" already specified';
   SCompilerAppVersionOrAppVerNameRequired = 'The [Setup] section must include an AppVersion or AppVerName directive';
   SCompilerMinVersionWinMustBeZero = 'Minimum non NT version specified by MinVersion must be 0. (Windows 95/98/Me are no longer supported.)';
@@ -289,7 +289,7 @@ const
     '"dontcopy" flag is used';
   SCompilerFilesWildcardNotMatched = 'No files found matching "%s"';
   SCompilerFilesDestNameCantBeSpecified = 'Parameter "DestName" cannot be specified if ' +
-    'the "Source" parameter contains wildcards';
+    'the "Source" parameter contains wildcards or flag "extractarchive" is used';
   SCompilerFilesStrongAssemblyNameMustBeSpecified = 'Parameter "StrongAssemblyName" must be specified if ' +
     'the flag "gacinstall" is used';
   SCompilerFilesCantHaveNonExternalExternalSize = 'Parameter "ExternalSize" may only be used when ' +

+ 7 - 5
Projects/Src/Compiler.ScriptClasses.pas

@@ -2,7 +2,7 @@ unit Compiler.ScriptClasses;
 
 {
   Inno Setup
-  Copyright (C) 1997-2020 Jordan Russell
+  Copyright (C) 1997-2025 Jordan Russell
   Portions by Martijn Laan
   For conditions of distribution and use, see LICENSE.TXT.
 
@@ -551,8 +551,10 @@ begin
     RegisterProperty('AbortButton', 'TNewButton', iptr);
     RegisterProperty('AbortedByUser', 'Boolean', iptr);
     RegisterProperty('ShowBaseNameInsteadOfUrl', 'Boolean', iptrw);
-    RegisterMethod('procedure Add(const Url, BaseName, RequiredSHA256OfFile: String)');
-    RegisterMethod('procedure AddEx(const Url, BaseName, RequiredSHA256OfFile, UserName, Password: String)');
+    RegisterMethod('function Add(const Url, BaseName, RequiredSHA256OfFile: String): Integer');
+    RegisterMethod('function AddWithISSigVerify(const Url, IssigUrl, BaseName: String; const AllowedKeysRuntimeIDs: TStringList): Integer;');
+    RegisterMethod('function AddEx(const Url, BaseName, RequiredSHA256OfFile, UserName, Password: String): Integer');
+    RegisterMethod('function AddExWithISSigVerify(const Url, IssigUrl, BaseName, UserName, Password: String; const AllowedKeysRuntimeIDs: TStringList): Integer;');
     RegisterMethod('procedure Clear');
     RegisterMethod('function Download: Int64');
     RegisterMethod('procedure Show'); { Without this TOutputProgressWizardPage's Show will be called }
@@ -566,8 +568,8 @@ begin
     RegisterProperty('AbortButton', 'TNewButton', iptr);
     RegisterProperty('AbortedByUser', 'Boolean', iptr);
     RegisterProperty('ShowArchiveInsteadOfFile', 'Boolean', iptrw);
-    RegisterMethod('procedure Add(const ArchiveFileName, DestDir: String; const FullPaths: Boolean)');
-    RegisterMethod('procedure AddEx(const ArchiveFileName, DestDir, Password: String; const FullPaths: Boolean)');
+    RegisterMethod('function Add(const ArchiveFileName, DestDir: String; const FullPaths: Boolean): Integer');
+    RegisterMethod('function AddEx(const ArchiveFileName, DestDir, Password: String; const FullPaths: Boolean): Integer');
     RegisterMethod('procedure Clear');
     RegisterMethod('procedure Extract');
     RegisterMethod('procedure Show'); { Without this TOutputProgressWizardPage's Show will be called }

+ 49 - 19
Projects/Src/Compiler.SetupCompiler.pas

@@ -19,7 +19,7 @@ interface
 
 uses
   Windows, SysUtils, Classes, Generics.Collections,
-  SimpleExpression, SHA256, ChaCha20,
+  SimpleExpression, SHA256, ChaCha20, Shared.SetupTypes,
   Shared.Struct, Shared.CompilerInt.Struct, Shared.PreprocInt, Shared.SetupMessageIDs,
   Shared.SetupSectionDirectives, Shared.VerInfoFunc, Shared.Int64Em, Shared.DebugStruct,
   Compiler.ScriptCompiler, Compiler.StringLists, Compression.LZMACompressor;
@@ -256,6 +256,8 @@ type
     procedure WriteCompiledCodeDebugInfo(const CompiledCodeDebugInfo: AnsiString);
     function CreateMemoryStreamsFromFiles(const ADirectiveName, AFiles: String): TObjectList<TCustomMemoryStream>;
     function CreateMemoryStreamsFromResources(const AResourceNamesPrefixes, AResourceNamesPostfixes: array of String): TObjectList<TCustomMemoryStream>;
+    procedure ISSigVerifyError(const AError: TISSigVerifySignatureError;
+      const AFilename: String; const ASigFilename: String = '');
   public
     AppData: Longint;
     CallbackProc: TCompilerCallbackProc;
@@ -298,7 +300,7 @@ uses
 {$IFDEF STATICPREPROC}
   ISPP.Preprocess,
 {$ENDIF}
-  Shared.SetupTypes, Compiler.CompressionHandler, Compiler.HelperFunc, Compiler.BuiltinPreproc;
+  Compiler.CompressionHandler, Compiler.HelperFunc, Compiler.BuiltinPreproc;
 
 type
   TLineInfo = class
@@ -4665,9 +4667,9 @@ procedure TSetupCompiler.EnumFilesProc(const Line: PChar; const Ext: Integer);
 
 type
   TParam = (paFlags, paSource, paDestDir, paDestName, paCopyMode, paAttribs,
-    paPermissions, paFontInstall, paExcludes, paExternalSize, paStrongAssemblyName,
-    paISSigAllowedKeys, paComponents, paTasks, paLanguages, paCheck, paBeforeInstall,
-    paAfterInstall, paMinVersion, paOnlyBelowVersion);
+    paPermissions, paFontInstall, paExcludes, paExternalSize, paExtractArchivePassword,
+    paStrongAssemblyName, paISSigAllowedKeys, paComponents, paTasks, paLanguages,
+    paCheck, paBeforeInstall, paAfterInstall, paMinVersion, paOnlyBelowVersion);
 const
   ParamFilesSource = 'Source';
   ParamFilesDestDir = 'DestDir';
@@ -4678,6 +4680,7 @@ const
   ParamFilesFontInstall = 'FontInstall';
   ParamFilesExcludes = 'Excludes';
   ParamFilesExternalSize = 'ExternalSize';
+  ParamFilesExtractArchivePassword = 'ExtractArchivePassword';
   ParamFilesStrongAssemblyName = 'StrongAssemblyName';
   ParamFilesISSigAllowedKeys = 'ISSigAllowedKeys';
   ParamInfo: array[TParam] of TParamInfo = (
@@ -4691,6 +4694,7 @@ const
     (Name: ParamFilesFontInstall; Flags: [piNoEmpty]),
     (Name: ParamFilesExcludes; Flags: []),
     (Name: ParamFilesExternalSize; Flags: []),
+    (Name: ParamFilesExtractArchivePassword; Flags: []),
     (Name: ParamFilesStrongAssemblyName; Flags: [piNoEmpty]),
     (Name: ParamFilesISSigAllowedKeys; Flags: [piNoEmpty]),
     (Name: ParamCommonComponents; Flags: []),
@@ -4701,7 +4705,7 @@ const
     (Name: ParamCommonAfterInstall; Flags: []),
     (Name: ParamCommonMinVersion; Flags: []),
     (Name: ParamCommonOnlyBelowVersion; Flags: []));
-  Flags: array[0..41] of PChar = (
+  Flags: array[0..42] of PChar = (
     'confirmoverwrite', 'uninsneveruninstall', 'isreadme', 'regserver',
     'sharedfile', 'restartreplace', 'deleteafterinstall',
     'comparetimestamp', 'fontisnttruetype', 'regtypelib', 'external',
@@ -4713,7 +4717,7 @@ const
     'uninsnosharedfileprompt', 'createallsubdirs', '32bit', '64bit',
     'solidbreak', 'setntfscompression', 'unsetntfscompression',
     'sortfilesbyname', 'gacinstall', 'sign', 'signonce', 'signcheck',
-    'issigverify');
+    'issigverify', 'extractarchive');
   SignFlags: array[TFileLocationSign] of String = (
     '', 'sign', 'signonce', 'signcheck');
   AttribsFlags: array[0..3] of PChar = (
@@ -5254,6 +5258,7 @@ begin
                    39: ApplyNewSign(Sign, fsOnce, SCompilerParamErrorBadCombo2);
                    40: ApplyNewSign(Sign, fsCheck, SCompilerParamErrorBadCombo2);
                    41: Include(Options, foISSigVerify);
+                   42: Include(Options, foExtractArchive);
                  end;
 
                { Source }
@@ -5339,6 +5344,9 @@ begin
                  Include(Options, foExternalSizePreset);
                end;
 
+               { ExtractArchivePassword }
+               ExtractArchivePassword := Values[paExtractArchivePassword].Data;
+
                { ISSigAllowedKeys }
                var S := Values[paISSigAllowedKeys].Data;
                while True do begin
@@ -5423,6 +5431,18 @@ begin
           Excludes := AExcludes.DelimitedText;
         end;
 
+        if foExtractArchive in Options then begin
+          if not ExternalFile then
+            AbortCompileFmt(SCompilerParamFlagMissing, ['external', 'extractarchive'])
+          else if not(foIgnoreVersion in Options) then
+            AbortCompileFmt(SCompilerParamFlagMissing, ['ignoreversion', 'extractarchive'])
+          else if SetupHeader.SevenZipLibraryName = '' then
+            AbortCompileFmt(SCompilerEntryValueUnsupported, ['Setup', 'ArchiveExtraction', 'basic', 'extractarchive']);
+        end;
+
+        if (foIgnoreVersion in Options) and (foReplaceSameVersionIfContentsDiffer in Options) then
+          AbortCompileFmt(SCompilerParamErrorBadCombo2, ['Flags', 'ignoreversion', 'replacesameversion']);
+
         if (ISSigKeyEntries.Count = 0) and (foISSigVerify in Options) then
           AbortCompile(SCompilerFilesISSigVerifyMissingISSigKeys);
         if (ISSigAllowedKeys <> '') and not (foISSigVerify in Options) then
@@ -5454,7 +5474,7 @@ begin
             Include(Options, foRecurseSubDirsExternal);
           CheckConst(SourceWildcard, MinVersion, []);
         end;
-        if (ADestName <> '') and SourceIsWildcard then
+        if (ADestName <> '') and (SourceIsWildcard or (foExtractArchive in Options)) then
           AbortCompile(SCompilerFilesDestNameCantBeSpecified);
         CheckConst(ADestDir, MinVersion, []);
         ADestDir := AddBackslash(ADestDir);
@@ -6584,6 +6604,19 @@ begin
   end;
 end;
 
+procedure TSetupCompiler.ISSigVerifyError(const AError: TISSigVerifySignatureError;
+  const AFilename, ASigFilename: String);
+const
+  Messages: array[TISSigVerifySignatureError] of String =
+    (SCompilerVerificationSignatureDoesntExist, SCompilerVerificationSignatureMalformed,
+     SCompilerVerificationKeyNotFound, SCompilerVerificationSignatureBad,
+     SCompilerVerificationFileSizeIncorrect, SCompilerVerificationFileHashIncorrect);
+begin
+  { Also see Setup.Install for a similar function }
+  AbortCompileFmt(SCompilerSourceFileVerificationFailed,
+    [AFilename, Format(Messages[AError], [PathExtractName(ASigFilename)])]); { Not all messages actually have a %s parameter but that's OK }
+end;
+
 procedure TSetupCompiler.Compile;
 
   procedure InitDebugInfo;
@@ -7088,25 +7121,23 @@ var
               nil,
               procedure(const Filename, SigFilename: String)
               begin
-                AbortCompileFmt(SCompilerSourceFileISSigMissingFile, [Filename]);
+                ISSigVerifyError(vseSignatureMissing, Filename, SigFilename);
               end,
-              procedure(const SigFilename: String; const VerifyResult: TISSigVerifySignatureResult)
+              procedure(const Filename, SigFilename: String; const VerifyResult: TISSigVerifySignatureResult)
               begin
                 var VerifyResultAsString: String;
                 case VerifyResult of
-                  vsrMalformed, vsrBadSignature: VerifyResultAsString := SCompilerSourceFileISSigMalformedOrBadSignature;
-                  vsrKeyNotFound: VerifyResultAsString := SCompilerSourceFileISSigKeyNotFound;
+                  vsrMalformed: ISSigVerifyError(vseSignatureMalformed, Filename, SigFilename);
+                  vsrBad: ISSigVerifyError(vseSignatureBad, Filename, SigFilename);
+                  vsrKeyNotFound: ISSigVerifyError(vseKeyNotFound, Filename, SigFilename);
                 else
-                  VerifyResultAsString := SCompilerSourceFileISSigUnknownVerifyResult;
+                  AbortCompileFmt(SCompilerCompressInternalError, ['Unknown ISSigVerifySignature result'])
                 end;
-                AbortCompileFmt(SCompilerSourceFileISSigInvalidSignature1,
-                  [SigFilename, VerifyResultAsString]);
               end
             ) then
               AbortCompileFmt(SCompilerCompressInternalError, ['Unexpected ISSigVerifySignature result']);
             if Int64(SourceFile.Size) <> ExpectedFileSize then
-              AbortCompileFmt(SCompilerSourceFileISSigInvalidSignature2,
-                [FileLocationEntryFilenames[I], SCompilerSourceFileISSigFileSizeIncorrect]);
+              ISSigVerifyError(vseFileSizeIncorrect, FileLocationEntryFilenames[I]);
             { ExpectedFileHash checked below after compression }
           end;
 
@@ -7158,8 +7189,7 @@ var
 
           if floISSigVerify in FLExtraInfo.Flags then begin
             if not SHA256DigestsEqual(FL.SHA256Sum, ExpectedFileHash) then
-              AbortCompileFmt(SCompilerSourceFileISSigInvalidSignature2,
-                [FileLocationEntryFilenames[I], SCompilerSourceFileISSigFileHashIncorrect]);
+              ISSigVerifyError(vseFileHashIncorrect, FileLocationEntryFilenames[I]);
             AddStatus(SCompilerStatusFilesISSigVerified);
           end;
         finally

+ 3 - 1
Projects/Src/Compression.SevenZipDLLDecoder.Interfaces.pas

@@ -88,6 +88,7 @@ const
   kpidAttrib = 9;
   kpidCTime = 10;
   kpidMTime = 12;
+  kpidSolid = 13;
 
   { From IArchive.h}
   kExtract = 0;
@@ -140,10 +141,11 @@ type
     function Open(stream: IInStream; const maxCheckStartPosition: PInt64;
       openCallback: IUnknown): HRESULT; stdcall;
     procedure Dummy1;
-    procedure Dummy2;
+    function GetNumberOfItems(out numItems: UInt32): HRESULT; stdcall;
     function GetProperty(index: UInt32; propID: PROPID; out value: OleVariant): HRESULT; stdcall;
     function Extract(indices: Pointer; numItems: UInt32; testMode: Integer;
       extractCallback: IArchiveExtractCallback): HRESULT; stdcall;
+    function GetArchiveProperty(propID: PROPID; out value: OleVariant): HRESULT; stdcall;
   end;
 
   { From IPassword.h }

File diff suppressed because it is too large
+ 753 - 377
Projects/Src/Compression.SevenZipDLLDecoder.pas


+ 52 - 13
Projects/Src/Compression.SevenZipDecoder.pas

@@ -12,9 +12,16 @@ unit Compression.SevenZipDecoder;
 
 interface
 
+uses
+  SysUtils;
+
 type
   TOnExtractionProgress = function(const ArchiveName, FileName: string; const Progress, ProgressMax: Int64): Boolean of object;
 
+  ESevenZipError = class(Exception);
+
+procedure SevenZipError(const ExceptMessage: String; const LogMessage: String = '');
+
 procedure Extract7ZipArchiveRedir(const DisableFsRedir: Boolean;
   const ArchiveFileName, DestDir, Password: String; const FullPaths: Boolean;
   const OnExtractionProgress: TOnExtractionProgress);
@@ -22,10 +29,10 @@ procedure Extract7ZipArchiveRedir(const DisableFsRedir: Boolean;
 implementation
 
 uses
-  Windows, SysUtils, Forms,
+  Windows, Forms,
   PathFunc,
-  Shared.SetupMessageIDs, SetupLdrAndSetup.Messages, SetupLdrAndSetup.RedirFunc,
-  Setup.LoggingFunc, Setup.MainFunc, Setup.InstFunc;
+  Shared.SetupMessageIDs, Shared.CommonFunc, SetupLdrAndSetup.Messages,
+  SetupLdrAndSetup.RedirFunc, Setup.LoggingFunc, Setup.MainFunc, Setup.InstFunc;
 
 type
   TSevenZipDecodeState = record
@@ -294,9 +301,43 @@ begin
     State.Aborted := True;
 end;
 
+procedure SevenZipError(const ExceptMessage, LogMessage: String);
+{ LogMessage may be non-localized or empty but ExceptMessage may be neither.
+  ExceptMessage should not already contain msgErrorExtractionFailed.
+  Should not be called from a secondary thread if LogMessage is not empty. }
+begin
+  if LogMessage <> '' then
+    LogFmt('ERROR: %s', [LogMessage]); { Just like 7zMain.c }
+  raise ESevenZipError.Create(ExceptMessage);
+end;
+
 procedure Extract7ZipArchiveRedir(const DisableFsRedir: Boolean;
   const ArchiveFileName, DestDir, Password: String; const FullPaths: Boolean;
   const OnExtractionProgress: TOnExtractionProgress);
+
+  procedure BadResultError(const Res: Integer);
+  const
+    SZ_ERROR_DATA = 1;
+    SZ_ERROR_MEM = 2;
+    SZ_ERROR_CRC = 3;
+    SZ_ERROR_UNSUPPORTED = 4;
+    SZ_ERROR_ARCHIVE = 16;
+    SZ_ERROR_NO_ARCHIVE = 17;
+  begin
+    { Logging already done by 7zMain.c }
+
+    case Res of
+      SZ_ERROR_UNSUPPORTED, SZ_ERROR_NO_ARCHIVE:
+        SevenZipError(SetupMessages[msgArchiveUnsupportedFormat]);
+      SZ_ERROR_DATA, SZ_ERROR_CRC, SZ_ERROR_ARCHIVE:
+        SevenZipError(SetupMessages[msgArchiveIsCorrupted]);
+      SZ_ERROR_MEM:
+        SevenZipError(Win32ErrorString(E_OUTOFMEMORY));
+    else
+      SevenZipError(Res.ToString);
+    end;
+  end;
+
 begin
   LogArchiveExtractionModeOnce;
 
@@ -304,16 +345,14 @@ begin
     InternalError('Extract7ZipArchive: Invalid ArchiveFileName value');
   if DestDir = '' then
     InternalError('Extract7ZipArchive: Invalid DestDir value');
+  if Password <> '' then
+    InternalError('Extract7ZipArchive: Invalid Password value');
 
-  LogFmt('Extracting 7-Zip archive %s to %s. Full paths? %s', [ArchiveFileName, DestDir, SYesNo[FullPaths]]);
+  LogFmt('Extracting 7-Zip archive %s to %s. Full paths? %s', [ArchiveFileName,
+    RemoveBackslashUnlessRoot(DestDir), SYesNo[FullPaths]]);
 
-  if Password <> '' then begin
-    Log('ERROR: Password not supported by basic archive extraction'); { Just like 7zMain.c }
-    raise Exception.Create(FmtSetupMessage(msgErrorExtractionFailed, ['-2']))
-  end else if not ForceDirectories(DisableFsRedir, DestDir) then begin
-    Log('ERROR: Failed to create destination directory'); { Just like 7zMain.c }
-    raise Exception.Create(FmtSetupMessage(msgErrorExtractionFailed, ['-1']));
-  end;
+  if not ForceDirectories(DisableFsRedir, DestDir) then
+    SevenZipError(FmtSetupMessage1(msgErrorCreatingDir, DestDir), 'Failed to create destination directory');
 
   State.DisableFsRedir := DisableFsRedir;
   State.ExpandedArchiveFileName := PathExpand(ArchiveFileName);
@@ -331,9 +370,9 @@ begin
     Log(State.LogBuffer);
 
   if State.Aborted then
-    raise Exception.Create(SetupMessages[msgErrorExtractionAborted])
+    Abort
   else if Res <> 0 then
-    raise Exception.Create(FmtSetupMessage(msgErrorExtractionFailed, [Res.ToString])); { Already logged by 7zMain.c }
+    BadResultError(Res);
 end;
 
 end.

BIN
Projects/Src/Compression.SevenZipDecoder/7zDecode/IS7zDec.obj


+ 4 - 4
Projects/Src/Compression.SevenZipDecoder/7zDecode/IS7zDec.obj.issig

@@ -1,6 +1,6 @@
 format issig-v1
-file-size 93692
-file-hash 3984b8b947779519be78c34858a3bd0f5eb75cd166fd5d43102af518bf35d4f7
+file-size 93886
+file-hash 43ef412680232ecf42556737b7ea1c341f8134e64bf0b36932458e97d569d7c5
 key-id def0147c3bbc17ab99bf7b7a9c2de1390283f38972152418d7c2a4a7d7131a38
-sig-r ba2f9ec8f07c76da83c9fb175f82f939e712865069783c151775ba53d0dd00c7
-sig-s 1e56679de72a7ec1493aed723ee77ec0df4ddc469d0505a503d7641850395257
+sig-r 6311ecc5160524349d2ae725ca31f605cfed4452f9b501bc082e6f8a75211106
+sig-s f2162f7b1aae75c9328b79b334d1d66474de3ef417440ceb500a0d83c249f371

+ 8 - 8
Projects/Src/IDE.ScintStylerInnoSetup.pas

@@ -2,7 +2,7 @@ unit IDE.ScintStylerInnoSetup;
 
 {
   Inno Setup
-  Copyright (C) 1997-2024 Jordan Russell
+  Copyright (C) 1997-2025 Jordan Russell
   Portions by Martijn Laan
   For conditions of distribution and use, see LICENSE.TXT.
 
@@ -240,18 +240,18 @@ const
 
   FilesSectionParameters: array of TScintRawString = [
     'AfterInstall', 'Attribs', 'BeforeInstall', 'Check', 'Components', 'CopyMode',
-    'DestDir', 'DestName', 'Excludes', 'ExternalSize', 'Flags', 'FontInstall',
-    'ISSigAllowedKeys', 'Languages', 'MinVersion', 'OnlyBelowVersion', 'Permissions',
-    'Source', 'StrongAssemblyName', 'Tasks'
+    'DestDir', 'DestName', 'Excludes', 'ExternalSize', 'ExtractArchivePassword',
+    'Flags', 'FontInstall', 'ISSigAllowedKeys', 'Languages', 'MinVersion',
+    'OnlyBelowVersion', 'Permissions', 'Source', 'StrongAssemblyName', 'Tasks'
   ];
 
   FilesSectionFlags: array of TScintRawString = [
     '32bit', '64bit', 'allowunsafefiles', 'comparetimestamp', 'confirmoverwrite',
     'createallsubdirs', 'deleteafterinstall', 'dontcopy', 'dontverifychecksum',
-    'external', 'fontisnttruetype', 'gacinstall', 'ignoreversion', 'isreadme', 'issigverify',
-    'nocompression', 'noencryption', 'noregerror', 'onlyifdestfileexists',
-    'onlyifdoesntexist', 'overwritereadonly', 'promptifolder', 'recursesubdirs',
-    'regserver', 'regtypelib', 'replacesameversion', 'restartreplace',
+    'external', 'extractarchive', 'fontisnttruetype', 'gacinstall', 'ignoreversion',
+    'isreadme', 'issigverify', 'nocompression', 'noencryption', 'noregerror',
+    'onlyifdestfileexists', 'onlyifdoesntexist', 'overwritereadonly', 'promptifolder',
+    'recursesubdirs', 'regserver', 'regtypelib', 'replacesameversion', 'restartreplace',
     'setntfscompression', 'sharedfile', 'sign', 'signcheck', 'signonce',
     'skipifsourcedoesntexist', 'solidbreak', 'sortfilesbyextension',
     'sortfilesbyname', 'touch', 'uninsnosharedfileprompt', 'uninsremovereadonly',

+ 21 - 13
Projects/Src/Setup.InstFunc.pas

@@ -12,7 +12,7 @@ unit Setup.InstFunc;
 interface
 
 uses
-  Windows, SysUtils, Shared.Int64Em, SHA256, Shared.CommonFunc;
+  Windows, SysUtils, Shared.Int64Em, SHA256, Shared.CommonFunc, Shared.FileClass;
 
 type
   PSimpleStringListArray = ^TSimpleStringListArray;
@@ -58,7 +58,8 @@ function GenerateNonRandomUniqueTempDir(const LimitCurrentUserSidAccess: Boolean
 function GetComputerNameString: String;
 function GetFileDateTime(const DisableFsRedir: Boolean; const Filename: String;
   var DateTime: TFileTime): Boolean;
-function GetSHA256OfFile(const DisableFsRedir: Boolean; const Filename: String): TSHA256Digest;
+function GetSHA256OfFile(const DisableFsRedir: Boolean; const Filename: String): TSHA256Digest; overload;
+function GetSHA256OfFile(const F: TFile): TSHA256Digest; overload;
 function GetSHA256OfAnsiString(const S: AnsiString): TSHA256Digest;
 function GetSHA256OfUnicodeString(const S: UnicodeString): TSHA256Digest;
 function GetRegRootKeyName(const RootKey: HKEY): String;
@@ -99,7 +100,7 @@ implementation
 
 uses
   Messages, ShellApi, PathFunc, SetupLdrAndSetup.InstFunc, SetupLdrAndSetup.Messages,
-  Shared.SetupMessageIDs, Shared.FileClass, SetupLdrAndSetup.RedirFunc, Shared.SetupTypes,
+  Shared.SetupMessageIDs, SetupLdrAndSetup.RedirFunc, Shared.SetupTypes,
   Classes, RegStr, Math;
 
 procedure InternalError(const Id: String);
@@ -553,21 +554,28 @@ end;
 function GetSHA256OfFile(const DisableFsRedir: Boolean; const Filename: String): TSHA256Digest;
 { Gets SHA-256 sum as a string of the file Filename. An exception will be raised upon
   failure. }
+begin
+  const F = TFileRedir.Create(DisableFsRedir, Filename, fdOpenExisting, faRead, fsReadWrite);
+  try
+    Result := GetSHA256OfFile(F);
+  finally
+    F.Free;
+  end;
+end;
+
+function GetSHA256OfFile(const F: TFile): TSHA256Digest;
+{ Gets SHA-256 sum as a string of the file F. An exception will be raised upon
+  failure. }
 var
   Buf: array[0..65535] of Byte;
 begin
   var Context: TSHA256Context;
   SHA256Init(Context);
-  var F := TFileRedir.Create(DisableFsRedir, Filename, fdOpenExisting, faRead, fsReadWrite);
-  try
-    while True do begin
-      var NumRead := F.Read(Buf, SizeOf(Buf));
-      if NumRead = 0 then
-        Break;
-      SHA256Update(Context, Buf, NumRead);
-    end;
-  finally
-    F.Free;
+  while True do begin
+    var NumRead := F.Read(Buf, SizeOf(Buf));
+    if NumRead = 0 then
+      Break;
+    SHA256Update(Context, Buf, NumRead);
   end;
   Result := SHA256Final(Context);
 end;

+ 273 - 105
Projects/Src/Setup.Install.pas

@@ -11,16 +11,27 @@ unit Setup.Install;
 
 interface
 
+uses
+  Classes, SHA256, Shared.FileClass, Shared.SetupTypes;
+
+procedure ISSigVerifyError(const AError: TISSigVerifySignatureError;
+  const ASigFilename: String = '');
+
+procedure DoISSigVerify(const SourceF: TFile; const SourceFS: TFileStream;
+  const SourceFilename: String; const ISSigAllowedKeys: AnsiString;
+  out ExpectedFileHash: TSHA256Digest);
+
 procedure PerformInstall(var Succeeded: Boolean; const ChangesEnvironment,
   ChangesAssociations: Boolean);
 
-
 type
   TOnDownloadProgress = function(const Url, BaseName: string; const Progress, ProgressMax: Int64): Boolean of object;
 
 procedure ExtractTemporaryFile(const BaseName: String);
 function ExtractTemporaryFiles(const Pattern: String): Integer;
-function DownloadTemporaryFile(const Url, BaseName, RequiredSHA256OfFile: String; const OnDownloadProgress: TOnDownloadProgress): Int64;
+function DownloadTemporaryFile(const Url, BaseName, RequiredSHA256OfFile: String;
+  const ISSigVerify: Boolean; const ISSigAllowedKeys: AnsiString;
+  const OnDownloadProgress: TOnDownloadProgress): Int64;
 function DownloadTemporaryFileSize(const Url: String): Int64;
 function DownloadTemporaryFileDate(const Url: String): String;
 procedure SetDownloadCredentials(const User, Pass: String);
@@ -28,10 +39,10 @@ procedure SetDownloadCredentials(const User, Pass: String);
 implementation
 
 uses
-  Windows, SysUtils, Messages, Classes, Forms, ShlObj, Shared.Struct, Setup.UninstallLog, Shared.SetupTypes,
+  Windows, SysUtils, Messages, Forms, ShlObj, Shared.Struct, Setup.UninstallLog,
   SetupLdrAndSetup.InstFunc, Setup.InstFunc, Setup.InstFunc.Ole, Setup.SecurityFunc, SetupLdrAndSetup.Messages,
-  Setup.MainFunc, Setup.LoggingFunc, Setup.FileExtractor, Shared.FileClass,
-  Compression.Base, SHA256, PathFunc, ISSigFunc, Shared.CommonFunc.Vcl,
+  Setup.MainFunc, Setup.LoggingFunc, Setup.FileExtractor,
+  Compression.Base, PathFunc, ISSigFunc, Shared.CommonFunc.Vcl, Compression.SevenZipDLLDecoder,
   Shared.CommonFunc, SetupLdrAndSetup.RedirFunc, Shared.Int64Em, Shared.SetupMessageIDs,
   Setup.WizardForm, Shared.DebugStruct, Setup.DebugClient, Shared.VerInfoFunc, Setup.ScriptRunner, Setup.RegDLL, Setup.Helper,
   Shared.ResUpdateFunc, Setup.DotNetFunc, TaskbarProgressFunc, NewProgressBar, RestartManager,
@@ -250,25 +261,69 @@ begin
   end;
 end;
 
-procedure ISSigVerifyError(const AReason, AExceptionMessage: String);
+procedure ISSigVerifyError(const AError: TISSigVerifySignatureError;
+  const ASigFilename: String);
+const
+  LogMessages: array[TISSigVerifySignatureError] of String =
+    ('Signature file does not exist', 'Signature is malformed', 'No matching key found',
+     'Signature is bad', 'File size is incorrect', 'File hash is incorrect');
+  SetupMessageIDs: array[TISSigVerifySignatureError] of TSetupMessageID =
+    (msgVerificationSignatureDoesntExist, msgVerificationSignatureInvalid, msgVerificationKeyNotFound,
+     msgVerificationSignatureInvalid, msgVerificationFileSizeIncorrect, msgVerificationFileHashIncorrect);
 begin
-  Log('ISSig verification error: ' + AddPeriod(AReason));
-  raise Exception.Create(AExceptionMessage);
+  { Also see Compiler.SetupCompiler for a similar function }
+  Log('ISSig verification error: ' + AddPeriod(LogMessages[AError]));
+  raise Exception.Create(FmtSetupMessage1(msgSourceVerificationFailed,
+    FmtSetupMessage1(SetupMessageIDs[AError], PathExtractName(ASigFilename)))); { Not all messages actually have a %1 parameter but that's OK }
 end;
 
+procedure DoISSigVerify(const SourceF: TFile; const SourceFS: TFileStream;
+  const SourceFilename: String; const ISSigAllowedKeys: AnsiString;
+  out ExpectedFileHash: TSHA256Digest);
+{ Either SourceF or SourceFS must be set }
+begin
+  if ((SourceF = nil) and (SourceFS = nil)) or ((SourceF <> nil) and (SourceFS <> nil)) then
+    InternalError('DoISSigVerify: Invalid SourceF / SourceFS combination');
+
+  var ExpectedFileSize: Int64;
+  if not ISSigVerifySignature(SourceFilename,
+    GetISSigAllowedKeys(ISSigAvailableKeys, ISSigAllowedKeys),
+    ExpectedFileSize, ExpectedFileHash,
+    nil,
+    procedure(const Filename, SigFilename: String)
+    begin
+      ISSigVerifyError(vseSignatureMissing, SigFilename);
+    end,
+    procedure(const Filename, SigFilename: String; const VerifyResult: TISSigVerifySignatureResult)
+    begin
+      case VerifyResult of
+        vsrMalformed:  ISSigVerifyError(vseSignatureMalformed, SigFilename);
+        vsrBad: ISSigVerifyError(vseSignatureBad, SigFilename);
+        vsrKeyNotFound: ISSigVerifyError(vseKeyNotFound, SigFilename);
+      else
+        InternalError('Unknown ISSigVerifySignature result');
+      end;
+    end
+  ) then
+    InternalError('Unexpected ISSigVerifySignature result');
+  var FileSize: Int64;
+  if SourceF <> nil then
+    FileSize := Int64(SourceF.Size)
+  else
+    FileSize := SourceFS.Size;
+  if FileSize <> ExpectedFileSize then
+    ISSigVerifyError(vseFileSizeIncorrect);
+  { Caller must check ExpectedFileHash }
+end;
+
+const
+  ISSigVerificationSuccessfulLogMessage = 'ISSig verification successful.';
+
 procedure CopySourceFileToDestFile(const SourceF, DestF: TFile;
-  const ISSigVerify: Boolean; [ref] const ISSigAvailableKeys: TArrayOfECDSAKey;
-  const ISSigAllowedKeys: AnsiString; const ISSigSourceFilename: String;
+  const ISSigVerify: Boolean; const ISSigAllowedKeys: AnsiString; const ISSigSourceFilename: String;
   const AExpectedSize: Integer64);
 { Copies all bytes from SourceF to DestF, incrementing process meter as it
   goes. Assumes file pointers of both are 0. }
-const
-  ISSigMissingFile = 'Signature file does not exist';
-  ISSigMalformedOrBadSignature = 'Malformed or bad signature';
-  ISSigKeyNotFound = 'No matching key found';
-  ISSigUnknownVerifyResult  = 'Unknown verify result';
-  ISSigFileSizeIncorrect = 'File size incorrect';
-  ISSigFileHashIncorrect = 'File hash incorrect';
 var
   BytesLeft: Integer64;
   NewProgress: Integer64;
@@ -278,32 +333,8 @@ var
 begin
   var ExpectedFileHash: TSHA256Digest;
   if ISSigVerify then begin
-    var ExpectedFileSize: Int64;
-    if not ISSigVerifySignature(ISSigSourceFilename,
-      GetISSigAllowedKeys(ISSigAvailableKeys, ISSigAllowedKeys),
-      ExpectedFileSize, ExpectedFileHash,
-      nil,
-      procedure(const Filename, SigFilename: String)
-      begin
-        ISSigVerifyError(ISSigMissingFile, FmtSetupMessage1(msgSourceDoesntExist, SigFilename));
-      end,
-      procedure(const SigFilename: String; const VerifyResult: TISSigVerifySignatureResult)
-      begin
-        var VerifyResultAsString: String;
-        case VerifyResult of
-          vsrMalformed, vsrBadSignature: VerifyResultAsString := ISSigMalformedOrBadSignature;
-          vsrKeyNotFound: VerifyResultAsString := ISSigKeyNotFound;
-        else
-          VerifyResultAsString := ISSigUnknownVerifyResult;
-        end;
-        ISSigVerifyError(VerifyResultAsString, SetupMessages[msgSourceIsCorrupted]);
-      end
-    ) then
-      InternalError('Unexpected ISSigVerifySignature result');
-    if Int64(SourceF.Size) <> ExpectedFileSize then
-      ISSigVerifyError(ISSigFileSizeIncorrect, SetupMessages[msgSourceIsCorrupted]);
+    DoISSigVerify(SourceF, nil, ISSigSourceFilename, ISSigAllowedKeys, ExpectedFileHash);
     { ExpectedFileHash checked below after copy }
-
     SHA256Init(Context);
   end;
 
@@ -344,8 +375,8 @@ begin
 
   if ISSigVerify then begin
     if not SHA256DigestsEqual(SHA256Final(Context), ExpectedFileHash) then
-      ISSigVerifyError(ISSigFileHashIncorrect, SetupMessages[msgSourceIsCorrupted]);
-    Log('ISSig verification successful.');
+      ISSigVerifyError(vseFileHashIncorrect);
+    Log(ISSigVerificationSuccessfulLogMessage);
   end;
 
   { In case the source file was shorter than we thought it was, bump the
@@ -960,10 +991,16 @@ var
     TOverwriteAll = (oaUnknown, oaOverwrite, oaKeep);
 
   procedure ProcessFileEntry(const CurFile: PSetupFileEntry;
-    const DisableFsRedir: Boolean; ASourceFile, ADestName: String;
+    const DisableFsRedir: Boolean; AExternalSourceFile, ADestFile: String;
     const FileLocationFilenames: TStringList; const AExternalSize: Integer64;
     var ConfirmOverwriteOverwriteAll, PromptIfOlderOverwriteAll: TOverwriteAll;
-    var WarnedPerUserFonts: Boolean);
+    var WarnedPerUserFonts: Boolean; const AExternalFileDate: PFileTime);
+  { Not external: AExternalSourceFile and ADestFile should be empty strings,
+                  FileLocationFilenames should be set, AExternalSize is unused,
+                  AExternalFileDate should not be set
+    External    : Opposite except AExternalFileDate still not set
+    Ext. Archive: Same as external except AExternalFileDate set and
+                  AExternalSourceFile should be set to ArchiveFindHandle as a string }
 
     procedure InstallFont(const Filename, FontName: String;
       const PerUserFont, AddToFontTableNow: Boolean; var WarnedPerUserFonts: Boolean);
@@ -1192,10 +1229,10 @@ var
           case CurFile^.FileType of
             ftUninstExe: DestFile := UninstallExeFilename;
           else
-            if ADestName = '' then
+            if ADestFile = '' then
               DestFile := ExpandConst(CurFile^.DestName)
             else
-              DestFile := ADestName;
+              DestFile := ADestFile;
           end;
           DestFile := PathExpand(DestFile);
         except
@@ -1236,9 +1273,11 @@ var
           else
             LocalFileTimeToFileTime(CurFileLocation^.SourceTimeStamp, CurFileDate);
           CurFileDateValid := True;
-        end
-        else
-          CurFileDateValid := GetFileDateTime(DisableFsRedir, ASourceFile, CurFileDate);
+        end else if Assigned(AExternalFileDate) then begin
+          CurFileDate := AExternalFileDate^;
+          CurFileDateValid := CurFileDate.HasTime;
+        end else
+          CurFileDateValid := GetFileDateTime(DisableFsRedir, AExternalSourceFile, CurFileDate);
         if CurFileDateValid then
           LogFmt('Time stamp of our file: %s', [FileTimeToStr(CurFileDate)])
         else
@@ -1263,6 +1302,8 @@ var
           if not(foIgnoreVersion in CurFile^.Options) then begin
             AllowTimeStampComparison := False;
             { Read version info of file being installed }
+            if foExtractArchive in CurFile^.Options then
+              InternalError('Unexpected extractarchive flag');
             if Assigned(CurFileLocation) then begin
               CurFileVersionInfoValid := floVersionInfoValid in CurFileLocation^.Flags;
               CurFileVersionInfo.MS := CurFileLocation^.FileVersionMS;
@@ -1270,7 +1311,7 @@ var
             end
             else
               CurFileVersionInfoValid := GetVersionNumbersRedir(DisableFsRedir,
-                PathExpand(ASourceFile), CurFileVersionInfo);
+                  PathExpand(AExternalSourceFile), CurFileVersionInfo);
             if CurFileVersionInfoValid then
               LogFmt('Version of our file: %u.%u.%u.%u',
                 [LongRec(CurFileVersionInfo.MS).Hi, LongRec(CurFileVersionInfo.MS).Lo,
@@ -1288,7 +1329,7 @@ var
                  ((ExistingVersionInfo.MS > CurFileVersionInfo.MS) or
                   ((ExistingVersionInfo.MS = CurFileVersionInfo.MS) and
                    (ExistingVersionInfo.LS > CurFileVersionInfo.LS))) then begin
-                { Existing file is newer, ask user what to do unless we shouldn't }
+                { No version info, or existing file is newer, ask user what to do unless we shouldn't }
                 if (foPromptIfOlder in CurFile^.Options) and not IsProtectedFile then begin
                   if PromptIfOlderOverwriteAll <> oaOverwrite then begin
                     Overwrite := AskOverwrite(DestFile, SetupMessages[msgExistingFileNewerSelectAction],
@@ -1322,7 +1363,7 @@ var
                         { This GetSHA256OfFile call could raise an exception, but
                           it's very unlikely since we were already able to
                           successfully read the file's version info. }
-                        CurFileHash := GetSHA256OfFile(DisableFsRedir, ASourceFile);
+                        CurFileHash := GetSHA256OfFile(DisableFsRedir, AExternalSourceFile);
                         LastOperation := SetupMessages[msgErrorReadingExistingDest];
                       end;
                       { If the two files' SHA-256 hashes are equal, skip the file }
@@ -1455,12 +1496,12 @@ var
         Log('Installing the file.');
 
         { Locate source file }
-        SourceFile := ASourceFile;
+        SourceFile := AExternalSourceFile; { Empty string if not external }
         if DisableFsRedir = InstallDefaultDisableFsRedir then begin
           { If the file is compressed in the setup package, has the same file
             already been copied somewhere else? If so, just make a duplicate of
             that file instead of extracting it over again. }
-          if (SourceFile = '') and
+          if (SourceFile = '') and (FileLocationFilenames <> nil) and
              (FileLocationFilenames[CurFile^.LocationEntry] <> '') and
              NewFileExistsRedir(DisableFsRedir, FileLocationFilenames[CurFile^.LocationEntry]) then
             SourceFile := FileLocationFilenames[CurFile^.LocationEntry];
@@ -1495,17 +1536,23 @@ var
               FileExtractor.DecompressFile(CurFileLocation^, DestF, ExtractorProgressProc,
                 not (foDontVerifyChecksum in CurFile^.Options));
             end
+            else if foExtractArchive in CurFile^.Options then begin
+              { Extract a file from archive. Note: foISSigVerify for archive has
+                already been handled by RecurseExternalArchiveCopyFiles. }
+              LastOperation := SetupMessages[msgErrorExtracting];
+              ArchiveFindExtract(StrToInt(SourceFile), DestF, ExtractorProgressProc);
+            end
             else begin
-              { Copy an external file, or a duplicated non-external file }
+              { Copy a duplicated non-external file, or an external file }
               SourceF := TFileRedir.Create(DisableFsRedir, SourceFile, fdOpenExisting, faRead, fsRead);
               try
                 LastOperation := SetupMessages[msgErrorCopying];
                 if Assigned(CurFileLocation) then
                   CopySourceFileToDestFile(SourceF, DestF, False,
-                    [], '', '', CurFileLocation^.OriginalSize)
+                    '', '', CurFileLocation^.OriginalSize)
                 else
                   CopySourceFileToDestFile(SourceF, DestF, foISSigVerify in CurFile^.Options,
-                    ISSigAvailableKeys, CurFile^.ISSigAllowedKeys, SourceFile, AExternalSize);
+                    CurFile^.ISSigAllowedKeys, SourceFile, AExternalSize);
               finally
                 SourceF.Free;
               end;
@@ -1794,26 +1841,23 @@ var
 
     function RecurseExternalCopyFiles(const DisableFsRedir: Boolean;
       const SearchBaseDir, SearchSubDir, SearchWildcard: String; const SourceIsWildcard: Boolean;
-      const Excludes: TStringList; const CurFile: PSetupFileEntry; const FileLocationFilenames: TStringList;
-      var ExpectedBytesLeft: Integer64; var ConfirmOverwriteOverwriteAll, PromptIfOlderOverwriteAll: TOverwriteAll;
+      const Excludes: TStrings; const CurFile: PSetupFileEntry; var ExpectedBytesLeft: Integer64;
+      var ConfirmOverwriteOverwriteAll, PromptIfOlderOverwriteAll: TOverwriteAll;
       var WarnedPerUserFonts: Boolean): Boolean;
-    var
-      FileName, SourceFile, DestName: String;
-      H: THandle;
-      FindData: TWin32FindData;
-      Size: Integer64;
-      Flags: TMakeDirFlags;
     begin
-      { Also see RecurseExternalFiles and RecurseExternalGetSizeOfFiles in Setup.MainFunc }
+      { Also see RecurseExternalFiles and RecurseExternalGetSizeOfFiles in Setup.MainFunc
+        Also see RecurseExternalArchiveCopyFiles directly below }
 
       Result := False;
 
-      H := FindFirstFileRedir(DisableFsRedir, SearchBaseDir + SearchSubDir + SearchWildcard, FindData);
+      var FindData: TWin32FindData;
+      var H := FindFirstFileRedir(DisableFsRedir, SearchBaseDir + SearchSubDir + SearchWildcard, FindData);
       if H <> INVALID_HANDLE_VALUE then begin
         try
           repeat
             if FindData.dwFileAttributes and FILE_ATTRIBUTE_DIRECTORY = 0 then begin
 
+              var FileName: String;
               if SourceIsWildcard then begin
                 if FindData.dwFileAttributes and FILE_ATTRIBUTE_HIDDEN <> 0 then
                   Continue;
@@ -1826,12 +1870,15 @@ var
                 Continue;
 
               Result := True;
-              SourceFile := SearchBaseDir + SearchSubDir + FileName;
-              DestName := ExpandConst(CurFile^.DestName);
+              var SourceFile := SearchBaseDir + SearchSubDir + FileName;
+              { Note: CurFile^.DestName only includes a a filename if foCustomDestName is set,
+                see TSetupCompiler.EnumFilesProc.ProcessFileList }
+              var DestFile := ExpandConst(CurFile^.DestName);
               if not(foCustomDestName in CurFile^.Options) then
-                DestName := DestName + SearchSubDir + FileName
+                DestFile := DestFile + SearchSubDir + FileName
               else if SearchSubDir <> '' then
-                DestName := PathExtractPath(DestName) + SearchSubDir + PathExtractName(DestName);
+                DestFile := PathExtractPath(DestFile) + SearchSubDir + PathExtractName(DestFile);
+              var Size: Integer64;
               Size.Hi := FindData.nFileSizeHigh;
               Size.Lo := FindData.nFileSizeLow;
               if Compare64(Size, ExpectedBytesLeft) > 0 then begin
@@ -1839,9 +1886,9 @@ var
                   files is greater than when we last checked }
                 Size := ExpectedBytesLeft;
               end;
-              ProcessFileEntry(CurFile, DisableFsRedir, SourceFile, DestName,
-                FileLocationFilenames, Size, ConfirmOverwriteOverwriteAll, PromptIfOlderOverwriteAll,
-                WarnedPerUserFonts);
+              ProcessFileEntry(CurFile, DisableFsRedir, SourceFile, DestFile, nil,
+                Size, ConfirmOverwriteOverwriteAll, PromptIfOlderOverwriteAll,
+                WarnedPerUserFonts, nil);
               Dec6464(ExpectedBytesLeft, Size);
             end;
           until not FindNextFile(H, FindData);
@@ -1858,8 +1905,8 @@ var
               if IsRecurseableDirectory(FindData) then
                 Result := RecurseExternalCopyFiles(DisableFsRedir, SearchBaseDir,
                   SearchSubDir + FindData.cFileName + '\', SearchWildcard,
-                  SourceIsWildcard, Excludes, CurFile, FileLocationFileNames,
-                  ExpectedBytesLeft, ConfirmOverwriteOverwriteAll, PromptIfOlderOverwriteAll,
+                  SourceIsWildcard, Excludes, CurFile, ExpectedBytesLeft,
+                  ConfirmOverwriteOverwriteAll, PromptIfOlderOverwriteAll,
                   WarnedPerUserFonts) or Result;
             until not FindNextFile(H, FindData);
           finally
@@ -1871,13 +1918,13 @@ var
       if SearchSubDir <> '' then begin
         { If Result is False this subdir won't be created, so create it now if
           CreateAllSubDirs was set }
-        if (foCreateAllSubDirs in CurFile.Options) and not Result then begin
-          DestName := ExpandConst(CurFile^.DestName);
+        if not Result and (foCreateAllSubDirs in CurFile.Options) then begin
+          var DestName := ExpandConst(CurFile^.DestName); { See above }
           if not(foCustomDestName in CurFile^.Options) then
             DestName := DestName + SearchSubDir
           else
             DestName := PathExtractPath(DestName) + SearchSubDir;
-          Flags := [];
+          var Flags: TMakeDirFlags := [];
           if foUninsNeverUninstall in CurFile^.Options then Include(Flags, mdNoUninstall);
           if foDeleteAfterInstall in CurFile^.Options then Include(Flags, mdDeleteAfterInstall);
           MakeDir(DisableFsRedir, DestName, Flags);
@@ -1891,6 +1938,92 @@ var
       ProcessEvents;
     end;
 
+    function RecurseExternalArchiveCopyFiles(const DisableFsRedir: Boolean;
+      const ArchiveFilename, Password: String; const Excludes: TStrings;
+      const CurFile: PSetupFileEntry; var ExpectedBytesLeft: Integer64;
+      var ConfirmOverwriteOverwriteAll, PromptIfOlderOverwriteAll: TOverwriteAll;
+      var WarnedPerUserFonts: Boolean): Boolean;
+    begin
+      { See above }
+
+      Result := NewFileExistsRedir(DisableFsRedir, ArchiveFilename);
+
+      if foCustomDestName in CurFile^.Options then
+        InternalError('Unexpected custom DestName');
+      const DestDir = ExpandConst(CurFile^.DestName);
+
+      var ISSigVerifySourceF: TFile := nil;
+      try
+        var FindData: TWin32FindData;
+        var H: TArchiveFindHandle := INVALID_HANDLE_VALUE;
+        var Failed: String;
+        repeat
+          try
+            if foISSigVerify in CurFile^.Options then begin
+              if ISSigVerifySourceF = nil then
+                ISSigVerifySourceF := TFileRedir.Create(DisableFsRedir, ArchiveFilename, fdOpenExisting, faRead, fsRead);
+              var ExpectedFileHash: TSHA256Digest;
+              DoISSigVerify(ISSigVerifySourceF, nil, ArchiveFilename, CurFile^.ISSigAllowedKeys,
+                ExpectedFileHash);
+              { Can't get the SHA-256 while extracting so need to get and check it now }
+              const ActualFileHash = GetSHA256OfFile(ISSigVerifySourceF);
+              if not SHA256DigestsEqual(ActualFileHash, ExpectedFileHash) then
+                ISSigVerifyError(vseFileHashIncorrect, SetupMessages[msgSourceIsCorrupted]);
+              Log(ISSigVerificationSuccessfulLogMessage);
+              { Keeping ISSigVerifySourceF open until extraction has completed }
+            end;
+
+            H := ArchiveFindFirstFileRedir(DisableFsRedir, ArchiveFilename, DestDir,
+              Password, foRecurseSubDirsExternal in CurFile^.Options, True, FindData);
+            Failed := '';
+          except
+            if ExceptObject is EAbort then
+              raise;
+            Failed := GetExceptMessage;
+          end;
+        until (Failed = '') or
+              AbortRetryIgnoreTaskDialogMsgBox(
+                ArchiveFilename + SNewLine2 + SetupMessages[msgErrorExtracting] + SNewLine + Failed,
+                [SetupMessages[msgAbortRetryIgnoreRetry], SetupMessages[msgFileAbortRetryIgnoreSkipNotRecommended], SetupMessages[msgAbortRetryIgnoreCancel]]);
+        if H <> INVALID_HANDLE_VALUE then begin
+          try
+            repeat
+              if FindData.dwFileAttributes and FILE_ATTRIBUTE_DIRECTORY = 0 then begin
+
+                if IsExcluded(FindData.cFileName, Excludes) then
+                  Continue;
+
+                var SourceFile := IntToStr(H);
+                const DestFile = DestDir + FindData.cFileName;
+                var Size: Integer64;
+                Size.Hi := FindData.nFileSizeHigh;
+                Size.Lo := FindData.nFileSizeLow;
+                if Compare64(Size, ExpectedBytesLeft) > 0 then begin
+                  { Don't allow the progress bar to overflow if the size of the
+                    files is greater than when we last checked }
+                  Size := ExpectedBytesLeft;
+                end;
+                ProcessFileEntry(CurFile, DisableFsRedir, SourceFile, DestFile,
+                  nil, Size, ConfirmOverwriteOverwriteAll, PromptIfOlderOverwriteAll,
+                  WarnedPerUserFonts, @FindData.ftLastWriteTime);
+                Dec6464(ExpectedBytesLeft, Size);
+              end else if foCreateAllSubDirs in CurFile.Options then begin
+                var Flags: TMakeDirFlags := [];
+                if foUninsNeverUninstall in CurFile^.Options then Include(Flags, mdNoUninstall);
+                if foDeleteAfterInstall in CurFile^.Options then Include(Flags, mdDeleteAfterInstall);
+                MakeDir(DisableFsRedir, DestDir + FindData.cFileName, Flags);
+                Result := True;
+              end;
+            until not ArchiveFindNextFile(H, FindData);
+          finally
+            ArchiveFindClose(H);
+          end;
+        end;
+      finally
+        ISSigVerifySourceF.Free;
+      end;
+    end;
+
   var
     I: Integer;
     CurFileNumber: Integer;
@@ -1937,7 +2070,7 @@ var
             ExternalSize.Hi := 0;  { not used... }
             ExternalSize.Lo := 0;
             ProcessFileEntry(CurFile, DisableFsRedir, '', '', FileLocationFilenames, ExternalSize,
-              ConfirmOverwriteOverwriteAll, PromptIfOlderOverwriteAll, WarnedPerUserFonts);
+              ConfirmOverwriteOverwriteAll, PromptIfOlderOverwriteAll, WarnedPerUserFonts, nil);
           end
           else begin
             { File is an 'external' file }
@@ -1953,11 +2086,17 @@ var
             repeat
               SetProgress(ProgressBefore);
               ExpectedBytesLeft := CurFile^.ExternalSize;
-              FoundFiles := RecurseExternalCopyFiles(DisableFsRedir,
-                PathExtractPath(SourceWildcard), '', PathExtractName(SourceWildcard),
-                IsWildcard(SourceWildcard), Excludes, CurFile, FileLocationFileNames,
-                ExpectedBytesLeft, ConfirmOverwriteOverwriteAll, PromptIfOlderOverwriteAll,
-                WarnedPerUserFonts);
+              if foExtractArchive in CurFile^.Options then
+                FoundFiles := RecurseExternalArchiveCopyFiles(DisableFsRedir,
+                  SourceWildcard, CurFile^.ExtractArchivePassword, Excludes, CurFile,
+                  ExpectedBytesLeft, ConfirmOverwriteOverwriteAll, PromptIfOlderOverwriteAll,
+                  WarnedPerUserFonts)
+              else
+                FoundFiles := RecurseExternalCopyFiles(DisableFsRedir,
+                  PathExtractPath(SourceWildcard), '', PathExtractName(SourceWildcard),
+                  IsWildcard(SourceWildcard), Excludes, CurFile,
+                  ExpectedBytesLeft, ConfirmOverwriteOverwriteAll, PromptIfOlderOverwriteAll,
+                  WarnedPerUserFonts);
             until FoundFiles or
                   (foSkipIfSourceDoesntExist in CurFile^.Options) or
                   AbortRetryIgnoreTaskDialogMsgBox(
@@ -3551,7 +3690,9 @@ begin
     Log('Download is not using basic authentication');
 end;
 
-function DownloadTemporaryFile(const Url, BaseName, RequiredSHA256OfFile: String; const OnDownloadProgress: TOnDownloadProgress): Int64;
+function DownloadTemporaryFile(const Url, BaseName, RequiredSHA256OfFile: String;
+  const ISSigVerify: Boolean; const ISSigAllowedKeys: AnsiString;
+  const OnDownloadProgress: TOnDownloadProgress): Int64;
 var
   DestFile, TempFile: String;
   TempF: TFileRedir;
@@ -3579,9 +3720,26 @@ begin
   const DisableFsRedir = False; { Like everything else working on the temp dir }
 
   { Prepare directory }
-  if FileExists(DestFile) then begin
-    if (RequiredSHA256OfFile <> '') and
-       (RequiredSHA256OfFile = SHA256DigestToString(GetSHA256OfFile(DisableFsRedir, DestFile))) then begin
+  if NewFileExists(DestFile) then begin
+    if ISSigVerify then begin
+      var ExistingFileSize: Int64;
+      var ExistingFileHash: TSHA256Digest;
+      if ISSigVerifySignature(DestFile, GetISSigAllowedKeys(ISSigAvailableKeys, ISSigAllowedKeys),
+           ExistingFileSize, ExistingFileHash, nil, nil, nil) then begin
+        const DestF = TFileRedir.Create(DisableFsRedir, DestFile, fdOpenExisting, faRead, fsReadWrite);
+        try
+          if (Int64(DestF.Size) = ExistingFileSize) and
+             (SHA256DigestsEqual(GetSHA256OfFile(DestF), ExistingFileHash)) then begin
+            Log('  File already downloaded.');
+            Result := 0;
+            Exit;
+          end;
+        finally
+          DestF.Free;
+        end;
+      end;
+    end else if (RequiredSHA256OfFile <> '') and
+                SameText(RequiredSHA256OfFile, SHA256DigestToString(GetSHA256OfFile(DisableFsRedir, DestFile))) then begin
       Log('  File already downloaded.');
       Result := 0;
       Exit;
@@ -3639,22 +3797,32 @@ begin
       { Download completed, get temporary file size and close it }
       Result := HandleStream.Size;
       FreeAndNil(HandleStream);
-      FreeAndNil(TempF);
 
-      { Check hash if specified, otherwise check everything else we can check }
-      if RequiredSHA256OfFile <> '' then begin
-        try
-          SHA256OfFile := SHA256DigestToString(GetSHA256OfFile(DisableFsRedir, TempFile));
-        except on E: Exception do
-          raise Exception.Create(FmtSetupMessage(msgErrorFileHash1, [E.Message]));
+      { Check .issig or hash if specified, otherwise check everything else we can check }
+      if ISSigVerify then begin
+        var ExpectedFileHash: TSHA256Digest;
+        DoISSigVerify(TempF, nil, DestFile, ISSigAllowedKeys, ExpectedFileHash);
+        FreeAndNil(TempF);
+        const FileHash = GetSHA256OfFile(DisableFsRedir, TempFile);
+        if not SHA256DigestsEqual(FileHash, ExpectedFileHash) then
+          ISSigVerifyError(vseFileHashIncorrect, SetupMessages[msgSourceIsCorrupted]);
+        Log(ISSigVerificationSuccessfulLogMessage);
+      end else begin
+        FreeAndNil(TempF);
+        if RequiredSHA256OfFile <> '' then begin
+          try
+            SHA256OfFile := SHA256DigestToString(GetSHA256OfFile(DisableFsRedir, TempFile));
+          except on E: Exception do
+            raise Exception.Create(FmtSetupMessage(msgErrorFileHash1, [E.Message]));
+          end;
+          if not SameText(RequiredSHA256OfFile, SHA256OfFile) then
+            raise Exception.Create(FmtSetupMessage(msgErrorFileHash2, [RequiredSHA256OfFile, SHA256OfFile]));
+        end else if HTTPDataReceiver.ProgressMax > 0 then begin
+          if HTTPDataReceiver.Progress <> HTTPDataReceiver.ProgressMax then
+            raise Exception.Create(FmtSetupMessage(msgErrorProgress, [IntToStr(HTTPDataReceiver.Progress), IntToStr(HTTPDataReceiver.ProgressMax)]))
+          else if HTTPDataReceiver.ProgressMax <> Result then
+            raise Exception.Create(FmtSetupMessage(msgErrorFileSize, [IntToStr(HTTPDataReceiver.ProgressMax), IntToStr(Result)]));
         end;
-        if not SameText(RequiredSHA256OfFile, SHA256OfFile) then
-          raise Exception.Create(FmtSetupMessage(msgErrorFileHash2, [RequiredSHA256OfFile, SHA256OfFile]));
-      end else if HTTPDataReceiver.ProgressMax > 0 then begin
-        if HTTPDataReceiver.Progress <> HTTPDataReceiver.ProgressMax then
-          raise Exception.Create(FmtSetupMessage(msgErrorProgress, [IntToStr(HTTPDataReceiver.Progress), IntToStr(HTTPDataReceiver.ProgressMax)]))
-        else if HTTPDataReceiver.ProgressMax <> Result then
-          raise Exception.Create(FmtSetupMessage(msgErrorFileSize, [IntToStr(HTTPDataReceiver.ProgressMax), IntToStr(Result)]));
       end;
 
       { Rename the temporary file to the new name now, with retries if needed }

+ 124 - 41
Projects/Src/Setup.MainFunc.pas

@@ -246,7 +246,8 @@ uses
   Setup.WizardForm, Setup.DebugClient, Shared.VerInfoFunc, Setup.FileExtractor,
   Shared.FileClass, Setup.LoggingFunc,
   SimpleExpression, Setup.Helper, Setup.SpawnClient, Setup.SpawnServer,
-  Setup.DotNetFunc, Shared.TaskDialogFunc, Setup.MainForm, Compression.SevenZipDLLDecoder;
+  Setup.DotNetFunc, Shared.TaskDialogFunc, Setup.MainForm, Compression.SevenZipDecoder,
+  Compression.SevenZipDLLDecoder;
 
 var
   ShellFolders: array[Boolean, TShellFolderID] of String;
@@ -1746,16 +1747,14 @@ function EnumFiles(const EnumFilesProc: TEnumFilesProc;
 
   function RecurseExternalFiles(const DisableFsRedir: Boolean;
     const SearchBaseDir, SearchSubDir, SearchWildcard: String;
-    const SourceIsWildcard: Boolean; const Excludes: TStringList; const CurFile: PSetupFileEntry): Boolean;
-  var
-    DestName: String;
-    H: THandle;
-    FindData: TWin32FindData;
+    const SourceIsWildcard: Boolean; const Excludes: TStrings; const CurFile: PSetupFileEntry): Boolean;
   begin
-    { Also see RecurseExternalGetSizeOfFiles below and RecurseExternalCopyFiles in Setup.Install }
+    { Also see RecurseExternalGetSizeOfFiles below and RecurseExternalCopyFiles in Setup.Install
+      Also see RecurseExternalArchiveFiles directly below }
     Result := True;
 
-    H := FindFirstFileRedir(DisableFsRedir, SearchBaseDir + SearchSubDir + SearchWildcard, FindData);
+    var FindData: TWin32FindData;
+    var H := FindFirstFileRedir(DisableFsRedir, SearchBaseDir + SearchSubDir + SearchWildcard, FindData);
     if H <> INVALID_HANDLE_VALUE then begin
       try
         repeat
@@ -1768,12 +1767,14 @@ function EnumFiles(const EnumFilesProc: TEnumFilesProc;
             if IsExcluded(SearchSubDir + FindData.cFileName, Excludes) then
               Continue;
 
-            DestName := ExpandConst(CurFile^.DestName);
+            { Note: CurFile^.DestName only includes a a filename if foCustomDestName is set,
+              see TSetupCompiler.EnumFilesProc.ProcessFileList }
+            var DestFile := ExpandConst(CurFile^.DestName);
             if not(foCustomDestName in CurFile^.Options) then
-              DestName := DestName + SearchSubDir + FindData.cFileName
+              DestFile := DestFile + SearchSubDir + FindData.cFileName
             else if SearchSubDir <> '' then
-              DestName := PathExtractPath(DestName) + SearchSubDir + PathExtractName(DestName);
-            if not EnumFilesProc(DisableFsRedir, DestName, Param) then begin
+              DestFile := PathExtractPath(DestFile) + SearchSubDir + PathExtractName(DestFile);
+            if not EnumFilesProc(DisableFsRedir, DestFile, Param) then begin
               Result := False;
               Exit;
             end;
@@ -1792,10 +1793,8 @@ function EnumFiles(const EnumFilesProc: TEnumFilesProc;
             if IsRecurseableDirectory(FindData) then
               if not RecurseExternalFiles(DisableFsRedir, SearchBaseDir,
                  SearchSubDir + FindData.cFileName + '\', SearchWildcard,
-                 SourceIsWildcard, Excludes, CurFile) then begin
-                Result := False;
-                Exit;
-              end;
+                 SourceIsWildcard, Excludes, CurFile) then
+                Exit(False);
           until not FindNextFile(H, FindData);
         finally
           Windows.FindClose(H);
@@ -1804,6 +1803,39 @@ function EnumFiles(const EnumFilesProc: TEnumFilesProc;
     end;
   end;
 
+  function RecurseExternalArchiveFiles(const DisableFsRedir: Boolean;
+    const ArchiveFilename, Password: String; const Excludes: TStrings;
+    const CurFile: PSetupFileEntry): Boolean;
+  begin
+    { See above }
+    Result := True;
+
+    if foCustomDestName in CurFile^.Options then
+      InternalError('Unexpected custom DestName');
+    const DestDir = ExpandConst(CurFile^.DestName);
+
+    var FindData: TWin32FindData;
+    var H := ArchiveFindFirstFileRedir(DisableFsRedir, ArchiveFilename, DestDir,
+      Password, foRecurseSubDirsExternal in CurFile^.Options, False, FindData);
+    if H <> INVALID_HANDLE_VALUE then begin
+      try
+        repeat
+          if FindData.dwFileAttributes and FILE_ATTRIBUTE_DIRECTORY = 0 then begin
+
+            if IsExcluded(FindData.cFileName, Excludes) then
+              Continue;
+
+            const DestFile = DestDir + FindData.cFileName;
+            if not EnumFilesProc(DisableFsRedir, DestFile, Param) then
+              Exit(False);
+          end;
+        until not ArchiveFindNextFile(H, FindData);
+      finally
+        ArchiveFindClose(H);
+      end;
+    end;
+  end;
+
 var
   I: Integer;
   CurFile: PSetupFileEntry;
@@ -1834,10 +1866,19 @@ begin
           { External file }
           SourceWildcard := ExpandConst(CurFile^.SourceFilename);
           Excludes.DelimitedText := CurFile^.Excludes;
-          if not RecurseExternalFiles(DisableFsRedir, PathExtractPath(SourceWildcard), '',
-             PathExtractName(SourceWildcard), IsWildcard(SourceWildcard), Excludes, CurFile) then begin
-            Result := False;
-            Exit;
+          if foExtractArchive in CurFile^.Options then begin
+            try
+              if not RecurseExternalArchiveFiles(DisableFsRedir, SourceWildcard,
+                 CurFile^.ExtractArchivePassword, Excludes, CurFile) then
+                Exit(False);
+            except on E: ESevenZipError do
+              { Ignore archive errors for now, will show up with proper UI during
+                installation }
+            end;
+          end else begin
+            if not RecurseExternalFiles(DisableFsRedir, PathExtractPath(SourceWildcard), '',
+               PathExtractName(SourceWildcard), IsWildcard(SourceWildcard), Excludes, CurFile) then
+              Exit(False);
           end;
         end;
       end;
@@ -2029,7 +2070,7 @@ begin
     try
       EnumFiles(RegisterFile, WizardComponents, WizardTasks, Pointer(True));
     except
-      Log('EnumFiles(RegisterFiles) raised an exception.');
+      Log('EnumFiles(RegisterFile) raised an exception.');
       Application.HandleException(nil);
     end;
     { Ask [Code] for more files. }
@@ -2725,17 +2766,16 @@ var
 
   function RecurseExternalGetSizeOfFiles(const DisableFsRedir: Boolean;
     const SearchBaseDir, SearchSubDir, SearchWildcard: String;
-    const SourceIsWildcard: Boolean; const Excludes: TStringList; const RecurseSubDirs: Boolean): Integer64;
-  var
-    H: THandle;
-    FindData: TWin32FindData;
-    I: Integer64;
+    const SourceIsWildcard: Boolean; const Excludes: TStrings;
+    const RecurseSubDirs: Boolean): Integer64;
   begin
-    { Also see RecurseExternalFiles above and RecurseExternalCopyFiles in Setup.Install }
+    { Also see RecurseExternalFiles above and RecurseExternalCopyFiles in Setup.Install
+      Also see RecurseExternalArchiveGetSizeOfFiles directly below }
     Result.Hi := 0;
     Result.Lo := 0;
 
-    H := FindFirstFileRedir(DisableFsRedir, SearchBaseDir + SearchSubDir + SearchWildcard, FindData);
+    var FindData: TWin32FindData;
+    var H := FindFirstFileRedir(DisableFsRedir, SearchBaseDir + SearchSubDir + SearchWildcard, FindData);
     if H <> INVALID_HANDLE_VALUE then begin
       repeat
         if FindData.dwFileAttributes and FILE_ATTRIBUTE_DIRECTORY = 0 then begin
@@ -2747,6 +2787,7 @@ var
           if IsExcluded(SearchSubDir + FindData.cFileName, Excludes) then
             Continue;
 
+          var I: Integer64;
           I.Hi := FindData.nFileSizeHigh;
           I.Lo := FindData.nFileSizeLow;
           Inc6464(Result, I);
@@ -2761,7 +2802,7 @@ var
         try
           repeat
             if IsRecurseableDirectory(FindData) then begin
-              I := RecurseExternalGetSizeOfFiles(DisableFsRedir, SearchBaseDir,
+              var I := RecurseExternalGetSizeOfFiles(DisableFsRedir, SearchBaseDir,
                 SearchSubDir + FindData.cFileName + '\', SearchWildcard,
                 SourceIsWildcard, Excludes, RecurseSubDirs);
               Inc6464(Result, I);
@@ -2773,6 +2814,38 @@ var
       end;
     end;
   end;
+
+  function RecurseExternalArchiveGetSizeOfFiles(const DisableFsRedir: Boolean;
+    const ArchiveFilename, Password: String; const Excludes: TStrings;
+    const RecurseSubDirs: Boolean): Integer64;
+  begin
+    { See above }
+    Result.Hi := 0;
+    Result.Lo := 0;
+
+    var FindData: TWin32FindData;
+    var H := ArchiveFindFirstFileRedir(DisableFsRedir, ArchiveFilename,
+      AddBackslash(TempInstallDir), { DestDir isn't known yet, pass a placeholder }
+      Password, RecurseSubDirs, False, FindData);
+    if H <> INVALID_HANDLE_VALUE then begin
+      try
+        repeat
+          if FindData.dwFileAttributes and FILE_ATTRIBUTE_DIRECTORY = 0 then begin
+
+            if IsExcluded(FindData.cFileName, Excludes) then
+              Continue;
+
+            var I: Integer64;
+            I.Hi := FindData.nFileSizeHigh;
+            I.Lo := FindData.nFileSizeLow;
+            Inc6464(Result, I);
+          end;
+        until not ArchiveFindNextFile(H, FindData);
+      finally
+        ArchiveFindClose(H);
+      end;
+    end;
+  end;
   
   { Also see Install.pas }
   function ExistingInstallationAt(const RootKey: HKEY; const SubkeyName: String): Boolean;
@@ -3444,19 +3517,27 @@ begin
         end else begin
           if not(foExternalSizePreset in Options) then begin
             try
-              if FileType <> ftUserFile then
-                SourceWildcard := NewParamStr(0)
-              else
-                SourceWildcard := ExpandConst(SourceFilename);
               LExcludes.DelimitedText := Excludes;
-              ExternalSize := RecurseExternalGetSizeOfFiles(
-                ShouldDisableFsRedirForFileEntry(PSetupFileEntry(Entries[seFile][I])),
-                PathExtractPath(SourceWildcard),
-                '', PathExtractName(SourceWildcard), IsWildcard(SourceWildcard),
-                LExcludes, foRecurseSubDirsExternal in Options);
+              if foExtractArchive in Options then begin
+                ExternalSize := RecurseExternalArchiveGetSizeOfFiles(
+                  ShouldDisableFsRedirForFileEntry(PSetupFileEntry(Entries[seFile][I])),
+                  ExpandConst(SourceFilename), ExtractArchivePassword, LExcludes,
+                  foRecurseSubDirsExternal in Options);
+              end else begin
+                if FileType <> ftUserFile then
+                  SourceWildcard := NewParamStr(0)
+                else
+                  SourceWildcard := ExpandConst(SourceFilename);
+                ExternalSize := RecurseExternalGetSizeOfFiles(
+                  ShouldDisableFsRedirForFileEntry(PSetupFileEntry(Entries[seFile][I])),
+                  PathExtractPath(SourceWildcard),
+                  '', PathExtractName(SourceWildcard), IsWildcard(SourceWildcard),
+                  LExcludes, foRecurseSubDirsExternal in Options);
+              end;
             except
-              { Ignore exceptions. One notable exception we want to ignore is
-                the one about "app" not being initialized. }
+              { Ignore exceptions. Two notable exceptions we want to ignore are
+                the one about "app" not being initialized and also archive errors
+                (ESevenZipError). Also see EnumFiles. }
             end;
           end;
           if Components = '' then { no types or a file that doesn't belong to any component }
@@ -3552,8 +3633,10 @@ begin
   { Free the _isdecmp.dll and _is7z.dll handles }
   if DecompressorDLLHandle <> 0 then
     FreeLibrary(DecompressorDLLHandle);
-  if SevenZipDLLHandle <> 0 then
+  if SevenZipDLLHandle <> 0 then begin
+    SevenZipDLLDeInit;
     FreeLibrary(SevenZipDLLHandle);
+  end;
 
   { Free the shfolder.dll handle }
   UnloadSHFolderDLL;

+ 3 - 1
Projects/Src/Setup.ScriptClasses.pas

@@ -2,7 +2,7 @@ unit Setup.ScriptClasses;
 
 {
   Inno Setup
-  Copyright (C) 1997-2024 Jordan Russell
+  Copyright (C) 1997-2025 Jordan Russell
   Portions by Martijn Laan
   For conditions of distribution and use, see LICENSE.TXT.
 
@@ -328,7 +328,9 @@ begin
   with CL.Add(TDownloadWizardPage) do
   begin
     RegisterMethod(@TDownloadWizardPage.Add, 'Add');
+    RegisterMethod(@TDownloadWizardPage.AddWithISSigVerify, 'AddWithISSigVerify');
     RegisterMethod(@TDownloadWizardPage.AddEx, 'AddEx');
+    RegisterMethod(@TDownloadWizardPage.AddExWithISSigVerify, 'AddExWithISSigVerify');
     RegisterMethod(@TDownloadWizardPage.Clear, 'Clear');
     RegisterMethod(@TDownloadWizardPage.Download, 'Download');
     RegisterMethod(@TDownloadWizardPage.Show, 'Show');

+ 90 - 25
Projects/Src/Setup.ScriptDlg.pas

@@ -174,6 +174,8 @@ type
 
   TDownloadFile = class
     Url, BaseName, RequiredSHA256OfFile, UserName, Password: String;
+    ISSigVerify: Boolean;
+    ISSigAllowedKeys: AnsiString;
   end;
   TDownloadFiles = TObjectList<TDownloadFile>;
 
@@ -184,6 +186,9 @@ type
       FShowBaseNameInsteadOfUrl: Boolean;
       FAbortButton: TNewButton;
       FShowProgressControlsOnNextProgress, FAbortedByUser: Boolean;
+      function DoAdd(const Url, BaseName, RequiredSHA256OfFile: String;
+        const UserName: String = ''; const Password: String = '';
+        const ISSigVerify: Boolean = False; const ISSigAllowedKeys: AnsiString = ''): Integer;
       procedure AbortButtonClick(Sender: TObject);
       function InternalOnDownloadProgress(const Url, BaseName: string; const Progress, ProgressMax: Int64): Boolean;
       procedure ShowProgressControls(const AVisible: Boolean);
@@ -191,8 +196,12 @@ type
       constructor Create(AOwner: TComponent); override;
       destructor Destroy; override;
       procedure Initialize; override;
-      procedure Add(const Url, BaseName, RequiredSHA256OfFile: String);
-      procedure AddEx(const Url, BaseName, RequiredSHA256OfFile, UserName, Password: String);
+      function Add(const Url, BaseName, RequiredSHA256OfFile: String): Integer;
+      function AddWithISSigVerify(const Url, IssigUrl, BaseName: String;
+        const AllowedKeysRuntimeIDs: TStringList): Integer;
+      function AddEx(const Url, BaseName, RequiredSHA256OfFile, UserName, Password: String): Integer;
+      function AddExWithISSigVerify(const Url, IssigUrl, BaseName, UserName, Password: String;
+        const AllowedKeysRuntimeIDs: TStringList): Integer;
       procedure Clear;
       function Download: Int64;
       property OnDownloadProgress: TOnDownloadProgress write FOnDownloadProgress;
@@ -223,8 +232,8 @@ type
       constructor Create(AOwner: TComponent); override;
       destructor Destroy; override;
       procedure Initialize; override;
-      procedure Add(const ArchiveFileName, DestDir: String; const FullPaths: Boolean);
-      procedure AddEx(const ArchiveFileName, DestDir, Password: String; const FullPaths: Boolean);
+      function Add(const ArchiveFileName, DestDir: String; const FullPaths: Boolean): Integer;
+      function AddEx(const ArchiveFileName, DestDir, Password: String; const FullPaths: Boolean): Integer;
       procedure Clear;
       procedure Extract;
       property OnExtractionProgress: TOnExtractionProgress write FOnExtractionProgress;
@@ -235,13 +244,16 @@ type
       property ShowArchiveInsteadOfFile: Boolean read FShowArchiveInsteadOfFile write FShowArchiveInsteadOfFile;
   end;
 
+function ConvertAllowedKeysRuntimeIDsToISSigAllowedKeys(const AllowedKeysRuntimeIDs: TStringList): AnsiString;
+
 implementation
 
 uses
-  StrUtils,
-  Shared.Struct, Setup.MainFunc, Setup.SelectFolderForm, SetupLdrAndSetup.Messages,
-  Shared.SetupMessageIDs, PathFunc, Shared.CommonFunc.Vcl, Shared.CommonFunc,
-  BrowseFunc, Setup.LoggingFunc, Setup.InstFunc, Compression.SevenZipDLLDecoder;
+  StrUtils, ISSigFunc,
+  Shared.Struct, Shared.SetupTypes, Setup.MainFunc, Setup.SelectFolderForm,
+  SetupLdrAndSetup.Messages, Shared.SetupMessageIDs, PathFunc, Shared.CommonFunc.Vcl,
+  Shared.CommonFunc, BrowseFunc, Setup.LoggingFunc, Setup.InstFunc,
+  Compression.SevenZipDLLDecoder;
 
 const
   DefaultLabelHeight = 14;
@@ -1045,12 +1057,8 @@ begin
   FAbortButton.Visible := AVisible;
 end;
 
-procedure TDownloadWizardPage.Add(const Url, BaseName, RequiredSHA256OfFile: String);
-begin
-  AddEx(Url, BaseName, RequiredSHA256OfFile, '', '');
-end;
-
-procedure TDownloadWizardPage.AddEx(const Url, BaseName, RequiredSHA256OfFile, UserName, Password: String);
+function TDownloadWizardPage.DoAdd(const Url, BaseName, RequiredSHA256OfFile, UserName, Password: String;
+  const ISSigVerify: Boolean; const ISSigAllowedKeys: AnsiString): Integer;
 begin
   var F := TDownloadFile.Create;
   F.Url := Url;
@@ -1058,7 +1066,56 @@ begin
   F.RequiredSHA256OfFile := RequiredSHA256OfFile;
   F.UserName := UserName;
   F.Password := Password;
-  FFiles.Add(F);
+  F.ISSigVerify := ISSigVerify;
+  F.ISSigAllowedKeys := ISSigAllowedKeys;
+  Result := FFiles.Add(F);
+end;
+
+function ConvertAllowedKeysRuntimeIDsToISSigAllowedKeys(const AllowedKeysRuntimeIDs: TStringList): AnsiString;
+begin
+  Result := '';
+  if AllowedKeysRuntimeIDs <> nil then begin
+    for var I := 0 to AllowedKeysRuntimeIDs.Count-1 do begin
+      const RuntimeID = AllowedKeysRuntimeIDs[I];
+      if RuntimeID = '' then
+        InternalError('RuntimeID cannot be empty');
+      var Found := False;
+      for var KeyIndex := 0 to Entries[seISSigKey].Count-1 do begin
+        var ISSigKeyEntry := PSetupISSigKeyEntry(Entries[seISSigKey][KeyIndex]);
+        if SameText(ISSigKeyEntry.RuntimeID, RuntimeID) then begin
+          SetISSigAllowedKey(Result, KeyIndex);
+          Found := True;
+          Break;
+        end;
+      end;
+      if not Found then
+        InternalError(Format('Unknown RuntimeID ''%s''', [RuntimeID]));
+    end;
+  end;
+end;
+
+function TDownloadWizardPage.Add(const Url, BaseName, RequiredSHA256OfFile: String): Integer;
+begin
+  Result := DoAdd(Url, BaseName, RequiredSHA256OfFile);
+end;
+
+function TDownloadWizardPage.AddWithISSigVerify(const Url, IssigUrl, BaseName: String;
+  const AllowedKeysRuntimeIDs: TStringList): Integer;
+begin
+  Result := AddExWithISSigVerify(Url, IssigUrl, BaseName, '', '', AllowedKeysRuntimeIDs);
+end;
+
+function TDownloadWizardPage.AddEx(const Url, BaseName, RequiredSHA256OfFile, UserName, Password: String): Integer;
+begin
+  Result := DoAdd(Url, BaseName, RequiredSHA256OfFile, UserName, Password);
+end;
+
+function TDownloadWizardPage.AddExWithISSigVerify(const Url, IssigUrl, BaseName, UserName,
+  Password: String; const AllowedKeysRuntimeIDs: TStringList): Integer;
+begin
+  const ISSigAllowedKeys = ConvertAllowedKeysRuntimeIDsToISSigAllowedKeys(AllowedKeysRuntimeIDs);
+  DoAdd(IssigUrl, BaseName + ISSigExt, '', UserName, Password, False, '');
+  Result := DoAdd(Url, BaseName, '', UserName, Password, True, ISSigAllowedKeys);
 end;
 
 procedure TDownloadWizardPage.Clear;
@@ -1074,7 +1131,8 @@ begin
   for var F in FFiles do begin
     { Don't need to set DownloadTemporaryFileOrExtractArchiveProcessMessages before downloading since we already process messages ourselves }
     SetDownloadCredentials(F.UserName, F.Password);
-    Result := Result + DownloadTemporaryFile(F.Url, F.BaseName, F.RequiredSHA256OfFile, InternalOnDownloadProgress);
+    Result := Result + DownloadTemporaryFile(F.Url, F.BaseName, F.RequiredSHA256OfFile,
+      F.ISSigVerify, F.ISSigAllowedKeys, InternalOnDownloadProgress);
   end;
   SetDownloadCredentials('', '');
 end;
@@ -1166,19 +1224,19 @@ begin
   FAbortButton.Visible := AVisible;
 end;
 
-procedure TExtractionWizardPage.Add(const ArchiveFileName, DestDir: String; const FullPaths: Boolean);
+function TExtractionWizardPage.Add(const ArchiveFileName, DestDir: String; const FullPaths: Boolean): Integer;
 begin
-  AddEx(ArchiveFileName, DestDir, '', FullPaths);
+  Result := AddEx(ArchiveFileName, DestDir, '', FullPaths);
 end;
 
-procedure TExtractionWizardPage.AddEx(const ArchiveFileName, DestDir, Password: String; const FullPaths: Boolean);
+function TExtractionWizardPage.AddEx(const ArchiveFileName, DestDir, Password: String; const FullPaths: Boolean): Integer;
 begin
   const A = TArchive.Create;
   A.FileName := ArchiveFileName;
   A.DestDir := DestDir;
   A.Password := Password;
   A.FullPaths := FullPaths;
-  FArchives.Add(A);
+  Result := FArchives.Add(A);
 end;
 
 procedure TExtractionWizardPage.Clear;
@@ -1190,12 +1248,19 @@ procedure TExtractionWizardPage.Extract;
 begin
   FAbortedByUser := False;
 
-  for var A in FArchives do begin
-    { Don't need to set DownloadTemporaryFileOrExtractArchiveProcessMessages before extraction since we already process messages ourselves }
-    if SetupHeader.SevenZipLibraryName <> '' then
-      ExtractArchiveRedir(ScriptFuncDisableFsRedir, A.FileName, A.DestDir, A.Password, A.FullPaths, InternalOnExtractionProgress)
+  try
+    for var A in FArchives do begin
+      { Don't need to set DownloadTemporaryFileOrExtractArchiveProcessMessages before extraction since we already process messages ourselves }
+      if SetupHeader.SevenZipLibraryName <> '' then
+        ExtractArchiveRedir(ScriptFuncDisableFsRedir, A.FileName, A.DestDir, A.Password, A.FullPaths, InternalOnExtractionProgress)
+      else
+        Extract7ZipArchiveRedir(ScriptFuncDisableFsRedir, A.FileName, A.DestDir, A.Password, A.FullPaths, InternalOnExtractionProgress);
+    end;
+  except
+    on E: EAbort do
+      raise Exception.Create(SetupMessages[msgErrorExtractionAborted])
     else
-      Extract7ZipArchiveRedir(ScriptFuncDisableFsRedir, A.FileName, A.DestDir, A.Password, A.FullPaths, InternalOnExtractionProgress);
+      raise Exception.Create(FmtSetupMessage1(msgErrorExtractionFailed, GetExceptMessage));
   end;
 end;
 

+ 18 - 57
Projects/Src/Setup.ScriptFunc.pas

@@ -804,7 +804,7 @@ var
     end);
     RegisterScriptFunc('DownloadTemporaryFile', sfNoUninstall, procedure(const Caller: TPSExec; const OrgName: AnsiString; const Stack: TPSStack; const PStart: Cardinal)
     begin
-      Stack.SetInt64(PStart, DownloadTemporaryFile(Stack.GetString(PStart-1), Stack.GetString(PStart-2), Stack.GetString(PStart-3), TOnDownloadProgress(Stack.GetProc(PStart-4, Caller))));
+      Stack.SetInt64(PStart, DownloadTemporaryFile(Stack.GetString(PStart-1), Stack.GetString(PStart-2), Stack.GetString(PStart-3), False, '', TOnDownloadProgress(Stack.GetProc(PStart-4, Caller))));
     end);
     RegisterScriptFunc('SetDownloadCredentials', sfNoUninstall, procedure(const Caller: TPSExec; const OrgName: AnsiString; const Stack: TPSStack; const PStart: Cardinal)
     begin
@@ -1796,10 +1796,17 @@ var
         FullDirsItemNo := PStart-3;
       end;
 
-      if SetupHeader.SevenZipLibraryName <> '' then
-        ExtractArchiveRedir(ScriptFuncDisableFsRedir, Stack.GetString(PStart), Stack.GetString(PStart-1), Password, Stack.GetBool(FullDirsItemNo), TOnExtractionProgress(Stack.GetProc(FullDirsItemNo-1, Caller)))
-      else
-        Extract7ZipArchiveRedir(ScriptFuncDisableFsRedir, Stack.GetString(PStart), Stack.GetString(PStart-1), Password, Stack.GetBool(FullDirsItemNo), TOnExtractionProgress(Stack.GetProc(FullDirsItemNo-1, Caller)));
+      try
+        if SetupHeader.SevenZipLibraryName <> '' then
+          ExtractArchiveRedir(ScriptFuncDisableFsRedir, Stack.GetString(PStart), Stack.GetString(PStart-1), Password, Stack.GetBool(FullDirsItemNo), TOnExtractionProgress(Stack.GetProc(FullDirsItemNo-1, Caller)))
+        else
+          Extract7ZipArchiveRedir(ScriptFuncDisableFsRedir, Stack.GetString(PStart), Stack.GetString(PStart-1), Password, Stack.GetBool(FullDirsItemNo), TOnExtractionProgress(Stack.GetProc(FullDirsItemNo-1, Caller)));
+      except
+        on E: EAbort do
+          raise Exception.Create(SetupMessages[msgErrorExtractionAborted])
+        else
+          raise Exception.Create(FmtSetupMessage1(msgErrorExtractionFailed, GetExceptMessage));
+      end;
     end);
     RegisterScriptFunc('DEBUGGING', procedure(const Caller: TPSExec; const OrgName: AnsiString; const Stack: TPSStack; const PStart: Cardinal)
     begin
@@ -1823,66 +1830,20 @@ var
     end);
     RegisterScriptFunc('ISSigVerify', procedure(const Caller: TPSExec; const OrgName: AnsiString; const Stack: TPSStack; const PStart: Cardinal)
     begin
-      const AllowedKeysRuntimeIDs = Stack.GetStringArray(PStart-1);
+      const ISSigAllowedKeys = ConvertAllowedKeysRuntimeIDsToISSigAllowedKeys(TStringList(Stack.GetClass(PStart-1)));
       const Filename = Stack.GetString(PStart-2);
       const KeepOpen = Stack.GetBool(PStart-3);
 
-      { Import keys }
-      var ISSigAllowedKeys: AnsiString;
-      for var I := 0 to Length(AllowedKeysRuntimeIDs)-1 do begin
-        const RuntimeID = AllowedKeysRuntimeIDs[I];
-        if RuntimeID = '' then
-          InternalError('RuntimeID cannot be empty');
-        var Found := False;
-        for var KeyIndex := 0 to Entries[seISSigKey].Count-1 do begin
-          var ISSigKeyEntry := PSetupISSigKeyEntry(Entries[seISSigKey][KeyIndex]);
-          if SameText(ISSigKeyEntry.RuntimeID, RuntimeID) then begin
-            SetISSigAllowedKey(ISSigAllowedKeys, KeyIndex);
-            Found := True;
-            Break;
-          end;
-        end;
-        if not Found then
-          InternalError(Format('Unknown RuntimeID ''%s''', [RuntimeID]));
-      end;
-
-      { Verify signature }
-      var ExpectedFileSize: Int64;
-      var ExpectedFileHash: TSHA256Digest;
-      if not ISSigVerifySignature(Filename,
-        GetISSigAllowedKeys(ISSigAvailableKeys, ISSigAllowedKeys),
-        ExpectedFileSize, ExpectedFileHash,
-        procedure(const Filename: String)
-        begin
-          raise Exception.Create('File does not exist');
-        end,
-        procedure(const Filename, SigFilename: String)
-        begin
-          raise Exception.Create('Signature file does not exist');
-        end,
-        procedure(const SigFilename: String; const VerifyResult: TISSigVerifySignatureResult)
-        begin
-          case VerifyResult of
-            vsrMalformed, vsrBadSignature:
-              raise Exception.Create('Signature file is not valid');
-            vsrKeyNotFound:
-              raise Exception.Create('Incorrect key ID');
-          else
-            InternalError('Unknown verify result');
-          end;
-        end
-      ) then
-        InternalError('Unexpected ISSigVerifySignature result');
-
-      { Verify file, keeping open afterwards if requested
+      { Verify signature & file, keeping open afterwards if requested
         Also see TrustFunc's CheckFileTrust }
       var F := TFileStream.Create(Filename, fmOpenRead or fmShareDenyWrite);
       try
-        if Int64(F.Size) <> ExpectedFileSize then
-          raise Exception.Create('File size is incorrect');
+        var ExpectedFileHash: TSHA256Digest;
+        DoISSigVerify(nil, F, Filename, ISSigAllowedKeys, ExpectedFileHash);
+         { Couldn't get the SHA-256 while downloading so need to get and check it now }
         const ActualFileHash = ISSigCalcStreamHash(F);
         if not SHA256DigestsEqual(ActualFileHash, ExpectedFileHash) then
-          raise Exception.Create('File hash is incorrect');
+          ISSigVerifyError(vseFileHashIncorrect);
       except
         FreeAndNil(F);
         raise;

+ 1 - 1
Projects/Src/Shared.ScriptFunc.pas

@@ -545,7 +545,7 @@ initialization
     'function StringJoin(const Separator: String; const Values: TArrayOfString): String;',
     'function StringSplit(const S: String; const Separators: TArrayOfString; const Typ: TSplitType): TArrayOfString;',
     'function StringSplitEx(const S: String; const Separators: TArrayOfString; const Quote: Char; const Typ: TSplitType): TArrayOfString;',
-    'function ISSigVerify(const AllowedKeysRuntimeIDs: TArrayOfString; const Filename: String; const KeepOpen: Boolean): TFileStream;'
+    'function ISSigVerify(const AllowedKeysRuntimeIDs: TStringList; const Filename: String; const KeepOpen: Boolean): TFileStream;'
   ];
 
   {$IFDEF COMPIL32PROJ}

+ 11 - 1
Projects/Src/Shared.SetupMessageIDs.pas

@@ -2,7 +2,7 @@ unit Shared.SetupMessageIDs;
 
 {
   Inno Setup
-  Copyright (C) 1997-2020 Jordan Russell
+  Copyright (C) 1997-2025 Jordan Russell
   Portions by Martijn Laan
   For conditions of distribution and use, see LICENSE.TXT.
 
@@ -29,6 +29,9 @@ type
     msgAdminPrivilegesRequired,
     msgApplicationsFound,
     msgApplicationsFound2,
+    msgArchiveIncorrectPassword,
+    msgArchiveIsCorrupted,
+    msgArchiveUnsupportedFormat,
     msgBadDirName32,
     msgBadGroupName,
     msgBeveledLabel,
@@ -86,6 +89,7 @@ type
     msgErrorDownloadFailed,
     msgErrorDownloadSizeFailed,
     msgErrorExecutingProgram,
+    msgErrorExtracting,
     msgErrorExtractionAborted,
     msgErrorExtractionFailed,
     msgErrorFileHash1,
@@ -225,6 +229,7 @@ type
     msgShutdownBlockReasonUninstallingApp,
     msgSourceDoesntExist,
     msgSourceIsCorrupted,
+    msgSourceVerificationFailed,
     msgStatusClosingApplications,
     msgStatusCreateDirs,
     msgStatusCreateIcons,
@@ -264,6 +269,11 @@ type
     msgUserInfoNameRequired,
     msgUserInfoOrg,
     msgUserInfoSerial,
+    msgVerificationFileHashIncorrect,
+    msgVerificationFileSizeIncorrect,
+    msgVerificationKeyNotFound,
+    msgVerificationSignatureDoesntExist,
+    msgVerificationSignatureInvalid,
     msgWelcomeLabel1,
     msgWelcomeLabel2,
     msgWindowsServicePackRequired,

+ 5 - 2
Projects/Src/Shared.SetupTypes.pas

@@ -39,6 +39,9 @@ type
 
   TArrayOfECDSAKey = array of TECDSAKey;
 
+  TISSigVerifySignatureError = (vseSignatureMissing, vseSignatureMalformed, vseKeyNotFound,
+    vseSignatureBad, vseFileSizeIncorrect, vseFileHashIncorrect);
+
 const
   crHand = 1;
 
@@ -58,7 +61,7 @@ procedure GenerateEncryptionKey(const Password: String; const Salt: TSetupKDFSal
 procedure SetISSigAllowedKey(var ISSigAllowedKeys: AnsiString; const KeyIndex: Integer);
 function GetISSigAllowedKeys([ref] const ISSigAvailableKeys: TArrayOfECDSAKey;
   const ISSigAllowedKeys: AnsiString): TArrayOfECDSAKey;
-function IsExcluded(Text: String; const AExcludes: TStringList): Boolean;
+function IsExcluded(Text: String; const AExcludes: TStrings): Boolean;
 
 implementation
 
@@ -351,7 +354,7 @@ begin
     Result := ISSigAvailableKeys;
 end;
 
-function IsExcluded(Text: String; const AExcludes: TStringList): Boolean;
+function IsExcluded(Text: String; const AExcludes: TStrings): Boolean;
 
   function CountBackslashes(S: PChar): Integer;
   begin

+ 5 - 4
Projects/Src/Shared.Struct.pas

@@ -36,7 +36,7 @@ const
   SetupID: TSetupID = 'Inno Setup Setup Data (6.5.0)';
   UninstallLogID: array[Boolean] of TUninstallLogID =
     ('Inno Setup Uninstall Log (b)', 'Inno Setup Uninstall Log (b) 64-bit');
-  MessagesHdrID: TMessagesHdrID = 'Inno Setup Messages (6.4.0) (u)';
+  MessagesHdrID: TMessagesHdrID = 'Inno Setup Messages (6.5.0) (u)';
   MessagesLangOptionsID: TMessagesLangOptionsID = '!mlo!001';
   ZLIBID: TCompID = 'zlb'#26;
   DiskSliceID: TDiskSliceID = 'idska32'#26;
@@ -227,13 +227,14 @@ type
     PublicX, PublicY, RuntimeID: String;
   end;
 const
-  SetupFileEntryStrings = 11;
+  SetupFileEntryStrings = 12;
   SetupFileEntryAnsiStrings = 1;
 type
   PSetupFileEntry = ^TSetupFileEntry;
   TSetupFileEntry = packed record
     SourceFilename, DestName, InstallFontName, StrongAssemblyName, Components,
-    Tasks, Languages, Check, AfterInstall, BeforeInstall, Excludes: String;
+    Tasks, Languages, Check, AfterInstall, BeforeInstall, Excludes,
+    ExtractArchivePassword: String;
     ISSigAllowedKeys: AnsiString;
     MinVersion, OnlyBelowVersion: TSetupVersionData;
     LocationEntry: Integer;
@@ -250,7 +251,7 @@ type
       foRecurseSubDirsExternal, foReplaceSameVersionIfContentsDiffer,
       foDontVerifyChecksum, foUninsNoSharedFilePrompt, foCreateAllSubDirs,
       fo32Bit, fo64Bit, foExternalSizePreset, foSetNTFSCompression,
-      foUnsetNTFSCompression, foGacInstall, foISSigVerify);
+      foUnsetNTFSCompression, foGacInstall, foISSigVerify, foExtractArchive);
     FileType: (ftUserFile, ftUninstExe);
   end;
 const

+ 0 - 1
setup.iss

@@ -176,7 +176,6 @@ Source: "Examples\CodeClasses.iss"; DestDir: "{app}\Examples"; Flags: ignorevers
 Source: "Examples\CodeDlg.iss"; DestDir: "{app}\Examples"; Flags: ignoreversion touch
 Source: "Examples\CodeDll.iss"; DestDir: "{app}\Examples"; Flags: ignoreversion touch
 Source: "Examples\CodeDownloadFiles.iss"; DestDir: "{app}\Examples"; Flags: ignoreversion touch
-Source: "Examples\CodeDownloadFiles2.iss"; DestDir: "{app}\Examples"; Flags: ignoreversion touch
 Source: "Examples\CodeExample1.iss"; DestDir: "{app}\Examples"; Flags: ignoreversion touch
 Source: "Examples\CodePrepareToInstall.iss"; DestDir: "{app}\Examples"; Flags: ignoreversion touch
 Source: "Examples\Components.iss"; DestDir: "{app}\Examples"; Flags: ignoreversion touch

+ 64 - 21
whatsnew.htm

@@ -39,6 +39,49 @@ For conditions of distribution and use, see <a href="files/is/license.txt">LICEN
 <p><b>Want to be notified by e-mail of new Inno Setup releases?</b> <a href="ismail.php">Subscribe</a> to the Inno Setup Mailing List!</p>
 
 <p><a name="6.5.0"></a><span class="ver">6.5.0-dev </span><span class="date">(?)</span></p>
+<span class="head2">Improved archive extraction</span>
+<p>Support for extraction of downloaded archives has been improved. It's now possible to enable support for password-protected archives and for multiple archive formats other than .7z such as .zip. This can also be used to lower the memory requirements for extraction of .7z archives that contain large files. Additionally the <tt>[Files]</tt> section now supports archive extraction.</p>
+<p>All of this is optional and does <i>not</i> increase the size of Setup if not used.</p>
+<ul>
+  <li>Updated <tt>[Setup]</tt> and <tt>[Files]</tt> section:
+  <ul>
+    <li>Added new <tt>[Setup]</tt> section directive <tt>ArchiveExtraction</tt> to specify the method of archive extraction used by new <tt>[Files]</tt> section flag <tt>extractarchive</tt> (see below) and support functions <tt>ExtractArchive</tt> and <tt>CreateExtractionPage</tt>:
+      <ul>
+        <li><tt>basic</tt> (default) is the method introduced by Inno Setup 6.4.0. It only supports .7z archives that are not password-protected.</li>
+        <li><tt>enhanced/nopassword</tt> is a new method and internally uses 7zxr.dll from the 7-Zip source code by Igor Pavlov, as-is, except that it was recompiled, code-signed, and renamed to is7zxr.dll. Compared to <tt>basic</tt>, it has lower memory requirements for archives that contain large files but increases the size of the Setup file(s). It still only supports .7z archives that are not password-protected.</li>
+        <li><tt>enhanced</tt> uses 7zxa.dll instead of 7zxr.dll, recompiled, code-signed, and renamed to is7zxa.dll. It still only supports .7z archives, but they may be password-protected.</li>
+        <li><tt>full</tt> uses 7z.dll instead of 7zxa.dll, recompiled, code-signed, and renamed to is7z.dll. It supports multiple archive formats (.7z, .zip, .rar, and more), although not as many as the original 7z.dll, to reduce its size.</li>
+      </ul>
+      New documentation topic <a href="https://jrsoftware.org/ishelp/index.php?topic=setup_archiveextraction">ArchiveExtraction</a> has a table summarizing the differences between these methods.
+    </li>
+    <li>Added new <tt>[Files]</tt> section flag <tt>extractarchive</tt> and parameter <tt>ExtractArchivePassword</tt>, intended to enable the seamless integration and extraction of downloaded archives.
+      <ul>
+        <li>The supported archive formats, beyond .7z, and the support for password-protected archives, depend on the aforementioned <tt>ArchiveExtraction</tt> directive, that must not be set to <tt>basic</tt>.</li>
+        <li>Flag <tt>extractarchive</tt> must be combined with the <tt>external</tt> and <tt>ignoreversion</tt> flags. It is usually also combined with the <tt>recursesubdirs</tt> and <tt>createallsubdirs</tt> flags.</li>
+        <li>Using a solid archive is not recommended; extraction performance may degrade depending on the solid block size.</li>
+        <li>Archive extraction otherwise behaves the same as external file copying. For example, it supports automatic uninstallation of extracted files and can be combined with same other flags and parameters.</li>
+        <li>Example script:
+          <pre>[Setup]
+ArchiveExtraction=enhanced/nopassword
+
+[Files]
+Source: "{tmp}\MyProg-ExtraReadmes.7z"; DestDir: "{app}"; \
+  Flags: external extractarchive recursesubdirs createallsubdirs ignoreversion</pre>
+        </li>
+      </ul>
+    </li>
+    <li>Updated example script <i>CodeDownloadFiles.iss</i> to demonstrate how to use a single <tt>[Files]</tt> entry to extract a downloaded archive.</li>
+    <li>Archive extraction now honors the file system redirection state set by 64-bit install mode, entry flags, and support function <tt>EnableFsRedirection</tt>.</li>
+  </ul>
+  </li>
+  <li>Updated Pascal Scripting:
+    <ul>
+      <li>New support function <tt>ExtractArchive</tt> replaces the deprecated <tt>Extract7ZipArchive</tt>. <tt>ExtractArchive</tt> includes an additional parameter to optionally specify a password.</li>
+      <li><tt>ExtractArchive</tt> and <tt>CreateExtractionPage</tt> now overwrite read-only files which already exist in the destination directory without prompting the user. Previously this would cause an extraction error.</li>
+      <li>Added new <tt>AddEx</tt> function to support class <tt>TExtractionWizardPage</tt> to add password-protected archives.</li>
+    </ul>
+  </li>
+</ul>
 <span class="head2">New signature-verification capability</span>
 <p>Inno Setup now includes an integrated signature-verification capability that can be used to detect corruption or tampering in your files at compile time, before files are included in an installer being built, or during installation, before Setup copies external files onto a user's system.</p>
 <p>Any type of file may be signed and verified, and creation of signatures does <i>not</i> require a certificate from a certificate authority. There is no cost involved.</p>
@@ -67,7 +110,8 @@ Name: bosskey; KeyFile: "boss.ispublickey"</pre>
     <li>Added a new <tt>issigverify</tt> flag for enforcing cryptographic signature verification of source files using a key from the <tt>[ISSigKeys]</tt> section, enhancing security during both compilation and installation.</li>
     <li>When used without the <tt>external</tt> flag, verification is performed during compilation, aborting if it fails. When used with the <tt>external</tt> flag, verification occurs during installation, ensuring the integrity of files as they are copied.</li>
     <li>Requires an <tt>.issig</tt> signature file to be present in the same directory as the source file. Signature files are human-readable files and can be created using the Inno Setup Signature Tool.</li>
-    <li>Has little performance impact since verification occurs while source files are being compressed/copied; the only extra I/O comes from reading the tiny <tt>.issig</tt> files. This approach also ensures there is no Time-Of-Check to Time-Of-Use (TOCTOU) problem.</li>
+    <li>Has little performance impact since verification occurs while source files are being compressed/copied and each file's contents are only read once; the only extra I/O comes from reading the tiny <tt>.issig</tt> files. Only archives and downloaded files are read a second time.</li>
+    <li>The verification process is protected against the Time-Of-Check to Time-Of-Use (TOCTOU) problem.</li>
     <li>Can be used to verify downloaded files, offering flexibility over SHA-256 checks as script changes aren't needed for file updates. See the updated <i>CodeDownloadFiles.iss</i> example script for an example.</li>
     <li>Added a new and optional <tt>ISSigAllowedKeys</tt> parameter to restrict which keys or groups of keys from the <tt>[ISSigKeys]</tt> section are permitted for signature verification using the <tt>issigverify</tt> flag.</li>
     <li>Note: The <tt>issigverify</tt> flag cannot be combined with the <tt>sign</tt> or <tt>signonce</tt> flags. Use <tt>signcheck</tt> instead.</li>
@@ -105,29 +149,15 @@ issigtool --key-file="MyKey.ispublickey" verify "MyProg.dll"</pre>
   <li>Other related changes:
   <ul>
     <li>The compiler now verifies that precompiled files like <i>SetupLdr.e32</i> and <i>Setup.e32</i> remain unchanged before using them. Can be disabled using new [Setup] section directive <tt>VerifyPrecompiledFiles</tt>.</li>
-    <li>Pascal Scripting: Added new <tt>ISSigVerify</tt> support function.</li>
+    <li>Pascal Scripting:
+      <ul>
+        <li>Added new <tt>ISSigVerify</tt> support function.</li>
+        <li>Added new <tt>AddWithISSigVerify</tt> and <tt>AddExWithISSigVerify</tt> functions to support class <tt>TDownloadWizardPage</tt>. See updated example script <i>CodeDownloadFiles.iss</i> for an example.</li>
+      </ul>
+    </li>
   </ul>
   </li>
 </ul>
-<span class="head2">Improved archive extraction</span>
-<p>Support for extraction of downloaded archives has been improved. It's now possible to enable support for password-protected archives and for multiple archive formats other than .7z such as .zip. This can also be used to lower the memory requirements for extraction of .7z archives that contain large files.</p>
-<p>All of this is optional and does <i>not</i> increase the size of Setup if not used.</p>
-<ul>
-  <li>Added new <tt>[Setup]</tt> section directive <tt>ArchiveExtraction</tt> to specify the method of archive extraction used by support functions <tt>ExtractArchive</tt> and <tt>CreateExtractionPage</tt>:
-    <ul>
-    <li><tt>basic</tt> (default) is the method introduced by Inno Setup 6.4.0. It only supports .7z archives that are not password-protected.</li>
-    <li><tt>enhanced/nopassword</tt> is a new method and internally uses 7zxr.dll from the 7-Zip source code by Igor Pavlov, as-is, except that it was recompiled, code-signed, and renamed to is7zxr.dll. Compared to <tt>basic</tt>, it has lower memory requirements for archives that contain large files but increases the size of the Setup file(s). It still only supports .7z archives that are not password-protected.</li>
-    <li><tt>enhanced</tt> uses 7zxa.dll instead of 7zxr.dll, recompiled, code-signed, and renamed to is7zxa.dll. It still only supports .7z archives, but they may be password-protected.</li>
-    <li><tt>full</tt> uses 7z.dll instead of 7zxa.dll, recompiled, code-signed, and renamed to is7z.dll. It supports multiple archive formats (.7z, .zip, .rar, and more), although not as many as the original 7z.dll, to reduce its size.</li>
-    </ul>
-    New documentation <a href="https://jrsoftware.org/ishelp/index.php?topic=setup_archiveextraction">ArchiveExtraction</a> topic has a table summarizing the differences between these methods.
-  </li>
-  <li>Added example script <i>CodeDownloadFiles2.iss</i> to demonstrate how to use the <tt>CreateExtractionPage</tt> support function to verify and extract a downloaded archive.</li>
-  <li>New support function <tt>ExtractArchive</tt> replaces the deprecated <tt>Extract7ZipArchive</tt>. <tt>ExtractArchive</tt> includes an additional parameter to optionally specify a password.</li>
-  <li>Archive extraction now overwrites read-only files which already exist in the destination directory without prompting the user. Previously this would cause an extraction error.</li>
-  <li>Archive extraction now honors the file system redirection state set by 64-bit install mode and support function <tt>EnableFsRedirection</tt>.</li>
-  <li>Added new <tt>AddEx</tt> function to support class <tt>TExtractionWizardPage</tt> to add password-protected archives.</li>
-</ul>
 <span class="head2">Other changes</span>
 <ul>
   <li>Compiler IDE: the <i>Find in Files</i> result list will now update its line numbers when you add or delete lines.</li>
@@ -146,6 +176,19 @@ issigtool --key-file="MyKey.ispublickey" verify "MyProg.dll"</pre>
   <li>Minor tweaks.</li>
 </ul>
 
+<p>Some messages have been added and changed in this version: (<a href="https://github.com/jrsoftware/issrc/commit/0445cba192ed530ff5a701f35492ba1ecdabdeb8">View differences in Default.isl</a>.)</p>
+<ul>
+  <li><b>New messages:</b>
+  <ul>
+    <li>ArchiveIncorrectPassword, ArchiveIsCorrupted, ArchiveUnsupportedFormat.</li>
+    <li>SourceVerificationFailed, VerificationSignatureDoesntExist, VerificationSignatureInvalid, VerificationKeyNotFound, VerificationFileSizeIncorrect, VerificationFileHashIncorrect.</li>
+    <li>ErrorExtracting.</li>
+  </ul>
+  </li>
+</ul>
+
+<p>Note: Not all official translations have been updated for these changes yet.</li>
+
 <p><a href="files/is6.4-whatsnew.htm">Inno Setup 6.4 Revision History</a></p>
 
 </body>

Some files were not shown because too many files changed in this diff