From 67d95c40e742ca68340d44e34b1a56e8e8795828 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 5 May 2026 01:13:19 +0200 Subject: [PATCH 01/11] loader: support setup loader offset table revision 2 Inno Setup 6.5.0 bumped SetupLdrOffsetTableVersion from 1 to 2 and widened the on-disk TSetupLdrOffsetTable struct: TotalSize, OffsetEXE, Offset0 and Offset1 each grew from 32-bit LongWord to Int64, and a new ReservedPadding UInt32 was inserted before TableCRC. The record also lost its `packed` qualifier, but with 32-bit Delphi default alignment (Int64 is 4-byte aligned in 32-bit Delphi) the on-disk layout remains contiguous, growing from 48 to 64 bytes total. See Projects/Src/Shared.Struct.pas at tag is-6_5_0 in https://github.com/jrsoftware/issrc. Without this, every installer produced by Inno Setup 6.5.0 or newer fails to load with: Warning: Unexpected setup loader revision: 2 Warning: Setup loader checksum mismatch! Could not determine setup data version! Detect revision 2 specifically and parse the new layout. The fields that innoextract actually consumes (exe_offset, header_offset, data_offset) are still stored as 32-bit in the offsets struct, so truncate the read 64-bit values to 32-bit and emit a warning if any of them exceed 4 GiB. Real installers that large are rare in the wild but possible, and widening the struct is left for a follow-up. Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- src/loader/offsets.cpp | 48 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/loader/offsets.cpp b/src/loader/offsets.cpp index 0c0d6a44..80bcdc4b 100644 --- a/src/loader/offsets.cpp +++ b/src/loader/offsets.cpp @@ -148,6 +148,54 @@ bool offsets::load_offsets_at(std::istream & is, boost::uint32_t pos) { is.clear(); debug("could not read loader header revision"); return false; + } else if(revision == 2) { + /* + * Inno Setup 6.5.0 introduced a wider TSetupLdrOffsetTable layout + * (revision 2). The fields previously stored as 32-bit LongWord + * are now Int64, allowing setup binaries larger than 4 GiB. A new + * ReservedPadding UInt32 sits between Offset1 and TableCRC. The + * record itself is no longer Delphi-`packed`, but with 32-bit + * Delphi default alignment the on-disk layout remains contiguous + * (Int64 has 4-byte alignment in 32-bit mode), so the total size + * goes from 48 to 64 bytes. See Projects/Src/Shared.Struct.pas in + * issrc. + */ + (void)checksum.load(is); // TotalSize + boost::int64_t offset_exe = checksum.load(is); + boost::uint32_t uncompressed_exe = checksum.load(is); + boost::uint32_t crc_exe = checksum.load(is); + boost::int64_t offset0 = checksum.load(is); + boost::int64_t offset1 = checksum.load(is); + (void)checksum.load(is); // ReservedPadding + if(is.fail()) { + is.clear(); + debug("could not read loader header (revision 2)"); + return false; + } + boost::int64_t max_uint32 = boost::int64_t(std::numeric_limits::max()); + if(offset_exe < 0 || offset_exe > max_uint32 || + offset0 < 0 || offset0 > max_uint32 || + offset1 < 0 || offset1 > max_uint32) { + log_warning << "Loader header offsets exceed 4 GiB; truncating to 32-bit"; + } + exe_offset = boost::uint32_t(offset_exe); + exe_compressed_size = 0; + exe_uncompressed_size = uncompressed_exe; + exe_checksum.type = crypto::CRC32; + exe_checksum.crc32 = crc_exe; + message_offset = 0; + header_offset = boost::uint32_t(offset0); + data_offset = boost::uint32_t(offset1); + boost::uint32_t expected = util::load(is); + if(is.fail()) { + is.clear(); + debug("could not read loader header checksum (revision 2)"); + return false; + } + if(checksum.finalize() != expected) { + log_warning << "Setup loader checksum mismatch!"; + } + return true; } else if(revision != 1) { log_warning << "Unexpected setup loader revision: " << revision; } From 9f3cee2dd8e1c105d0bd280a3bac331ea3a0417d Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 5 May 2026 01:24:06 +0200 Subject: [PATCH 02/11] Add support for Inno Setup 6.4.2 and 6.4.3 Inno Setup 6.4.2 added the new [Setup] section directive CloseApplicationsFilterExcludes (see https://jrsoftware.org/files/is6.4-whatsnew.htm). It is serialised as a binary string immediately after architectures_installed_in_64bit_mode_expr in TSetupHeader (Projects/Src/Shared.Struct.pas in https://github.com/jrsoftware/issrc at tag is-6_4_2). Read it as a new header field for installers from 6.4.2 onward; clear it for older installers. 6.4.2 also bumped the SetupID magic to 'Inno Setup Setup Data (6.4.2)'; 6.4.3 to 'Inno Setup Setup Data (6.4.3)'. Both versions are otherwise on-disk-compatible with the post-6.4.2 layout, so add them to the known versions table without further parsing changes. This change is sourced from the prior-art PR #202 by itai-delphos (https://github.com/dscharrer/innoextract/pull/202), which I am picking up and extending to cover Inno Setup 6.5.x, 6.6.x and 6.7.x in the follow-up commits in this series. Helped-by: itai-delphos@github Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- src/setup/header.cpp | 6 ++++++ src/setup/header.hpp | 1 + src/setup/version.cpp | 2 ++ 3 files changed, 9 insertions(+) diff --git a/src/setup/header.cpp b/src/setup/header.cpp index bbe9df9c..6ed61cce 100644 --- a/src/setup/header.cpp +++ b/src/setup/header.cpp @@ -266,6 +266,11 @@ void header::load(std::istream & is, const version & version) { is >> util::binary_string(architectures_allowed_expr); is >> util::binary_string(architectures_installed_in_64bit_mode_expr); } + if(version >= INNO_VERSION(6, 4, 2)) { + is >> util::binary_string(close_applications_filter_excludes); + } else { + close_applications_filter_excludes.clear(); + } if(version >= INNO_VERSION(5, 2, 5)) { is >> util::ansi_string(license_text); is >> util::ansi_string(info_before); @@ -762,6 +767,7 @@ void header::decode(util::codepage_id codepage) { util::to_utf8(create_uninstall_registry_key, codepage, &lead_bytes); util::to_utf8(uninstallable, codepage); util::to_utf8(close_applications_filter, codepage); + util::to_utf8(close_applications_filter_excludes, codepage); util::to_utf8(setup_mutex, codepage, &lead_bytes); util::to_utf8(changes_environment, codepage); util::to_utf8(changes_associations, codepage); diff --git a/src/setup/header.hpp b/src/setup/header.hpp index 78e5b64a..08d492c6 100644 --- a/src/setup/header.hpp +++ b/src/setup/header.hpp @@ -167,6 +167,7 @@ struct header { std::string changes_associations; std::string architectures_allowed_expr; std::string architectures_installed_in_64bit_mode_expr; + std::string close_applications_filter_excludes; std::string license_text; std::string info_before; std::string info_after; diff --git a/src/setup/version.cpp b/src/setup/version.cpp index 7f68af26..be6682b5 100644 --- a/src/setup/version.cpp +++ b/src/setup/version.cpp @@ -186,6 +186,8 @@ const known_version versions[] = { { "Inno Setup Setup Data (6.3.0)", INNO_VERSION_EXT(6, 3, 0, 0), version::Unicode }, { "Inno Setup Setup Data (6.4.0)", /* prerelease */ INNO_VERSION_EXT(6, 4, 0, 0), version::Unicode }, { "Inno Setup Setup Data (6.4.0.1)", /* 6.4.0 */ INNO_VERSION_EXT(6, 4, 0, 1), version::Unicode }, + { "Inno Setup Setup Data (6.4.2)", INNO_VERSION_EXT(6, 4, 2, 0), version::Unicode }, + { "Inno Setup Setup Data (6.4.3)", INNO_VERSION_EXT(6, 4, 3, 0), version::Unicode }, }; } // anonymous namespace From 6c584226b9adc9decd64f621d36d267b7bc5026d Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 5 May 2026 01:44:12 +0200 Subject: [PATCH 03/11] Add support for Inno Setup 6.5.0, 6.5.1, 6.5.2, 6.5.3 and 6.5.4 Inno Setup 6.5.0 made two on-disk changes to TSetupHeader: - A new SevenZipLibraryName: String, serialised right after CloseApplicationsFilterExcludes (Projects/Src/Shared.Struct.pas at tag is-6_5_0 in https://github.com/jrsoftware/issrc). - A new NumISSigKeyEntries: Integer entry-count field, slotted between NumDirEntries and NumFileEntries. ISSig (Inno Setup Signature) key entries are part of the new ECDSA P-256 ISSigTool technology preview added in 6.4.3 and turned into a serialised table in 6.5.0. innoextract does not yet process the entries themselves; for now consume the count so that NumFileEntries and every entry count after it remains aligned. 6.5.0 also bumped the SetupLdrOffsetTableVersion from 1 to 2 and widened the offset table; that part is handled in the previous "loader: support setup loader offset table revision 2" commit. Add SetupID magic strings: - 6.5.0 covers 6.5.0 and 6.5.1, which share `Inno Setup Setup Data (6.5.0)`. - 6.5.2 covers 6.5.2, 6.5.3 and 6.5.4, which share `Inno Setup Setup Data (6.5.2)`. The header changes (new wizard image background colours and dynamic-dark-mode variants) are added in a separate follow-up commit since they are not in the same on-disk position as the older WizardImageBackColor field, so they require a different branch in `header::load`. This change continues the prior-art line started by PR #202 (https://github.com/dscharrer/innoextract/pull/202) by itai-delphos. Helped-by: itai-delphos@github Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- src/setup/header.cpp | 14 ++++++++++++++ src/setup/header.hpp | 1 + src/setup/version.cpp | 2 ++ 3 files changed, 17 insertions(+) diff --git a/src/setup/header.cpp b/src/setup/header.cpp index 6ed61cce..65c6603d 100644 --- a/src/setup/header.cpp +++ b/src/setup/header.cpp @@ -271,6 +271,11 @@ void header::load(std::istream & is, const version & version) { } else { close_applications_filter_excludes.clear(); } + if(version >= INNO_VERSION(6, 5, 0)) { + is >> util::binary_string(seven_zip_library_name); + } else { + seven_zip_library_name.clear(); + } if(version >= INNO_VERSION(5, 2, 5)) { is >> util::ansi_string(license_text); is >> util::ansi_string(info_before); @@ -324,6 +329,14 @@ void header::load(std::istream & is, const version & version) { } directory_count = util::load(is, version.bits()); + if(version >= INNO_VERSION(6, 5, 0)) { + // Inno Setup 6.5.0 inserted a NumISSigKeyEntries: Integer field + // between NumDirEntries and NumFileEntries + // (Projects/Src/Shared.Struct.pas at tag is-6_5_0). innoextract + // does not yet use the count, but the bytes must be consumed to + // keep the stream position correct. + (void)util::load(is, version.bits()); + } file_count = util::load(is, version.bits()); data_entry_count = util::load(is, version.bits()); icon_count = util::load(is, version.bits()); @@ -768,6 +781,7 @@ void header::decode(util::codepage_id codepage) { util::to_utf8(uninstallable, codepage); util::to_utf8(close_applications_filter, codepage); util::to_utf8(close_applications_filter_excludes, codepage); + util::to_utf8(seven_zip_library_name, codepage); util::to_utf8(setup_mutex, codepage, &lead_bytes); util::to_utf8(changes_environment, codepage); util::to_utf8(changes_associations, codepage); diff --git a/src/setup/header.hpp b/src/setup/header.hpp index 08d492c6..fa47892a 100644 --- a/src/setup/header.hpp +++ b/src/setup/header.hpp @@ -168,6 +168,7 @@ struct header { std::string architectures_allowed_expr; std::string architectures_installed_in_64bit_mode_expr; std::string close_applications_filter_excludes; + std::string seven_zip_library_name; std::string license_text; std::string info_before; std::string info_after; diff --git a/src/setup/version.cpp b/src/setup/version.cpp index be6682b5..29113963 100644 --- a/src/setup/version.cpp +++ b/src/setup/version.cpp @@ -188,6 +188,8 @@ const known_version versions[] = { { "Inno Setup Setup Data (6.4.0.1)", /* 6.4.0 */ INNO_VERSION_EXT(6, 4, 0, 1), version::Unicode }, { "Inno Setup Setup Data (6.4.2)", INNO_VERSION_EXT(6, 4, 2, 0), version::Unicode }, { "Inno Setup Setup Data (6.4.3)", INNO_VERSION_EXT(6, 4, 3, 0), version::Unicode }, + { "Inno Setup Setup Data (6.5.0)", INNO_VERSION_EXT(6, 5, 0, 0), version::Unicode }, + { "Inno Setup Setup Data (6.5.2)", INNO_VERSION_EXT(6, 5, 2, 0), version::Unicode }, }; } // anonymous namespace From 65c61e140738ffebf45083a10b6a2cb835f130b2 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 5 May 2026 01:49:32 +0200 Subject: [PATCH 04/11] Add support for Inno Setup 6.6.0 and 6.6.1 Inno Setup 6.6.0 made several rearrangements in TSetupHeader's wizard section (Projects/Src/Shared.Struct.pas at tag is-6_6_0 in https://github.com/jrsoftware/issrc): - Removed WizardStyle. The classic/modern toggle was always going to end up as ModernStyle for any installer reaching modern Inno Setup versions, so 6.6.0 dropped the field on disk entirely. - Added WizardDarkStyle: TSetupWizardDarkStyle (light / dark / dynamic), serialised AFTER the WizardSizePercentX/Y pair instead of where WizardStyle used to sit. - Added WizardImageBackColorDynamicDark and WizardSmallImageBackColorDynamicDark, the dark-mode counterparts of the colours added back in 6.5.2. 6.6.1 then added one more field at the same position: - WizardImageOpacity: Byte. Defaults to 0xff (fully opaque) for older installers. In innoextract these changes are not purely additive: the wizard read block has to take a 6.6.0+ branch that omits the WizardStyle byte, and the new colour and opacity fields land between the old WizardImageAlphaFormat and the rest of the wizard scalars. Add a stored_dark_style enum mapping (analogous to stored_setup_style), declare it as a NAMED_ENUM and provide its NAMES so that stored_enum<>::get()'s warning path can reference its name at compile time, and add new header fields image_back_color_dynamic_dark, small_image_back_color_dynamic_dark and wizard_image_opacity. TSetupHeaderOption itself was reshuffled at the same time. The shWizardResizable bit was removed (the new modern wizard is always resizable, so the flag became redundant) and four new wizard-styling bits were appended at the same position: shWizardModern, shWizardBorderStyled, shWizardKeepAspectRatio and shWizardLightButtonsUnstyled. Mirror this in header::flags by adding the four new enum values, gating the existing WizardResizable add to ``< 6.6.0`` and adding the four new flags for ``>= 6.6.0`` so that the on-disk bit positions stay aligned. Provide NAMES strings for the new flags. shWizardLightButtonsUnstyled is later dropped again in 6.7.0; that gate is added in the 6.7.0 patch so that this commit remains self-contained for installers compiled with 6.6.0 or 6.6.1. Add the SetupID magic strings 'Inno Setup Setup Data (6.6.0)' and 'Inno Setup Setup Data (6.6.1)'. (Each 6.6.x point release carries a distinct magic, unlike 6.5.0/.1 and 6.5.2/.3/.4.) Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- src/setup/header.cpp | 68 ++++++++++++++++++++++++++++++++++++++++++- src/setup/header.hpp | 14 +++++++++ src/setup/version.cpp | 2 ++ 3 files changed, 83 insertions(+), 1 deletion(-) diff --git a/src/setup/header.cpp b/src/setup/header.cpp index 65c6603d..87b30b20 100644 --- a/src/setup/header.cpp +++ b/src/setup/header.cpp @@ -56,6 +56,12 @@ STORED_ENUM_MAP(stored_setup_style, header::ClassicStyle, header::ModernStyle ); +STORED_ENUM_MAP(stored_dark_style, header::LightStyle, + header::LightStyle, + header::DarkStyle, + header::DynamicStyle +); + STORED_ENUM_MAP(stored_bool_auto_no_yes, header::Auto, header::Auto, header::No, @@ -379,14 +385,25 @@ void header::load(std::istream & is, const version & version) { small_image_back_color = 0; } - if(version >= INNO_VERSION(6, 0, 0)) { + if(version >= INNO_VERSION(6, 6, 0)) { + // Inno Setup 6.6.0 removed WizardStyle (the classic/modern toggle is + // gone) and added WizardDarkStyle (light/dark/dynamic) in its place, + // after the WizardSizePercentX/Y pair instead of before it. See + // TSetupHeader in Projects/Src/Shared.Struct.pas at tag is-6_6_0. + wizard_style = ModernStyle; + wizard_resize_percent_x = util::load(is); + wizard_resize_percent_y = util::load(is); + wizard_dark_style = stored_enum(is).get(); + } else if(version >= INNO_VERSION(6, 0, 0)) { wizard_style = stored_enum(is).get(); wizard_resize_percent_x = util::load(is); wizard_resize_percent_y = util::load(is); + wizard_dark_style = LightStyle; } else { wizard_style = ClassicStyle; wizard_resize_percent_x = 0; wizard_resize_percent_y = 0; + wizard_dark_style = LightStyle; } if(version >= INNO_VERSION(5, 5, 7)) { @@ -395,6 +412,30 @@ void header::load(std::istream & is, const version & version) { image_alpha_format = AlphaIgnored; } + if(version >= INNO_VERSION(6, 5, 2)) { + // Inno Setup 6.5.2 re-introduced WizardImageBackColor and + // WizardSmallImageBackColor as serialised header fields after + // WizardImageAlphaFormat; before 5.5.7 they sat much earlier + // in the struct (handled above). + image_back_color = util::load(is); + small_image_back_color = util::load(is); + } + if(version >= INNO_VERSION(6, 6, 0)) { + // 6.6.0 added the DynamicDark variants. + image_back_color_dynamic_dark = util::load(is); + small_image_back_color_dynamic_dark = util::load(is); + } else { + image_back_color_dynamic_dark = 0; + small_image_back_color_dynamic_dark = 0; + } + if(version >= INNO_VERSION(6, 6, 1)) { + // 6.6.1 added WizardImageOpacity (Byte). Default for older versions + // is fully opaque. + wizard_image_opacity = util::load(is); + } else { + wizard_image_opacity = 0xff; + } + if(version >= INNO_VERSION(6, 4, 0)) { is.read(password.sha256, 4); password.type = crypto::PBKDF2_SHA256_XChaCha20; @@ -742,11 +783,26 @@ header::flags header::load_flags(std::istream & is, const version & version) { if(version >= INNO_VERSION(6, 0, 0)) { flagreader.add(AppNameHasConsts); flagreader.add(UsePreviousPrivileges); + } + if(version >= INNO_VERSION(6, 0, 0) && version < INNO_VERSION(6, 6, 0)) { + // Inno Setup 6.6.0 dropped shWizardResizable from + // TSetupHeaderOption (the wizard is unconditionally resizable in + // the new modern style; the four flags appended at the same + // position in the enum take its slot). flagreader.add(WizardResizable); } if(version >= INNO_VERSION(6, 3, 0)) { flagreader.add(UninstallLogging); } + if(version >= INNO_VERSION(6, 6, 0)) { + // Inno Setup 6.6.0 added four new wizard-styling flags after + // shUninstallLogging. WizardLightButtonsUnstyled is later + // dropped again in 6.7.0 (handled in the 6.7.0 patch). + flagreader.add(WizardModern); + flagreader.add(WizardBorderStyled); + flagreader.add(WizardKeepAspectRatio); + flagreader.add(WizardLightButtonsUnstyled); + } return flagreader.finalize(); } @@ -843,6 +899,10 @@ NAMES(setup::header::flags, "Setup Option", "use_previous_privileges", "wizard_resizable", "uninstall_logging", + "wizard modern", + "wizard border styled", + "wizard keep aspect ratio", + "wizard light buttons unstyled", "uninstallable", "disable dir page", "disable program group page", @@ -895,6 +955,12 @@ NAMES(setup::header::style, "Style", "modern", ) +NAMES(setup::header::dark_style, "Dark Style", + "light", + "dark", + "dynamic", +) + NAMES(setup::header::auto_bool, "Auto Boolean", "auto", "no", diff --git a/src/setup/header.hpp b/src/setup/header.hpp index fa47892a..77fa651c 100644 --- a/src/setup/header.hpp +++ b/src/setup/header.hpp @@ -102,6 +102,10 @@ struct header { UsePreviousPrivileges, WizardResizable, UninstallLogging, + WizardModern, + WizardBorderStyled, + WizardKeepAspectRatio, + WizardLightButtonsUnstyled, // Obsolete flags Uninstallable, @@ -201,12 +205,21 @@ struct header { Color back_color2; Color image_back_color; Color small_image_back_color; + Color image_back_color_dynamic_dark; + Color small_image_back_color_dynamic_dark; + boost::uint8_t wizard_image_opacity; enum style { ClassicStyle, ModernStyle }; style wizard_style; + enum dark_style { + LightStyle, + DarkStyle, + DynamicStyle + }; + dark_style wizard_dark_style; boost::uint32_t wizard_resize_percent_x; boost::uint32_t wizard_resize_percent_y; @@ -300,6 +313,7 @@ NAMED_ENUM(setup::header::alpha_format) NAMED_ENUM(setup::header::install_verbosity) NAMED_ENUM(setup::header::log_mode) NAMED_ENUM(setup::header::style) +NAMED_ENUM(setup::header::dark_style) NAMED_ENUM(setup::header::auto_bool) NAMED_ENUM(setup::header::privilege_level) NAMED_ENUM(setup::header::language_detection_method) diff --git a/src/setup/version.cpp b/src/setup/version.cpp index 29113963..4aadc569 100644 --- a/src/setup/version.cpp +++ b/src/setup/version.cpp @@ -190,6 +190,8 @@ const known_version versions[] = { { "Inno Setup Setup Data (6.4.3)", INNO_VERSION_EXT(6, 4, 3, 0), version::Unicode }, { "Inno Setup Setup Data (6.5.0)", INNO_VERSION_EXT(6, 5, 0, 0), version::Unicode }, { "Inno Setup Setup Data (6.5.2)", INNO_VERSION_EXT(6, 5, 2, 0), version::Unicode }, + { "Inno Setup Setup Data (6.6.0)", INNO_VERSION_EXT(6, 6, 0, 0), version::Unicode }, + { "Inno Setup Setup Data (6.6.1)", INNO_VERSION_EXT(6, 6, 1, 0), version::Unicode }, }; } // anonymous namespace From 37c2d6eed1fe0041fdd270f005022d4a10cd2437 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 5 May 2026 12:42:56 +0200 Subject: [PATCH 05/11] refactor: widen seekable offset and size types to 64-bit Inno Setup 6.7.0 widens several on-disk size and offset fields from 32 to 64 bits (TSetupLdrOffsetTable.OffsetEXE/Offset0/Offset1, the block-header stored_size, and the new "idskb32" disk-slice header), to allow setup binaries larger than 4 GiB. The current "loader: support setup loader offset table revision 2" code path already reads the widened TSetupLdrOffsetTable fields as Int64, but truncates them back to 32 bits with a warning when storing into `loader::offsets` because the struct still holds them in `boost::uint32_t`. Widen the affected types now, ahead of consuming the new on-disk formats, so that the follow-up commit can drop the truncations and read the wider on-disk fields natively without churn in the call sites. The fields touched are: - `loader::offsets::{exe_offset, exe_compressed_size, exe_uncompressed_size, message_offset, header_offset, data_offset}` and the matching argument of `load_offsets_at`. - `stream::chunk::{sort_offset, offset}` (chunk byte offsets within a slice; `size` was already 64-bit). - `stream::slice_reader::{data_offset, slice_size}`, the matching argument of the embedded-data constructor, and the offset parameter of `seek(slice, offset)`. The local `read_pos`/ `remaining` in `slice_reader::read` follow `slice_size`. - `cli/extract.cpp`'s local `sort_offset` mirror of `chunk::sort_offset`. The on-disk reads themselves are unchanged: every site still pulls in the same number of bytes as before, just into wider locals or members, and the existing `boost::uint32_t(...)` truncation in the loader-v2 path remains in place so behaviour is bit-identical for installers up to 4 GiB. Where `streampos` / `seekg(off_t)` interactions now produce unsigned-to-signed warnings on the wider integers, wrap with `std::streamoff(...)` to keep the conversion explicit and silent. A follow-up commit drops the loader-v2 truncation and the "exceeds 4 GiB" warning so installers above the legacy limit are parsed natively; another follow-up reads the new 64-bit block-header `stored_size` for Inno Setup 6.7.0 and later. Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- src/cli/debug.cpp | 2 +- src/cli/extract.cpp | 6 +++--- src/loader/offsets.cpp | 4 ++-- src/loader/offsets.hpp | 14 +++++++------- src/stream/chunk.hpp | 4 ++-- src/stream/slice.cpp | 22 +++++++++++----------- src/stream/slice.hpp | 8 ++++---- 7 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/cli/debug.cpp b/src/cli/debug.cpp index c4e4cd36..78d1c358 100644 --- a/src/cli/debug.cpp +++ b/src/cli/debug.cpp @@ -652,7 +652,7 @@ static void dump_headers(std::istream & is, const setup::version & version, cons void dump_headers(std::istream & is, const loader::offsets & offsets, const extract_options & o) { setup::version version; - is.seekg(offsets.header_offset); + is.seekg(std::streamoff(offsets.header_offset)); version.load(is); dump_headers(is, version, o, 0); diff --git a/src/cli/extract.cpp b/src/cli/extract.cpp index 49893117..ccff1bbd 100644 --- a/src/cli/extract.cpp +++ b/src/cli/extract.cpp @@ -946,7 +946,7 @@ void process_file(const fs::path & installer, const extract_options & o) { if(o.data_version) { setup::version version; - ifs.seekg(offsets.header_offset); + ifs.seekg(std::streamoff(offsets.header_offset)); version.load(ifs); if(o.silent) { std::cout << version << '\n'; @@ -985,7 +985,7 @@ void process_file(const fs::path & installer, const extract_options & o) { } #endif - ifs.seekg(offsets.header_offset); + ifs.seekg(std::streamoff(offsets.header_offset)); setup::info info; try { info.load(ifs, entries, o.codepage); @@ -1100,7 +1100,7 @@ void process_file(const fs::path & installer, const extract_options & o) { if(o.test || o.extract) { boost::uint64_t offset = info.data_entries[file.entry().location].uncompressed_size; boost::uint32_t sort_slice = info.data_entries[file.entry().location].chunk.first_slice; - boost::uint32_t sort_offset = info.data_entries[file.entry().location].chunk.sort_offset; + boost::uint64_t sort_offset = info.data_entries[file.entry().location].chunk.sort_offset; BOOST_FOREACH(boost::uint32_t location, file.entry().additional_locations) { setup::data_entry & data = info.data_entries[location]; files_for_location[location].push_back(output_location(&file, offset)); diff --git a/src/loader/offsets.cpp b/src/loader/offsets.cpp index 80bcdc4b..6285f0c3 100644 --- a/src/loader/offsets.cpp +++ b/src/loader/offsets.cpp @@ -109,9 +109,9 @@ bool offsets::load_from_exe_resource(std::istream & is) { return load_offsets_at(is, resource.offset); } -bool offsets::load_offsets_at(std::istream & is, boost::uint32_t pos) { +bool offsets::load_offsets_at(std::istream & is, boost::uint64_t pos) { - if(is.seekg(pos).fail()) { + if(is.seekg(std::streamoff(pos)).fail()) { is.clear(); debug("could not seek to loader header"); return false; diff --git a/src/loader/offsets.hpp b/src/loader/offsets.hpp index f49bd918..cefefb2a 100644 --- a/src/loader/offsets.hpp +++ b/src/loader/offsets.hpp @@ -60,17 +60,17 @@ struct offsets { * * A value of \c 0 means there is no setup.e32 embedded in this file */ - boost::uint32_t exe_offset; + boost::uint64_t exe_offset; /*! * Size of `setup.e32` after compression, in bytes * * A value of \c 0 means the executable size is not known */ - boost::uint32_t exe_compressed_size; + boost::uint64_t exe_compressed_size; //! Size of `setup.e32` before compression, in bytes - boost::uint32_t exe_uncompressed_size; + boost::uint64_t exe_uncompressed_size; /*! * Checksum of `setup.e32` before compression @@ -80,7 +80,7 @@ struct offsets { crypto::checksum exe_checksum; //! Offset of embedded setup messages - boost::uint32_t message_offset; + boost::uint64_t message_offset; /*! * Offset of embedded `setup-0.bin` data (the setup headers) @@ -93,7 +93,7 @@ struct offsets { * * Loading the version and headers is done in \ref setup::info. */ - boost::uint32_t header_offset; + boost::uint64_t header_offset; /*! * Offset of embedded `setup-1.bin` data @@ -109,7 +109,7 @@ struct offsets { * The layout of the chunks and files is stored in the \ref setup::data_entry headers * while the \ref setup::file_entry headers provide the filenames and meta information. */ - boost::uint32_t data_offset; + boost::uint64_t data_offset; /*! * \brief Find the setup loader offsets in a file @@ -128,7 +128,7 @@ struct offsets { bool load_from_exe_resource(std::istream & is); - bool load_offsets_at(std::istream & is, boost::uint32_t pos); + bool load_offsets_at(std::istream & is, boost::uint64_t pos); }; diff --git a/src/stream/chunk.hpp b/src/stream/chunk.hpp index fae34840..16c3ccb6 100644 --- a/src/stream/chunk.hpp +++ b/src/stream/chunk.hpp @@ -81,9 +81,9 @@ struct chunk { boost::uint32_t first_slice; //!< Slice where the chunk starts. boost::uint32_t last_slice; //!< Slice where the chunk ends. - boost::uint32_t sort_offset; + boost::uint64_t sort_offset; - boost::uint32_t offset; //!< Offset of the compressed chunk in firstSlice. + boost::uint64_t offset; //!< Offset of the compressed chunk in firstSlice. boost::uint64_t size; //! Total compressed size of the chunk. compression_method compression; //!< Compression method used by the chunk. diff --git a/src/stream/slice.cpp b/src/stream/slice.cpp index c638f405..6e28db04 100644 --- a/src/stream/slice.cpp +++ b/src/stream/slice.cpp @@ -50,7 +50,7 @@ const char slice_ids[][8] = { } // anonymous namespace -slice_reader::slice_reader(std::istream * istream, boost::uint32_t offset) +slice_reader::slice_reader(std::istream * istream, boost::uint64_t offset) : data_offset(offset), slices_per_disk(1), current_slice(0), slice_size(0), is(istream) { @@ -59,8 +59,8 @@ slice_reader::slice_reader(std::istream * istream, boost::uint32_t offset) std::streampos file_size = is->seekg(0, std::ios_base::end).tellg(); - slice_size = boost::uint32_t(std::min(file_size, max_size)); - if(is->seekg(data_offset).fail()) { + slice_size = boost::uint64_t(std::min(file_size, max_size)); + if(is->seekg(std::streamoff(data_offset)).fail()) { throw slice_error("could not seek to data"); } } @@ -125,12 +125,12 @@ bool slice_reader::open_file(const path_type & file) { if(ifs.fail()) { ifs.close(); throw slice_error("could not read slice size in \"" + file.string() + "\""); - } else if(std::streampos(slice_size) > file_size) { + } else if(std::streampos(std::streamoff(slice_size)) > file_size) { ifs.close(); std::ostringstream oss; oss << "bad slice size in " << file << ": " << slice_size << " > " << file_size; throw slice_error(oss.str()); - } else if(std::streampos(slice_size) < ifs.tellg()) { + } else if(std::streampos(std::streamoff(slice_size)) < ifs.tellg()) { ifs.close(); std::ostringstream oss; oss << "bad slice size in " << file << ": " << slice_size << " < " << ifs.tellg(); @@ -208,7 +208,7 @@ void slice_reader::open(size_t slice) { throw slice_error(oss.str()); } -bool slice_reader::seek(size_t slice, boost::uint32_t offset) { +bool slice_reader::seek(size_t slice, boost::uint64_t offset) { seek(slice); @@ -218,7 +218,7 @@ bool slice_reader::seek(size_t slice, boost::uint32_t offset) { return false; } - if(is->seekg(offset).fail()) { + if(is->seekg(std::streamoff(offset)).fail()) { return false; } @@ -233,21 +233,21 @@ std::streamsize slice_reader::read(char * buffer, std::streamsize bytes) { while(bytes > 0) { - boost::uint32_t read_pos = boost::uint32_t(is->tellg()); + boost::uint64_t read_pos = boost::uint64_t(is->tellg()); if(read_pos > slice_size) { break; } - boost::uint32_t remaining = slice_size - read_pos; + boost::uint64_t remaining = slice_size - read_pos; if(!remaining) { seek(current_slice + 1); - read_pos = boost::uint32_t(is->tellg()); + read_pos = boost::uint64_t(is->tellg()); if(read_pos > slice_size) { break; } remaining = slice_size - read_pos; } - boost::uint64_t toread = std::min(boost::uint64_t(remaining), boost::uint64_t(bytes)); + boost::uint64_t toread = std::min(remaining, boost::uint64_t(bytes)); toread = std::min(toread, boost::uint64_t(std::numeric_limits::max())); if(is->read(buffer, std::streamsize(toread)).fail()) { break; diff --git a/src/stream/slice.hpp b/src/stream/slice.hpp index b2ec52c0..d0b3bc02 100644 --- a/src/stream/slice.hpp +++ b/src/stream/slice.hpp @@ -59,7 +59,7 @@ class slice_reader : public boost::iostreams::source { typedef boost::filesystem::path path_type; // Information for reading embedded setup data - const boost::uint32_t data_offset; + const boost::uint64_t data_offset; // Information for eading external setup data path_type dir; //!< Slice directory specified at construction. @@ -69,7 +69,7 @@ class slice_reader : public boost::iostreams::source { // Information about the current slice size_t current_slice; //!< Number of the currently opened slice. - boost::uint32_t slice_size; //!< Size in bytes of the currently opened slice. + boost::uint64_t slice_size; //!< Size in bytes of the currently opened slice. // Streams util::ifstream ifs; //!< File input stream used when reading from external slices. @@ -97,7 +97,7 @@ class slice_reader : public boost::iostreams::source { * The constructed reader will allow reading the byte range [data_offset, file end) * from the setup executable and provide this as the range [0, file end - data_offset). */ - slice_reader(std::istream * istream, boost::uint32_t offset); + slice_reader(std::istream * istream, boost::uint64_t offset); /*! * Construct a \ref slice_reader to read from external data slices (aka disks). @@ -126,7 +126,7 @@ class slice_reader : public boost::iostreams::source { * \return \c false if the requested slice could not be opened, or if the requested * offset is not a valid position in that slice - \c true otherwise. */ - bool seek(size_t slice, boost::uint32_t offset); + bool seek(size_t slice, boost::uint64_t offset); /*! * Read a number of bytes starting at the current slice and offset within that slice. From b7388dfd161f8de29085e68fafbc32d0250c8924 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 5 May 2026 13:07:43 +0200 Subject: [PATCH 06/11] loader, block: consume native 64-bit offsets and sizes Now that loader::offsets, stream::chunk, stream::slice_reader and the block-reader local size hold their values in uint64_t, drop the truncating uint32_t casts that the previous "loader: support setup loader offset table revision 2" commit had to use as a placeholder. The loader-revision-2 path stores the full 64-bit OffsetEXE / Offset0 / Offset1 directly, and the warning about offsets exceeding 4 GiB is gone because there is no longer any truncation happening; only a much narrower sanity check for negative values remains, since the on-disk fields are signed Int64 in Inno Setup's struct. stream/block.cpp grows a 6.7.0+ branch in block_reader::get that loads stored_size as uint64_t to match the on-disk widening introduced in Inno Setup 6.7.0 (TSetupLdrCompressedBlockHeader's StoredSize was bumped from LongWord to Int64 in Projects/Src/Stream.BlockReader.pas at tag is-6_7_0 in https://github.com/jrsoftware/issrc). With the local stored_size already widened by the previous refactor, the rest of block_reader needs no change: the boost::iostreams::restrict slice still receives the value unchanged via std::streamoff, and the per-4 KiB-subblock checksum-overhead arithmetic now operates on uint64_t directly so the explicit uint32_t cast falls away. Without this commit we observed `block header CRC32 mismatch` on 6.7.0 installers because the older code read just 4 bytes of a freshly-widened 8-byte StoredSize and then mistook the next 4 bytes (actually the upper half of StoredSize) for the compressed flag and the trailing CRC, causing the actual_checksum.finalize() check to fail. The mismatch is what motivated the type-widening refactor in the previous commit. Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- src/loader/offsets.cpp | 13 +++++-------- src/stream/block.cpp | 17 +++++++++++++---- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/loader/offsets.cpp b/src/loader/offsets.cpp index 6285f0c3..2e30a447 100644 --- a/src/loader/offsets.cpp +++ b/src/loader/offsets.cpp @@ -172,20 +172,17 @@ bool offsets::load_offsets_at(std::istream & is, boost::uint64_t pos) { debug("could not read loader header (revision 2)"); return false; } - boost::int64_t max_uint32 = boost::int64_t(std::numeric_limits::max()); - if(offset_exe < 0 || offset_exe > max_uint32 || - offset0 < 0 || offset0 > max_uint32 || - offset1 < 0 || offset1 > max_uint32) { - log_warning << "Loader header offsets exceed 4 GiB; truncating to 32-bit"; + if(offset_exe < 0 || offset0 < 0 || offset1 < 0) { + log_warning << "Loader header has negative offset(s)"; } - exe_offset = boost::uint32_t(offset_exe); + exe_offset = boost::uint64_t(offset_exe); exe_compressed_size = 0; exe_uncompressed_size = uncompressed_exe; exe_checksum.type = crypto::CRC32; exe_checksum.crc32 = crc_exe; message_offset = 0; - header_offset = boost::uint32_t(offset0); - data_offset = boost::uint32_t(offset1); + header_offset = boost::uint64_t(offset0); + data_offset = boost::uint64_t(offset1); boost::uint32_t expected = util::load(is); if(is.fail()) { is.clear(); diff --git a/src/stream/block.cpp b/src/stream/block.cpp index 2a90c209..9a7d958a 100644 --- a/src/stream/block.cpp +++ b/src/stream/block.cpp @@ -158,12 +158,21 @@ block_reader::pointer block_reader::get(std::istream & base, const setup::versio crypto::crc32 actual_checksum; actual_checksum.init(); - boost::uint32_t stored_size; + boost::uint64_t stored_size; block_compression compression; if(version >= INNO_VERSION(4, 0, 9)) { - stored_size = actual_checksum.load(base); + if(version >= INNO_VERSION(6, 7, 0)) { + // Inno Setup 6.7.0 widened the block-header stored_size field + // from 4 to 8 bytes; the on-disk struct is now Int64 instead of + // LongWord, allowing data blocks larger than 4 GiB. See + // Projects/Src/Stream.BlockReader.pas at tag is-6_7_0 in + // https://github.com/jrsoftware/issrc. + stored_size = actual_checksum.load(base); + } else { + stored_size = actual_checksum.load(base); + } boost::uint8_t compressed = actual_checksum.load(base); compression = compressed ? (version >= INNO_VERSION(4, 1, 6) ? LZMA1 : Zlib) : Stored; @@ -180,7 +189,7 @@ block_reader::pointer block_reader::get(std::istream & base, const setup::versio } // Add the size of a CRC32 checksum for each 4KiB subblock. - stored_size += boost::uint32_t(util::ceildiv(stored_size, 4096) * 4); + stored_size += util::ceildiv(stored_size, 4096) * 4; } if(actual_checksum.finalize() != expected_checksum) { @@ -204,7 +213,7 @@ block_reader::pointer block_reader::get(std::istream & base, const setup::versio fis->push(inno_block_filter(), 4096); - fis->push(io::restrict(base, 0, stored_size)); + fis->push(io::restrict(base, std::streamoff(0), std::streamoff(stored_size))); fis->exceptions(std::ios_base::badbit | std::ios_base::failbit); From 0e92dbc4b09405bd7f4d506fc4b21fc0ae767bbb Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 6 May 2026 15:56:44 +0200 Subject: [PATCH 07/11] util/storedenum: let stored_flag_reader pad to a fixed byte count Inno Setup 6.7.0 padded both TSetupHeaderOption and TSetupFileEntryOption to 57 elements (foUnusedPadding/shUnusedPadding = 56) so that the serialised `packed set` always occupies 8 bytes regardless of how many flags are actually defined. The motivation in issrc is to keep 32-bit and 64-bit Delphi builds bit-compatible: a `set` with more than 32 flags has a minimum size of 8 bytes in 64-bit Delphi, so 32-bit builds need to match by reserving the same width. innoextract reads bitsets through stored_flag_reader<>, which sizes itself to the number of flags the caller adds. To support the new padding without exposing the internal byte counter or duplicating the reader API, add a small public helper that drains additional padding bytes from the stream until at least `target` bytes have been consumed in total. A bytes_consumed() getter is added too so callers can introspect the count if needed. The follow-up commits that bring innoextract up to Inno Setup 6.7.0 use this from header::load_flags and file_entry::load. Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- src/util/storedenum.hpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/util/storedenum.hpp b/src/util/storedenum.hpp index 0ccf3338..00aec390 100644 --- a/src/util/storedenum.hpp +++ b/src/util/storedenum.hpp @@ -244,6 +244,22 @@ class stored_flag_reader { return result; } + //! Number of stored bytes already read from the stream. + size_t bytes_consumed() const { return bytes; } + + /*! + * Discard padding bytes so that the total number of bytes read from + * the stream is at least `target`. Used for setups (e.g. Inno Setup + * 6.7.0) that always pad the bitset to a fixed width regardless of + * the number of flags actually defined. + */ + void discard_padding_to(size_t target) { + while(bytes < target) { + (void)util::load(stream); + bytes++; + } + } + }; template From cc1444754d1ac68c59acb91f60bfb22ac2b34acf Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 6 May 2026 20:33:46 +0200 Subject: [PATCH 08/11] Add support for the Inno Setup 6.5.0 entry stream rework Inno Setup 6.5.0 made several invasive changes to the on-disk serialisation that go well beyond the SetupID magic and the new SevenZipLibraryName header field that the previous patch picked up. To actually parse a 6.5.0 installer, four further changes are needed. First, a new TSetupEncryptionHeader record now sits on the outer stream, between the version magic and the first compressed block (Projects/Src/Shared.Struct.pas at tag is-6_5_0 in https://github.com/jrsoftware/issrc). It carries a 4-byte expected CRC32, a 1-byte EncryptionUse flag, a 16-byte KDFSalt, the KDF iteration count, the 24-byte BaseNonce triple (RandomXorStartOffset / RandomXorFirstSlice / RemainingRandom) and a 4-byte PasswordTest, with the CRC32 covering the 49 bytes after itself. Read it on the outer stream so the block reader starts at the right offset; if the EncryptionUse byte is non-zero, log a warning that decrypting 6.5.0+ encrypted installers is not yet supported. Validate the CRC32 against expected_crc and warn on mismatch. Second, the matching encryption fields move out of TSetupHeader. The 4-byte SHA-256 digest prefix and the 44-byte salt-plus-nonce blob that 6.4.0 added directly to the header are no longer present in 6.5.0; the shEncryptionUsed flag is also dropped from TSetupHeaderOption. Gate the existing 6.4.x reads to ``< 6.5.0`` and clear the header.password / password_salt members for 6.5.0+ so that downstream code sees the same "no embedded password" state. Third, the new TSetupISSigKeyEntry array sits between the directory and file entry streams. Read NumISSigKeyEntries (already wired up by the 6.5.0 magic patch as a placeholder, now actually stored in the new header.issig_key_count). After the directory entries, load exactly that many records via the standard load_entries helper, mirroring how component_entry, task_entry, etc. are handled. Each record holds three binary strings (PublicX, PublicY, RuntimeID) naming an Ed25519 public key the installer trusts; the new issig_key_entry struct exposes them so callers that opt in via the ISSigKeys entry-type flag can inspect them. Fourth, when a 6.5.0+ installer sets [Setup] SevenZipLibraryName (supporting Extract7ZipArchive that was added in 6.4.0), an additional binary string appears on the wizard / decompressor stream after the decompressor DLL and before the decryptor DLL slot. Skip past it so later reads of decrypt_dll align correctly. In TSetupFileEntry, 6.5.0 appended five new expression strings (Excludes, DownloadISSigSource, DownloadUserName, DownloadPassword, ExtractArchivePassword) and a per-file Verification packed record made of an ISSigAllowedKeys ansi string, a 32-byte SHA-256 digest, and a TSetupFileVerificationType byte enum (none / hash / IS sig); these sit between BeforeInstall and MinVersion. Two new flag bits foDownload and foExtractArchive were added to TSetupFileEntryOption for the new [Files] flags `download` and `extractarchive`. Add the matching members and stored_enum mapping, gated to `>= 6.5.0`. The SHA-256 digest is stored in the existing file_entry::checksum field (unused before 6.5.0 in upstream innoextract); the trailing unconditional reset of that field is gated to `< 6.5.0` so the verification hash survives. In TSetupFileLocationEntry, Inno Setup 6.4.3 (issrc commits 6aec0a55 "Distinguish file options (fo) and file location options (flo)." and 00d335b7 "Cleanup: TSetupFileLocationEntry contained a few things which Setup doesn't need and are only for the compiler.", both first tagged in is-6_4_3) had already dropped four flags (foVersionInfoNotValid, foIsUninstExe, foApplyTouchDateTime, foSolidBreak) from the Flags bitset and removed the standalone Sign field entirely; the [Files] `signonce` and `signcheck` directives still record a per-file location signing intent at compile time but the result is no longer serialised. Gate the four obsolete flag adds to `< 6.4.3` and short-circuit the Sign read for `>= 6.4.3` to NoSetting. Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- CMakeLists.txt | 2 ++ src/setup/data.cpp | 27 ++++++++++++++++---- src/setup/file.cpp | 58 +++++++++++++++++++++++++++++++++++++++++- src/setup/file.hpp | 19 +++++++++++++- src/setup/header.cpp | 29 ++++++++++++++++----- src/setup/header.hpp | 1 + src/setup/info.cpp | 49 +++++++++++++++++++++++++++++++++++ src/setup/info.hpp | 3 +++ src/setup/issigkey.cpp | 38 +++++++++++++++++++++++++++ src/setup/issigkey.hpp | 55 +++++++++++++++++++++++++++++++++++++++ 10 files changed, 267 insertions(+), 14 deletions(-) create mode 100644 src/setup/issigkey.cpp create mode 100644 src/setup/issigkey.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 69956105..cd21eaf9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -416,6 +416,8 @@ set(INNOEXTRACT_SOURCES src/setup/info.cpp src/setup/ini.hpp src/setup/ini.cpp + src/setup/issigkey.hpp + src/setup/issigkey.cpp src/setup/item.hpp src/setup/item.cpp src/setup/language.hpp diff --git a/src/setup/data.cpp b/src/setup/data.cpp index 4444ce29..a269c3c7 100644 --- a/src/setup/data.cpp +++ b/src/setup/data.cpp @@ -136,20 +136,28 @@ void data_entry::load(std::istream & is, const info & i) { stored_flag_reader flagreader(is, i.version.bits()); flagreader.add(VersionInfoValid); - flagreader.add(VersionInfoNotValid); + if(i.version < INNO_VERSION(6, 4, 3)) { + // Inno Setup 6.4.3 dropped foVersionInfoNotValid, foIsUninstExe, + // foApplyTouchDateTime and foSolidBreak from + // TSetupFileLocationEntry.Flags. See + // Projects/Src/Shared.Struct.pas at tag is-6_4_3 in + // https://github.com/jrsoftware/issrc (issrc commit 6aec0a55, + // "Distinguish file options (fo) and file location options (flo)."). + flagreader.add(VersionInfoNotValid); + } if(i.version >= INNO_VERSION(2, 0, 17) && i.version < INNO_VERSION(4, 0, 1)) { flagreader.add(BZipped); } if(i.version >= INNO_VERSION(4, 0, 10)) { flagreader.add(TimeStampInUTC); } - if(i.version >= INNO_VERSION(4, 1, 0)) { + if(i.version >= INNO_VERSION(4, 1, 0) && i.version < INNO_VERSION(6, 4, 3)) { flagreader.add(IsUninstallerExe); } if(i.version >= INNO_VERSION(4, 1, 8)) { flagreader.add(CallInstructionOptimized); } - if(i.version >= INNO_VERSION(4, 2, 0)) { + if(i.version >= INNO_VERSION(4, 2, 0) && i.version < INNO_VERSION(6, 4, 3)) { flagreader.add(Touch); } if(i.version >= INNO_VERSION(4, 2, 2)) { @@ -160,7 +168,7 @@ void data_entry::load(std::istream & is, const info & i) { } else { options |= ChunkCompressed; } - if(i.version >= INNO_VERSION(5, 1, 13)) { + if(i.version >= INNO_VERSION(5, 1, 13) && i.version < INNO_VERSION(6, 4, 3)) { flagreader.add(SolidBreak); } if(i.version >= INNO_VERSION(5, 5, 7) && i.version < INNO_VERSION(6, 3, 0)) { @@ -171,7 +179,16 @@ void data_entry::load(std::istream & is, const info & i) { options |= flagreader.finalize(); - if(i.version >= INNO_VERSION(6, 3, 0)) { + if(i.version >= INNO_VERSION(6, 4, 3)) { + // Inno Setup 6.4.3 dropped the standalone Sign field from + // TSetupFileLocationEntry. The compiler still records the source + // signing intent in the [Files] section, but it is no longer + // serialised per-file-location. See issrc commit 00d335b7 + // ("Cleanup: TSetupFileLocationEntry contained a few things which + // Setup doesn't need and are only for the compiler.") first + // tagged in is-6_4_3. + sign = NoSetting; + } else if(i.version >= INNO_VERSION(6, 3, 0)) { sign = stored_enum(is).get(); } else if(options & SignOnce) { sign = Once; diff --git a/src/setup/file.cpp b/src/setup/file.cpp index a2e917e3..f8864096 100644 --- a/src/setup/file.cpp +++ b/src/setup/file.cpp @@ -56,6 +56,14 @@ STORED_ENUM_MAP(stored_file_type_1, file_entry::UserFile, file_entry::RegSvrExe, ); +// Inno Setup 6.5.0 introduced the per-file verification kind alongside +// the SHA-256 hash and ISSig allowed-keys list appended to TSetupFileEntry. +STORED_ENUM_MAP(stored_file_verification_type, file_entry::FileVerificationNone, + file_entry::FileVerificationNone, + file_entry::FileVerificationHash, + file_entry::FileVerificationISSig, +); + } // anonymous namespace } // namespace setup @@ -69,6 +77,12 @@ NAMES(setup::file_copy_mode, "File Copy Mode", "always skip if same or older", ) +NAMES(setup::file_entry::file_verification_type, "File Verification Type", + "none", + "hash", + "IS sig", +) + namespace setup { void file_entry::load(std::istream & is, const info & i) { @@ -92,6 +106,34 @@ void file_entry::load(std::istream & is, const info & i) { load_condition_data(is, i); + if(i.version >= INNO_VERSION(6, 5, 0)) { + // Inno Setup 6.5.0 appended five new expression strings (Excludes, + // DownloadISSigSource, DownloadUserName, DownloadPassword, + // ExtractArchivePassword) and a per-file Verification packed + // record (ISSigAllowedKeys ansi string, 32-byte SHA-256 digest, + // TSetupFileVerificationType byte enum) to TSetupFileEntry, after + // the BeforeInstall string and before MinVersion. See + // Projects/Src/Shared.Struct.pas at tag is-6_5_0 in + // https://github.com/jrsoftware/issrc. + is >> util::encoded_string(excludes, i.codepage, i.header.lead_bytes); + is >> util::encoded_string(download_source, i.codepage, i.header.lead_bytes); + is >> util::encoded_string(download_user, i.codepage, i.header.lead_bytes); + is >> util::encoded_string(download_password, i.codepage, i.header.lead_bytes); + is >> util::encoded_string(archive_password, i.codepage, i.header.lead_bytes); + is >> util::ansi_string(issig_allowed_keys); + is.read(checksum.sha256, std::streamsize(sizeof(checksum.sha256))); + checksum.type = crypto::SHA256; + verification = stored_enum(is).get(); + } else { + excludes.clear(); + download_source.clear(); + download_user.clear(); + download_password.clear(); + archive_password.clear(); + issig_allowed_keys.clear(); + verification = FileVerificationNone; + } + load_version_data(is, i.version); location = util::load(is, i.version.bits()); @@ -189,6 +231,14 @@ void file_entry::load(std::istream & is, const info & i) { if(i.version >= INNO_VERSION(5, 2, 5)) { flagreader.add(GacInstall); } + if(i.version >= INNO_VERSION(6, 5, 0)) { + // Inno Setup 6.5.0 added two flags at the end of the bitset for + // the new [Files] flags `download` and `extractarchive` + // (TSetupFileEntry.Options in Projects/Src/Shared.Struct.pas at + // tag is-6_5_0 in https://github.com/jrsoftware/issrc). + flagreader.add(Download); + flagreader.add(ExtractArchive); + } options |= flagreader.finalize(); @@ -199,7 +249,11 @@ void file_entry::load(std::istream & is, const info & i) { } additional_locations.clear(); - checksum.type = crypto::None; + if(i.version < INNO_VERSION(6, 5, 0)) { + // For Inno Setup 6.5.0+, file_entry::checksum carries the SHA-256 + // from the per-file ISSig Verification block, populated above. + checksum.type = crypto::None; + } size = 0; } @@ -239,6 +293,8 @@ NAMES(setup::file_entry::flags, "File Option", "set ntfs compression", "unset ntfs compression", "gac install", + "download", + "extract archive", "readme", ) diff --git a/src/setup/file.hpp b/src/setup/file.hpp index 604b2ae8..9f306bfb 100644 --- a/src/setup/file.hpp +++ b/src/setup/file.hpp @@ -77,11 +77,19 @@ struct file_entry : public item { SetNtfsCompression, UnsetNtfsCompression, GacInstall, + Download, + ExtractArchive, // obsolete options: IsReadmeFile ); + enum file_verification_type { + FileVerificationNone, + FileVerificationHash, + FileVerificationISSig, + }; + enum file_type { UserFile, UninstExe, @@ -96,6 +104,14 @@ struct file_entry : public item { std::string destination; std::string install_font_name; std::string strong_assembly_name; + std::string excludes; //!< Inno Setup 6.5.0+ + std::string download_source; //!< DownloadISSigSource, 6.5.0+ + std::string download_user; //!< DownloadUserName, 6.5.0+ + std::string download_password; //!< 6.5.0+ + std::string archive_password; //!< ExtractArchivePassword, 6.5.0+ + std::string issig_allowed_keys; //!< 6.5.0+ + + file_verification_type verification; //!< 6.5.0+ boost::uint32_t location; //!< index into the data entry list boost::uint32_t attributes; @@ -110,7 +126,7 @@ struct file_entry : public item { // Information about GOG Galaxy multi-part files // These are not used in normal Inno Setup installers std::vector additional_locations; - crypto::checksum checksum; + crypto::checksum checksum; //!< Inno Setup 6.5.0+: SHA-256 from the per-file ISSig Verification block boost::uint64_t size; void load(std::istream & is, const info & i); @@ -121,5 +137,6 @@ struct file_entry : public item { NAMED_FLAGS(setup::file_entry::flags) NAMED_ENUM(setup::file_entry::file_type) +NAMED_ENUM(setup::file_entry::file_verification_type) #endif // INNOEXTRACT_SETUP_FILE_HPP diff --git a/src/setup/header.cpp b/src/setup/header.cpp index 87b30b20..1babfbe7 100644 --- a/src/setup/header.cpp +++ b/src/setup/header.cpp @@ -338,10 +338,14 @@ void header::load(std::istream & is, const version & version) { if(version >= INNO_VERSION(6, 5, 0)) { // Inno Setup 6.5.0 inserted a NumISSigKeyEntries: Integer field // between NumDirEntries and NumFileEntries - // (Projects/Src/Shared.Struct.pas at tag is-6_5_0). innoextract - // does not yet use the count, but the bytes must be consumed to - // keep the stream position correct. - (void)util::load(is, version.bits()); + // (Projects/Src/Shared.Struct.pas at tag is-6_5_0). Each entry is a + // triple of binary strings (PublicX, PublicY, RuntimeID) describing + // an ISSigTool-issued public key the installer trusts; innoextract + // does not yet make use of the keys themselves but stores the count + // so info::try_load can skip the right number of entries. + issig_key_count = util::load(is, version.bits()); + } else { + issig_key_count = 0; } file_count = util::load(is, version.bits()); data_entry_count = util::load(is, version.bits()); @@ -436,7 +440,16 @@ void header::load(std::istream & is, const version & version) { wizard_image_opacity = 0xff; } - if(version >= INNO_VERSION(6, 4, 0)) { + if(version >= INNO_VERSION(6, 5, 0)) { + // Inno Setup 6.5.0 moved the encryption fields (password test, KDF + // salt, KDF iterations, base nonce) out of TSetupHeader and into a + // separate TSetupEncryptionHeader stored elsewhere. innoextract + // does not yet parse encrypted 6.5.0+ installers; for unencrypted + // ones the right thing is to read no encryption bytes from the + // header stream at all. + password.type = crypto::None; + password_salt.clear(); + } else if(version >= INNO_VERSION(6, 4, 0)) { is.read(password.sha256, 4); password.type = crypto::PBKDF2_SHA256_XChaCha20; } else if(version >= INNO_VERSION(5, 3, 9)) { @@ -449,7 +462,9 @@ void header::load(std::istream & is, const version & version) { password.crc32 = util::load(is); password.type = crypto::CRC32; } - if(version >= INNO_VERSION(6, 4, 0)) { + if(version >= INNO_VERSION(6, 5, 0)) { + // Already cleared above; nothing to read. + } else if(version >= INNO_VERSION(6, 4, 0)) { password_salt.resize(44); // PBKDF2 salt + iteration count + ChaCha2 base nonce is.read(&password_salt[0], std::streamsize(password_salt.length())); } else if(version >= INNO_VERSION(4, 2, 2)) { @@ -749,7 +764,7 @@ header::flags header::load_flags(std::istream & is, const version & version) { flagreader.add(AppendDefaultDirName); flagreader.add(AppendDefaultGroupName); } - if(version >= INNO_VERSION(4, 2, 2)) { + if(version >= INNO_VERSION(4, 2, 2) && version < INNO_VERSION(6, 5, 0)) { flagreader.add(EncryptionUsed); } if(version >= INNO_VERSION(5, 0, 4) && version < INNO_VERSION(5, 6, 1)) { diff --git a/src/setup/header.hpp b/src/setup/header.hpp index 77fa651c..a0211fc9 100644 --- a/src/setup/header.hpp +++ b/src/setup/header.hpp @@ -188,6 +188,7 @@ struct header { size_t component_count; size_t task_count; size_t directory_count; + size_t issig_key_count; size_t file_count; size_t data_entry_count; size_t icon_count; diff --git a/src/setup/info.cpp b/src/setup/info.cpp index d6969240..0678f360 100644 --- a/src/setup/info.cpp +++ b/src/setup/info.cpp @@ -27,6 +27,7 @@ #include #include "crypto/hasher.hpp" +#include "crypto/crc32.hpp" #include "crypto/pbkdf2.hpp" #include "crypto/sha256.hpp" #include "crypto/xchacha20.hpp" @@ -37,6 +38,7 @@ #include "setup/file.hpp" #include "setup/icon.hpp" #include "setup/ini.hpp" +#include "setup/issigkey.hpp" #include "setup/item.hpp" #include "setup/language.hpp" #include "setup/message.hpp" @@ -123,6 +125,16 @@ void load_wizard_and_decompressor(std::istream & is, const setup::version & vers } } + if(version >= INNO_VERSION(6, 5, 0) && !header.seven_zip_library_name.empty()) { + // Inno Setup 6.5.0 added an embedded 7-Zip decoder DLL stream + // after the decompressor DLL block, present whenever the [Setup] + // directive SevenZipLibraryName is set (which Setup uses to + // implement Extract7ZipArchive support added in 6.4.0). Skip it; + // innoextract does not need to decode 7-Zip archives during a + // listing. + util::binary_string::skip(is); + } + info.decrypt_dll.clear(); if((header.options & header::EncryptionUsed) && version < INNO_VERSION(6, 4, 0)) { if(entries & (info::DecryptDll | info::NoSkip)) { @@ -204,6 +216,8 @@ void info::try_load(std::istream & is, entry_types entries, util::codepage_id fo load_entries(*reader, entries, header.task_count, tasks, Tasks); debug("loading directories"); load_entries(*reader, entries, header.directory_count, directories, Directories); + debug("loading issig keys"); + load_entries(*reader, entries, header.issig_key_count, issig_keys, ISSigKeys); debug("loading files"); load_entries(*reader, entries, header.file_count, files, Files); debug("loading icons"); @@ -263,6 +277,41 @@ void info::load(std::istream & is, entry_types entries, util::codepage_id force_ entries |= NoSkip; } + if(version >= INNO_VERSION(6, 5, 0)) { + // Inno Setup 6.5.0 split the encryption metadata out of + // TSetupHeader into a standalone TSetupEncryptionHeader stored on + // the outer stream between the version magic and the first block + // (Projects/Src/Shared.Struct.pas at tag is-6_5_0 in + // https://github.com/jrsoftware/issrc). Read it here so the block + // reader starts at the right offset; innoextract does not yet + // support actually decrypting 6.5.0+ encrypted installers. + boost::uint32_t expected_crc = util::load(is); + crypto::crc32 checksum; + checksum.init(); + boost::uint8_t encryption_use = checksum.load(is); + if(encryption_use != 0) { + log_warning << "Encrypted setup not supported; cannot decrypt files"; + } + // 16-byte KDFSalt + 4-byte KDFIterations + // + 8-byte BaseNonce.RandomXorStartOffset + // + 4-byte BaseNonce.RandomXorFirstSlice + // + 12-byte BaseNonce.RemainingRandom (3 x 4 bytes) + // + 4-byte PasswordTest = 48 bytes after encryption_use. + for(int i = 0; i < 16; i++) { + (void)checksum.load(is); + } + (void)checksum.load(is); + (void)checksum.load(is); + (void)checksum.load(is); + for(int i = 0; i < 3; i++) { + (void)checksum.load(is); + } + (void)checksum.load(is); + if(checksum.finalize() != expected_crc) { + log_warning << "Encryption header checksum mismatch!"; + } + } + bool parsed_without_errors = false; std::streampos start = is.tellg(); for(;;) { diff --git a/src/setup/info.hpp b/src/setup/info.hpp index e08cfd43..fb8d57b7 100644 --- a/src/setup/info.hpp +++ b/src/setup/info.hpp @@ -43,6 +43,7 @@ struct directory_entry; struct file_entry; struct icon_entry; struct ini_entry; +struct issig_key_entry; struct language_entry; struct message_entry; struct permission_entry; @@ -69,6 +70,7 @@ struct info { Files, Icons, IniEntries, + ISSigKeys, Languages, Messages, Permissions, @@ -98,6 +100,7 @@ struct info { std::vector files; //! \c Files std::vector icons; //! \c Icons std::vector ini_entries; //! \c IniEntries + std::vector issig_keys; //! \c ISSigKeys std::vector languages; //! \c Languages std::vector messages; //! \c Messages std::vector permissions; //! \c Permissions diff --git a/src/setup/issigkey.cpp b/src/setup/issigkey.cpp new file mode 100644 index 00000000..7d3bfc09 --- /dev/null +++ b/src/setup/issigkey.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2011-2019 Daniel Scharrer + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the author(s) be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +#include "setup/issigkey.hpp" + +#include "util/load.hpp" + +namespace setup { + +void issig_key_entry::load(std::istream & is, const info & /* i */) { + + // TSetupISSigKeyEntry: PublicX, PublicY, RuntimeID + // (Projects/Src/Shared.Struct.pas at tag is-6_5_0 in + // https://github.com/jrsoftware/issrc). + is >> util::binary_string(public_x); + is >> util::binary_string(public_y); + is >> util::binary_string(runtime_id); + +} + +} // namespace setup diff --git a/src/setup/issigkey.hpp b/src/setup/issigkey.hpp new file mode 100644 index 00000000..e027d435 --- /dev/null +++ b/src/setup/issigkey.hpp @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2011-2019 Daniel Scharrer + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the author(s) be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +/*! + * \file + * + * Structures for ISSig public key entries stored in Inno Setup 6.5.0+ files. + */ +#ifndef INNOEXTRACT_SETUP_ISSIGKEY_HPP +#define INNOEXTRACT_SETUP_ISSIGKEY_HPP + +#include +#include + +namespace setup { + +struct info; + +struct issig_key_entry { + + // introduced in 6.5.0 + + // X coordinate of the Ed25519 public key, as raw bytes. + std::string public_x; + + // Y coordinate of the Ed25519 public key, as raw bytes. + std::string public_y; + + // Tool-supplied identifier (typically the ISSigTool key file name). + std::string runtime_id; + + void load(std::istream & is, const info & i); + +}; + +} // namespace setup + +#endif // INNOEXTRACT_SETUP_ISSIGKEY_HPP From 5cb290709b281e8de5a752ae07b7c3eeddc5cd0f Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 6 May 2026 21:20:26 +0200 Subject: [PATCH 09/11] data: widen the per-file location start offset to 64 bits for Inno Setup 6.5.2 Inno Setup 6.5.2 widened TSetupFileLocationEntry.StartOffset from LongWord (4 bytes) to Int64 (8 bytes) in Projects/Src/Shared.Struct.pas (tag is-6_5_2 in https://github.com/jrsoftware/issrc), so that the data offset within a slice can exceed 4 GiB. The matching change for chunk.first_slice and chunk.last_slice had already happened in 4.0.0 / 5.x; only StartOffset was still 32-bit. Read the new layout as uint64 for `>= 6.5.2`, keep the old 32-bit read for everything below. The earlier offsets-table widening patch already widened chunk_location::offset itself to 64 bits, so the only thing that needed to change here is the read width. Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- src/setup/data.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/setup/data.cpp b/src/setup/data.cpp index a269c3c7..9e48939c 100644 --- a/src/setup/data.cpp +++ b/src/setup/data.cpp @@ -56,7 +56,16 @@ void data_entry::load(std::istream & is, const info & i) { } } - chunk.sort_offset = chunk.offset = util::load(is); + if(i.version >= INNO_VERSION(6, 5, 2)) { + // Inno Setup 6.5.2 widened TSetupFileLocationEntry.StartOffset + // from LongWord (4 bytes) to Int64 (8 bytes) so data offsets + // within a slice can exceed 4 GiB. See + // Projects/Src/Shared.Struct.pas at tag is-6_5_2 in + // https://github.com/jrsoftware/issrc. + chunk.sort_offset = chunk.offset = util::load(is); + } else { + chunk.sort_offset = chunk.offset = util::load(is); + } if(i.version >= INNO_VERSION(4, 0, 1)) { file.offset = util::load(is); From 1f1c5ce97d4b44deca39c8fd11130936ac9522e4 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 6 May 2026 22:33:50 +0200 Subject: [PATCH 10/11] Add support for the Inno Setup 6.6.0 entry stream rework Inno Setup 6.6.0 reorganised TSetupLanguageEntry beyond what is already covered by the matching SetupID magic patch. To actually parse 6.6.0 language entries, the on-disk layout in Projects/Src/Shared.Struct.pas (tag is-6_6_0 in https://github.com/jrsoftware/issrc) needs three changes. The TitleFontName and CopyrightFontName binary strings were dropped entirely (the new dialog-font-base-scale machinery makes per-language title/copyright fonts unnecessary). The matching TitleFontSize and CopyrightFontSize integers were replaced by DialogFontBaseScaleHeight and DialogFontBaseScaleWidth, sitting between DialogFontSize and WelcomeFontSize. And LanguageID narrowed from Cardinal (4 bytes) to Word (2 bytes) on disk, since the upper half was always zero anyway. Add corresponding members for the new scale fields, gate the obsolete reads to ``< 6.6.0``, and read LanguageID at the right width. In TSetupCustomMessageEntry/TSetupPermissionEntry the read order is unchanged; only the wizard-and-decompressor stream gains material: 6.6.0 added a second copy of each wizard image group right after the existing main and small image groups, holding the dynamic-dark images that match the new WantWizardImagesDynamicDark plumbing in Setup.MainFunc.pas (tag is-6_6_0). Both groups are always serialised even when dark-mode imagery is not configured; the runtime is what picks which set to display. innoextract does not expose dark-theme images yet, but the bytes have to be consumed so the decompressor / decryptor blocks that follow stay aligned. Read both groups into local vectors and discard them, gated to ``>= 6.6.0``. Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- src/setup/info.cpp | 11 +++++++++++ src/setup/language.cpp | 43 +++++++++++++++++++++++++++++++++++++----- src/setup/language.hpp | 2 ++ 3 files changed, 51 insertions(+), 5 deletions(-) diff --git a/src/setup/info.cpp b/src/setup/info.cpp index 0678f360..8afe01b8 100644 --- a/src/setup/info.cpp +++ b/src/setup/info.cpp @@ -113,6 +113,17 @@ void load_wizard_and_decompressor(std::istream & is, const setup::version & vers load_wizard_images(is, version, info.wizard_images_small, entries); } + if(version >= INNO_VERSION(6, 6, 0)) { + // Inno Setup 6.6.0 added a second copy of each wizard image + // group for the dynamic-dark theme. The on-disk format always + // includes both copies regardless of whether dark-mode images + // were actually configured; the runtime decides which set to + // use based on WantWizardImagesDynamicDark. + std::vector dark_images, dark_images_small; + load_wizard_images(is, version, dark_images, entries); + load_wizard_images(is, version, dark_images_small, entries); + } + info.decompressor_dll.clear(); if(header.compression == stream::BZip2 || (header.compression == stream::LZMA1 && version == INNO_VERSION(4, 1, 5)) diff --git a/src/setup/language.cpp b/src/setup/language.cpp index 6b408136..62be8401 100644 --- a/src/setup/language.cpp +++ b/src/setup/language.cpp @@ -140,9 +140,17 @@ void language_entry::load(std::istream & is, const info & i) { } is >> util::binary_string(dialog_font); - is >> util::binary_string(title_font); + if(i.version < INNO_VERSION(6, 6, 0)) { + is >> util::binary_string(title_font); + } else { + title_font.clear(); + } is >> util::binary_string(welcome_font); - is >> util::binary_string(copyright_font); + if(i.version < INNO_VERSION(6, 6, 0)) { + is >> util::binary_string(copyright_font); + } else { + copyright_font.clear(); + } if(i.version >= INNO_VERSION(4, 0, 0)) { is >> util::binary_string(data); @@ -156,7 +164,15 @@ void language_entry::load(std::istream & is, const info & i) { license_text.clear(), info_before.clear(), info_after.clear(); } - language_id = util::load(is); + if(i.version >= INNO_VERSION(6, 6, 0)) { + // Inno Setup 6.6.0 narrowed LanguageID from Cardinal (4 bytes) to + // Word (2 bytes) on disk; see TSetupLanguageEntry in + // Projects/Src/Shared.Struct.pas at tag is-6_6_0 in + // https://github.com/jrsoftware/issrc. + language_id = util::load(is); + } else { + language_id = util::load(is); + } if(i.version < INNO_VERSION(4, 2, 2)) { codepage = default_codepage_for_language(language_id); @@ -186,9 +202,26 @@ void language_entry::load(std::istream & is, const info & i) { dialog_font_standard_height = 0; } - title_font_size = util::load(is); + if(i.version >= INNO_VERSION(6, 6, 0)) { + // 6.6.0 replaced TitleFontSize and CopyrightFontSize with + // DialogFontBaseScaleHeight and DialogFontBaseScaleWidth (each a + // 32-bit Integer) sitting between DialogFontSize and + // WelcomeFontSize. See TSetupLanguageEntry in + // Projects/Src/Shared.Struct.pas at tag is-6_6_0. + dialog_font_base_scale_height = util::load(is); + dialog_font_base_scale_width = util::load(is); + title_font_size = 0; + } else { + dialog_font_base_scale_height = 0; + dialog_font_base_scale_width = 0; + title_font_size = util::load(is); + } welcome_font_size = util::load(is); - copyright_font_size = util::load(is); + if(i.version < INNO_VERSION(6, 6, 0)) { + copyright_font_size = util::load(is); + } else { + copyright_font_size = 0; + } if(i.version == INNO_VERSION_EXT(5, 5, 7, 1)) { util::load(is); // always 8 or 9? diff --git a/src/setup/language.hpp b/src/setup/language.hpp index 8801c643..99b65d0e 100644 --- a/src/setup/language.hpp +++ b/src/setup/language.hpp @@ -56,6 +56,8 @@ struct language_entry { boost::uint32_t codepage; size_t dialog_font_size; size_t dialog_font_standard_height; + size_t dialog_font_base_scale_height; //!< Inno Setup 6.6.0+ + size_t dialog_font_base_scale_width; //!< Inno Setup 6.6.0+ size_t title_font_size; size_t welcome_font_size; size_t copyright_font_size; From 0254c3f11e34701000ad763be7753623f81152bf Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 7 May 2026 01:08:13 +0200 Subject: [PATCH 11/11] Add support for Inno Setup 6.7.0 and 6.7.1 Inno Setup 6.7.0 made another sweep of changes to the on-disk serialisation. The 6.7.x point releases share a single SetupID magic, "Inno Setup Setup Data (6.7.0)" (Projects/Src/Setup.MainFunc.pas at tag is-6_7_0 in https://github.com/jrsoftware/issrc), so 6.7.0 and 6.7.1 are detected together and parsed identically. In TSetupHeader, the five shUsePrevious* flags (UsePreviousAppDir, UsePreviousGroup, UsePreviousSetupType, UsePreviousTasks, UsePreviousUserInfo) were promoted out of the TSetupHeaderOption bitset into expression-string fields of the same names, so that they can take per-install conditional values rather than just on/off. Read the five new strings between SevenZipLibraryName and LicenseText, and gate the corresponding flag adds in load_flags to ``< 6.7.0`` so the bitset matches the new length on the wire. Three more wizard-colour fields were added next to the 6.5.2/6.6.0 ones: WizardBackColor between WizardSmallImageBackColor and the *DynamicDark colour pair, WizardBackColorDynamicDark in the same position relative to the dynamic-dark companions, and WizardBackImageOpacity (Byte) right after WizardImageOpacity. TSetupWizardLightControlStyling (wcsAll, wcsAllButButtons, wcsOnlyRequired) is also stored as a byte enum at that point. Add header members for all four, default the older versions to 0xff / LightControlStyleAll respectively, and provide the matching stored_enum and NAMES blocks. TSetupHeaderOption itself dropped shWizardLightButtonsUnstyled (the 6.6.x experiment for light theme) and added shRedirectionGuard and shWizardBevelsHidden at the end. Tighten the existing WizardLightButtonsUnstyled add to ``< 6.7.0`` and add the two new flags for ``>= 6.7.0`` with their NAMES strings, mirroring the 6.6.0 pattern. The whole bitset was also padded to 57 elements via shUnusedPadding=56 so the serialised set is always 8 bytes regardless of which subset of flags is actually defined. The motivation in issrc is bit-compatibility between 32-bit and 64-bit Delphi builds (a `set` with more than 32 elements has a minimum size of 8 bytes in 64-bit Delphi). With every active 6.7.0 header flag now modelled, the flagreader fills 6 bytes; use the new storedenum helper to drain the remaining 2 padding bytes up to 8 bytes total. The same pattern applies to TSetupFileEntryOption with its foUnusedPadding=56 sentinel; add the matching discard_padding_to(8) call after file_entry's flagreader.finalize(). In TSetupComponentEntry and TSetupTaskEntry, Level narrowed from Integer (4 bytes) to Byte (1 byte). Both fields were already restricted to 0..99 in the compiler, so the storage shrink is purely a wire-format change. Finally Setup.MainFunc.pas adds a third wizard image group on the wizard / decompressor stream for the new WizardBackImageFile directive, in addition to the existing main and small image groups. The same group is duplicated for the dynamic-dark theme that 6.6.0 introduced, matching the existing main/small dark pair. Read the new non-dark group into a new info::wizard_images_back vector (so extraction can later expose it), and read the dark back-image group into a discarded local. Both are gated to ``>= 6.7.0`` and sit on exactly the same fork between the two pre-existing groups and the decompressor DLL block as the 6.6.0 dynamic-dark groups. Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- src/setup/component.cpp | 8 +++- src/setup/file.cpp | 9 ++++ src/setup/header.cpp | 103 +++++++++++++++++++++++++++++++++++++--- src/setup/header.hpp | 17 +++++++ src/setup/info.cpp | 15 +++++- src/setup/info.hpp | 1 + src/setup/task.cpp | 7 ++- src/setup/version.cpp | 1 + 8 files changed, 151 insertions(+), 10 deletions(-) diff --git a/src/setup/component.cpp b/src/setup/component.cpp index eab4f1a5..b6d3734d 100644 --- a/src/setup/component.cpp +++ b/src/setup/component.cpp @@ -74,7 +74,13 @@ void component_entry::load(std::istream & is, const info & i) { } else { extra_disk_pace_required = util::load(is); } - if(i.version >= INNO_VERSION(4, 0, 0) || (i.version.is_isx() && i.version >= INNO_VERSION(3, 0, 3))) { + if(i.version >= INNO_VERSION(6, 7, 0)) { + // Inno Setup 6.7.0 narrowed TSetupComponentEntry.Level from + // Integer (4 bytes) to Byte (1 byte). See + // Projects/Src/Shared.Struct.pas at tag is-6_7_0 in + // https://github.com/jrsoftware/issrc. + level = util::load(is); + } else if(i.version >= INNO_VERSION(4, 0, 0) || (i.version.is_isx() && i.version >= INNO_VERSION(3, 0, 3))) { level = util::load(is); } else { level = 0; diff --git a/src/setup/file.cpp b/src/setup/file.cpp index f8864096..484b0272 100644 --- a/src/setup/file.cpp +++ b/src/setup/file.cpp @@ -241,6 +241,15 @@ void file_entry::load(std::istream & is, const info & i) { } options |= flagreader.finalize(); + if(i.version >= INNO_VERSION(6, 7, 0)) { + // Inno Setup 6.7.0 padded TSetupFileEntryOption to 57 elements + // (foUnusedPadding=56) so the set is always 8 bytes regardless + // of the actual flag count, matching the analogous change to + // TSetupHeaderOption (see is-6_7_0 in issrc). Skip past the + // extra padding bytes so the trailing FileType byte is read + // from the right offset. + flagreader.discard_padding_to(8); + } if(i.version.bits() == 16 || i.version >= INNO_VERSION(5, 0, 0)) { type = stored_enum(is).get(); diff --git a/src/setup/header.cpp b/src/setup/header.cpp index 1babfbe7..7fbac6fc 100644 --- a/src/setup/header.cpp +++ b/src/setup/header.cpp @@ -62,6 +62,12 @@ STORED_ENUM_MAP(stored_dark_style, header::LightStyle, header::DynamicStyle ); +STORED_ENUM_MAP(stored_light_control_styling, header::LightControlStyleAll, + header::LightControlStyleAll, + header::LightControlStyleAllButButtons, + header::LightControlStyleOnlyRequired +); + STORED_ENUM_MAP(stored_bool_auto_no_yes, header::Auto, header::Auto, header::No, @@ -282,6 +288,24 @@ void header::load(std::istream & is, const version & version) { } else { seven_zip_library_name.clear(); } + if(version >= INNO_VERSION(6, 7, 0)) { + // Inno Setup 6.7.0 moved five settings out of the + // TSetupHeaderOption bitset into expression-string fields: + // UsePreviousAppDir, UsePreviousGroup, UsePreviousSetupType, + // UsePreviousTasks and UsePreviousUserInfo + // (Projects/Src/Shared.Struct.pas at tag is-6_7_0). + is >> util::binary_string(use_previous_app_dir); + is >> util::binary_string(use_previous_group); + is >> util::binary_string(use_previous_setup_type); + is >> util::binary_string(use_previous_tasks); + is >> util::binary_string(use_previous_user_info); + } else { + use_previous_app_dir.clear(); + use_previous_group.clear(); + use_previous_setup_type.clear(); + use_previous_tasks.clear(); + use_previous_user_info.clear(); + } if(version >= INNO_VERSION(5, 2, 5)) { is >> util::ansi_string(license_text); is >> util::ansi_string(info_before); @@ -424,6 +448,13 @@ void header::load(std::istream & is, const version & version) { image_back_color = util::load(is); small_image_back_color = util::load(is); } + if(version >= INNO_VERSION(6, 7, 0)) { + // 6.7.0 inserted WizardBackColor between WizardSmallImageBackColor + // and the *DynamicDark colour pair. + wizard_back_color = util::load(is); + } else { + wizard_back_color = 0; + } if(version >= INNO_VERSION(6, 6, 0)) { // 6.6.0 added the DynamicDark variants. image_back_color_dynamic_dark = util::load(is); @@ -432,6 +463,13 @@ void header::load(std::istream & is, const version & version) { image_back_color_dynamic_dark = 0; small_image_back_color_dynamic_dark = 0; } + if(version >= INNO_VERSION(6, 7, 0)) { + // 6.7.0 also added WizardBackColorDynamicDark, in the same + // position relative to its dynamic-dark companions. + wizard_back_color_dynamic_dark = util::load(is); + } else { + wizard_back_color_dynamic_dark = 0; + } if(version >= INNO_VERSION(6, 6, 1)) { // 6.6.1 added WizardImageOpacity (Byte). Default for older versions // is fully opaque. @@ -439,6 +477,16 @@ void header::load(std::istream & is, const version & version) { } else { wizard_image_opacity = 0xff; } + if(version >= INNO_VERSION(6, 7, 0)) { + // 6.7.0 added WizardBackImageOpacity (Byte) and + // WizardLightControlStyling (TSetupWizardLightControlStyling, byte + // enum: wcsAll / wcsAllButButtons / wcsOnlyRequired). + wizard_back_image_opacity = util::load(is); + wizard_light_control_styling = stored_enum(is).get(); + } else { + wizard_back_image_opacity = 0xff; + wizard_light_control_styling = LightControlStyleAll; + } if(version >= INNO_VERSION(6, 5, 0)) { // Inno Setup 6.5.0 moved the encryption fields (password test, KDF @@ -700,19 +748,20 @@ header::flags header::load_flags(std::istream & is, const version & version) { if(version >= INNO_VERSION(1, 3, 0) && version < INNO_VERSION(5, 3, 8)) { flagreader.add(CreateUninstallRegKey); } - if(version >= INNO_VERSION(1, 3, 1)) { + if(version >= INNO_VERSION(1, 3, 1) && version < INNO_VERSION(6, 7, 0)) { flagreader.add(UsePreviousAppDir); } if(version >= INNO_VERSION(1, 3, 3) && version < INNO_VERSION_EXT(6, 4, 0, 1)) { flagreader.add(BackColorHorizontal); } - if(version >= INNO_VERSION(1, 3, 10)) { + if(version >= INNO_VERSION(1, 3, 10) && version < INNO_VERSION(6, 7, 0)) { flagreader.add(UsePreviousGroup); } if(version >= INNO_VERSION(1, 3, 20)) { flagreader.add(UpdateUninstallLogAppName); } - if(version >= INNO_VERSION(2, 0, 0) || (version.is_isx() && version >= INNO_VERSION(1, 3, 10))) { + if((version >= INNO_VERSION(2, 0, 0) || (version.is_isx() && version >= INNO_VERSION(1, 3, 10))) + && version < INNO_VERSION(6, 7, 0)) { flagreader.add(UsePreviousSetupType); } if(version >= INNO_VERSION(2, 0, 0)) { @@ -720,7 +769,9 @@ header::flags header::load_flags(std::istream & is, const version & version) { flagreader.add(AlwaysShowComponentsList); flagreader.add(FlatComponentsList); flagreader.add(ShowComponentSizes); - flagreader.add(UsePreviousTasks); + if(version < INNO_VERSION(6, 7, 0)) { + flagreader.add(UsePreviousTasks); + } flagreader.add(DisableReadyPage); } if(version >= INNO_VERSION(2, 0, 7)) { @@ -735,7 +786,9 @@ header::flags header::load_flags(std::istream & is, const version & version) { } if(version >= INNO_VERSION(3, 0, 0)) { flagreader.add(UserInfoPage); - flagreader.add(UsePreviousUserInfo); + if(version < INNO_VERSION(6, 7, 0)) { + flagreader.add(UsePreviousUserInfo); + } } if(version >= INNO_VERSION(3, 0, 1)) { flagreader.add(UninstallRestartComputer); @@ -811,13 +864,36 @@ header::flags header::load_flags(std::istream & is, const version & version) { } if(version >= INNO_VERSION(6, 6, 0)) { // Inno Setup 6.6.0 added four new wizard-styling flags after - // shUninstallLogging. WizardLightButtonsUnstyled is later - // dropped again in 6.7.0 (handled in the 6.7.0 patch). + // shUninstallLogging, three of which carry over into 6.7.0. flagreader.add(WizardModern); flagreader.add(WizardBorderStyled); flagreader.add(WizardKeepAspectRatio); + } + if(version >= INNO_VERSION(6, 6, 0) && version < INNO_VERSION(6, 7, 0)) { + // shWizardLightButtonsUnstyled exists only in the 6.6.x series; + // 6.7.0 dropped it again in favour of the new + // TSetupWizardLightControlStyling enum stored further up in + // TSetupHeader. flagreader.add(WizardLightButtonsUnstyled); } + if(version >= INNO_VERSION(6, 7, 0)) { + // Inno Setup 6.7.0 added two more wizard-related flags at the end + // of TSetupHeaderOption; see Projects/Src/Shared.Struct.pas at + // tag is-6_7_0 in https://github.com/jrsoftware/issrc. + flagreader.add(RedirectionGuard); + flagreader.add(WizardBevelsHidden); + } + + if(version >= INNO_VERSION(6, 7, 0)) { + // Inno Setup 6.7.0 padded TSetupHeaderOption to 57 elements via a + // shUnusedPadding=56 sentinel so the set is always 8 bytes + // regardless of the actual flag count, in order to keep 32-bit + // and 64-bit Delphi builds bit-compatible (Projects/Src/Shared.Struct.pas + // at tag is-6_7_0). The flags above only fill 6 bytes; skip the + // remaining padding bytes so file_count and the rest of the + // header stay aligned. + flagreader.discard_padding_to(8); + } return flagreader.finalize(); } @@ -853,6 +929,11 @@ void header::decode(util::codepage_id codepage) { util::to_utf8(close_applications_filter, codepage); util::to_utf8(close_applications_filter_excludes, codepage); util::to_utf8(seven_zip_library_name, codepage); + util::to_utf8(use_previous_app_dir, codepage); + util::to_utf8(use_previous_group, codepage); + util::to_utf8(use_previous_setup_type, codepage); + util::to_utf8(use_previous_tasks, codepage); + util::to_utf8(use_previous_user_info, codepage); util::to_utf8(setup_mutex, codepage, &lead_bytes); util::to_utf8(changes_environment, codepage); util::to_utf8(changes_associations, codepage); @@ -918,6 +999,8 @@ NAMES(setup::header::flags, "Setup Option", "wizard border styled", "wizard keep aspect ratio", "wizard light buttons unstyled", + "redirection guard", + "wizard bevels hidden", "uninstallable", "disable dir page", "disable program group page", @@ -976,6 +1059,12 @@ NAMES(setup::header::dark_style, "Dark Style", "dynamic", ) +NAMES(setup::header::light_control_styling, "Light Control Styling", + "all", + "all but buttons", + "only required", +) + NAMES(setup::header::auto_bool, "Auto Boolean", "auto", "no", diff --git a/src/setup/header.hpp b/src/setup/header.hpp index a0211fc9..2138c22e 100644 --- a/src/setup/header.hpp +++ b/src/setup/header.hpp @@ -106,6 +106,8 @@ struct header { WizardBorderStyled, WizardKeepAspectRatio, WizardLightButtonsUnstyled, + RedirectionGuard, + WizardBevelsHidden, // Obsolete flags Uninstallable, @@ -173,6 +175,11 @@ struct header { std::string architectures_installed_in_64bit_mode_expr; std::string close_applications_filter_excludes; std::string seven_zip_library_name; + std::string use_previous_app_dir; + std::string use_previous_group; + std::string use_previous_setup_type; + std::string use_previous_tasks; + std::string use_previous_user_info; std::string license_text; std::string info_before; std::string info_after; @@ -208,7 +215,10 @@ struct header { Color small_image_back_color; Color image_back_color_dynamic_dark; Color small_image_back_color_dynamic_dark; + Color wizard_back_color; + Color wizard_back_color_dynamic_dark; boost::uint8_t wizard_image_opacity; + boost::uint8_t wizard_back_image_opacity; enum style { ClassicStyle, @@ -221,6 +231,12 @@ struct header { DynamicStyle }; dark_style wizard_dark_style; + enum light_control_styling { + LightControlStyleAll, + LightControlStyleAllButButtons, + LightControlStyleOnlyRequired + }; + light_control_styling wizard_light_control_styling; boost::uint32_t wizard_resize_percent_x; boost::uint32_t wizard_resize_percent_y; @@ -315,6 +331,7 @@ NAMED_ENUM(setup::header::install_verbosity) NAMED_ENUM(setup::header::log_mode) NAMED_ENUM(setup::header::style) NAMED_ENUM(setup::header::dark_style) +NAMED_ENUM(setup::header::light_control_styling) NAMED_ENUM(setup::header::auto_bool) NAMED_ENUM(setup::header::privilege_level) NAMED_ENUM(setup::header::language_detection_method) diff --git a/src/setup/info.cpp b/src/setup/info.cpp index 8afe01b8..30a3c986 100644 --- a/src/setup/info.cpp +++ b/src/setup/info.cpp @@ -106,6 +106,7 @@ void load_wizard_and_decompressor(std::istream & is, const setup::version & vers info.wizard_images.clear(); info.wizard_images_small.clear(); + info.wizard_images_back.clear(); load_wizard_images(is, version, info.wizard_images, entries); @@ -113,15 +114,27 @@ void load_wizard_and_decompressor(std::istream & is, const setup::version & vers load_wizard_images(is, version, info.wizard_images_small, entries); } + if(version >= INNO_VERSION(6, 7, 0)) { + // Inno Setup 6.7.0 added a "back image" wizard image group (used + // for the new WizardBackImageFile directive), in addition to the + // existing main and small image groups. See the wizard-image read + // block in Projects/Src/Setup.MainFunc.pas at tag is-6_7_0 in + // https://github.com/jrsoftware/issrc. + load_wizard_images(is, version, info.wizard_images_back, entries); + } + if(version >= INNO_VERSION(6, 6, 0)) { // Inno Setup 6.6.0 added a second copy of each wizard image // group for the dynamic-dark theme. The on-disk format always // includes both copies regardless of whether dark-mode images // were actually configured; the runtime decides which set to // use based on WantWizardImagesDynamicDark. - std::vector dark_images, dark_images_small; + std::vector dark_images, dark_images_small, dark_images_back; load_wizard_images(is, version, dark_images, entries); load_wizard_images(is, version, dark_images_small, entries); + if(version >= INNO_VERSION(6, 7, 0)) { + load_wizard_images(is, version, dark_images_back, entries); + } } info.decompressor_dll.clear(); diff --git a/src/setup/info.hpp b/src/setup/info.hpp index fb8d57b7..10f7f078 100644 --- a/src/setup/info.hpp +++ b/src/setup/info.hpp @@ -114,6 +114,7 @@ struct info { //! Loading enabled by \c WizardImages std::vector wizard_images; std::vector wizard_images_small; + std::vector wizard_images_back; //!< Inno Setup 6.7.0+ background images //! Contents of the helper DLL used to decompress setup data in some versions. //! Loading enabled by \c DecompressorDll diff --git a/src/setup/task.cpp b/src/setup/task.cpp index a0bbdd8c..6fbe1ef8 100644 --- a/src/setup/task.cpp +++ b/src/setup/task.cpp @@ -45,7 +45,12 @@ void task_entry::load(std::istream & is, const info & i) { } else { check.clear(); } - if(i.version >= INNO_VERSION(4, 0, 0) || (i.version.is_isx() && i.version >= INNO_VERSION(3, 0, 3))) { + if(i.version >= INNO_VERSION(6, 7, 0)) { + // Inno Setup 6.7.0 narrowed TSetupTaskEntry.Level from Integer + // (4 bytes) to Byte (1 byte). See Projects/Src/Shared.Struct.pas + // at tag is-6_7_0 in https://github.com/jrsoftware/issrc. + level = util::load(is); + } else if(i.version >= INNO_VERSION(4, 0, 0) || (i.version.is_isx() && i.version >= INNO_VERSION(3, 0, 3))) { level = util::load(is); } else { level = 0; diff --git a/src/setup/version.cpp b/src/setup/version.cpp index 4aadc569..01582aa6 100644 --- a/src/setup/version.cpp +++ b/src/setup/version.cpp @@ -192,6 +192,7 @@ const known_version versions[] = { { "Inno Setup Setup Data (6.5.2)", INNO_VERSION_EXT(6, 5, 2, 0), version::Unicode }, { "Inno Setup Setup Data (6.6.0)", INNO_VERSION_EXT(6, 6, 0, 0), version::Unicode }, { "Inno Setup Setup Data (6.6.1)", INNO_VERSION_EXT(6, 6, 1, 0), version::Unicode }, + { "Inno Setup Setup Data (6.7.0)", INNO_VERSION_EXT(6, 7, 0, 0), version::Unicode }, }; } // anonymous namespace