Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
matrix:
rust: [stable, "1.88"]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.rust }}
Expand All @@ -27,7 +27,7 @@ jobs:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
Expand All @@ -37,7 +37,7 @@ jobs:
name: Format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
Expand All @@ -47,7 +47,7 @@ jobs:
name: Docs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- run: cargo doc --no-deps --all-features
env:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
contents: read
id-token: write # required to mint the crates.io OIDC token
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
with:
fetch-depth: 0 # merge-base needs real history

Expand Down
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,29 @@ All notable changes to this project are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.1.4] - 2026-09-18

### Changed

- LZMA1 and LZMA2 decoding now uses `lzma-rust2` instead of `lzma-rs` (about 1.4x faster
on LZMA1, still pure Rust), and `flate2` uses the `zlib-rs` backend instead of
`miniz_oxide`. The LZMA2 dictionary size is taken from the property byte Inno writes
ahead of each chunk, so installers built with large dictionaries (for example
`lzma2/ultra64`) decode correctly. `lzma-rs` remains a dev-dependency for test fixtures.
Contributed by @maboloshi in #3.
- Replaced em-dashes and en-dashes with plain ASCII hyphens throughout rustdoc comments,
the README, and test-sample notes, so generated documentation renders consistently.
This also touches a few runtime strings: the one-line summaries returned by
`inno_api_description` and the section banners printed by the `dump` example.
- Raised the `pascalscript` minimum to 0.1.3 (documentation-only release upstream).
- Refreshed dependencies and raised the manifest minimums to the current releases:
`bitflags` 2.13.2, `chacha20` 0.10.2, `codepage` 0.1.3, `encoding_rs` 0.8.41,
`flate2` 1.1.10.
- Updated CI and publish workflows from `actions/checkout@v4` to `actions/checkout@v7`.
- UTF-16LE decoding now pairs bytes with `as_chunks::<2>()` instead of `chunks_exact(2)`
plus a slice copy, which clears the new `chunks_exact_to_as_chunks` lint on current
stable Clippy. Also dropped a test import that newer compilers report as unused.

## [0.1.3] - 2026-08-09

### Fixed
Expand Down Expand Up @@ -58,6 +81,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Initial published release.

[0.1.4]: https://github.com/ATRAPSLLC/innospect/compare/v0.1.3...v0.1.4
[0.1.3]: https://github.com/ATRAPSLLC/innospect/compare/v0.1.2...v0.1.3
[0.1.2]: https://github.com/ATRAPSLLC/innospect/compare/v0.1.1...v0.1.2
[0.1.1]: https://github.com/ATRAPSLLC/innospect/releases/tag/v0.1.1
14 changes: 7 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "innospect"
version = "0.1.3"
version = "0.1.4"
edition = "2024"
rust-version = "1.88"
description = "Parse and inspect Inno Setup installer binaries"
Expand Down Expand Up @@ -28,20 +28,20 @@ arithmetic_side_effects = "deny"
indexing_slicing = "deny"

[dependencies]
bitflags = "2.13.0"
bitflags = "2.13.2"
goblin = "0.10.7"
encoding_rs = "0.8.35"
codepage = "0.1.2"
flate2 = { version = "1.1.9", default-features = false, features = ["zlib-rs"] }
encoding_rs = "0.8.41"
codepage = "0.1.3"
flate2 = { version = "1.1.10", default-features = false, features = ["zlib-rs"] }
lzma-rust2 = { version = "0.18", default-features = false, features = ["std", "optimization"] }
bzip2 = "0.6.1"
sha1 = "0.11.0"
sha2 = "0.11.0"
md-5 = "0.11.0"
pbkdf2 = { version = "0.13.0", default-features = false }
hmac = "0.13.0"
chacha20 = { version = "0.10.1", features = ["xchacha"] }
pascalscript = "0.1.2"
chacha20 = { version = "0.10.2", features = ["xchacha"] }
pascalscript = "0.1.3"

[dev-dependencies]
lzma-rs = "0.3"
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
A pure-Rust parser for [Inno Setup](https://jrsoftware.org/isinfo.php) installer
binaries. Provides typed access to the loader stub overlay, setup headers, every
typed record stream, and on-demand file extraction across Inno Setup 5.0 through
the 7.x preview series — including the modern XChaCha20 / `euFiles` / `euFull`
the 7.x preview series - including the modern XChaCha20 / `euFiles` / `euFull`
encryption modes and the legacy ARC4 + SHA-1 / MD5 password-verifier path.

Built for **malware analysis** and **reverse engineering**. The crate is
Expand All @@ -16,31 +16,31 @@ arithmetic-with-side-effects are all denied at the lib root.

## Features

- **PE locator** — both the modern signature-scan path (5.1.5+) and the legacy
- **PE locator** - both the modern signature-scan path (5.1.5+) and the legacy
`0x30` file-offset pointer used by pre-5.1.5 installers; tolerates BlackBox /
GOG re-packagers.
- **Setup-0 decompression** — Stored / Zlib / LZMA1 / LZMA2 across every
- **Setup-0 decompression** - Stored / Zlib / LZMA1 / LZMA2 across every
documented version cutover, with the standalone-encryption-header path for
6.5+ and the inline-verifier path for 6.4.x.
- **Setup-1 file extraction** — solid-LZMA chunks with per-file BCJ inverse
- **Setup-1 file extraction** - solid-LZMA chunks with per-file BCJ inverse
filtering (4108 / 5200 / 5.3.9-flip variants) and SHA-256 / SHA-1 / MD5 /
CRC-32 / Adler-32 checksum verification at EOF.
- **Per-version `TSetupHeader`** — every `String` / `AnsiString` / count / tail
- **Per-version `TSetupHeader`** - every `String` / `AnsiString` / count / tail
field across the 1.x..7.x history, with `[Files]` / `[Run]` / `[Icons]` /
`[Registry]` / `[INI]` / `[Components]` / `[Tasks]` / `[Types]` / `[Languages]`
/ `[CustomMessages]` / `[Permissions]` records typed individually.
- **Encryption** — XChaCha20 with PBKDF2-SHA-256 key derivation (Inno 6.4+);
- **Encryption** - XChaCha20 with PBKDF2-SHA-256 key derivation (Inno 6.4+);
ARC4 with salted SHA-1 (5.3.9..6.4) or MD5 (4.2..5.3.9); CRC32 verifier
(pre-4.2). Password trial via `from_bytes_with_passwords`.
- **Embedded `[Code]` / IFPS** — re-exports the
- **Embedded `[Code]` / IFPS** - re-exports the
[`pascalscript`](https://crates.io/crates/pascalscript) container parser; the
`inno_api_description` table maps Inno-runtime imports
(`RegWriteStringValue`, `Exec`, `ShellExec`, …) to one-line summaries for
triage.
- **Analysis API** — `exec_commands()` / `registry_ops()` / `shortcuts()` walk
- **Analysis API** - `exec_commands()` / `registry_ops()` / `shortcuts()` walk
the relevant record streams and tag each entry with install-vs-uninstall
phase, registry-write classification, or icon-target resolution.
- **Uninstaller reconstruction** — `extract_uninstaller()` patches the loader
- **Uninstaller reconstruction** - `extract_uninstaller()` patches the loader
stub bytes back to the canonical `InUn` form.

## Quick start
Expand Down
4 changes: 2 additions & 2 deletions examples/dump.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ fn main() {

println!();
println!(
"== Records (3c lightweight) — languages={} messages={} permissions={} types={} components={} tasks={} ==",
"== Records (3c lightweight) - languages={} messages={} permissions={} types={} components={} tasks={} ==",
installer.languages().len(),
installer.messages().len(),
installer.permissions().len(),
Expand Down Expand Up @@ -204,7 +204,7 @@ fn main() {

println!();
println!(
"== Records (3d heavy) — dirs={} files={} icons={} ini={} reg={} ins_del={} unins_del={} run={} unins_run={} file_loc={} ==",
"== Records (3d heavy) - dirs={} files={} icons={} ini={} reg={} ins_del={} unins_del={} run={} unins_run={} file_loc={} ==",
installer.directories().len(),
installer.files().len(),
installer.icons().len(),
Expand Down
20 changes: 10 additions & 10 deletions src/analysis/compiledcode/innoapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,23 @@
//!
//! Inno's `Setup.exe` registers ~200 functions / procedures that
//! `[Code]` scripts can call. The canonical inventory lives in
//! `research/issrc/Projects/Src/Shared.ScriptFunc.pas` —
//! `research/issrc/Projects/Src/Shared.ScriptFunc.pas` -
//! `ScriptFuncTables[sftScriptDlg]`, `[sftCommonFunc]`,
//! `[sftPathFunc]`, `[sftInstall]`, `[sftInstFunc]`,
//! `[sftMainFunc]`, etc. — plus the `DelphiScriptFuncTable` of
//! `[sftMainFunc]`, etc. - plus the `DelphiScriptFuncTable` of
//! pre-registered runtime helpers.
//!
//! This table covers the security-relevant subset analysts care
//! about most: registry mutations, command execution, file
//! operations, network downloads, and privilege checks. It is
//! curated rather than exhaustive — adding remaining categories
//! curated rather than exhaustive - adding remaining categories
//! is mechanical (transcribe the names + write one-liner
//! descriptions) and tracked separately. Lookups are by exact
//! ASCII name, case-sensitive (PascalScript identifiers
//! collide-detect on hash, but the wire form preserves the
//! author's casing).

/// One row of the lookup table — name + short, single-line
/// One row of the lookup table - name + short, single-line
/// description.
type Row = (&'static str, &'static str);

Expand Down Expand Up @@ -104,7 +104,7 @@ pub const INNO_API: &[Row] = &[
),
(
"ShellExec",
"Wraps `ShellExecuteEx` — opens a file or URL via the shell's verb dispatcher.",
"Wraps `ShellExecuteEx` - opens a file or URL via the shell's verb dispatcher.",
),
(
"ShellExecAsOriginalUser",
Expand Down Expand Up @@ -159,11 +159,11 @@ pub const INNO_API: &[Row] = &[
),
(
"DownloadTemporaryFileSize",
"HEAD request — returns the Content-Length of a remote resource.",
"HEAD request - returns the Content-Length of a remote resource.",
),
(
"DownloadTemporaryFileDate",
"HEAD request — returns the Last-Modified header of a remote resource.",
"HEAD request - returns the Last-Modified header of a remote resource.",
),
(
"SetDownloadCredentials",
Expand Down Expand Up @@ -270,21 +270,21 @@ pub const INNO_API: &[Row] = &[
),
(
"WizardForm",
"Returns the running `TWizardForm` instance — exposes the live install UI.",
"Returns the running `TWizardForm` instance - exposes the live install UI.",
),
];

/// Looks up `name` in [`INNO_API`] via binary search and returns
/// the short description if found.
///
/// Returns `None` for names not in the table — this includes both
/// Returns `None` for names not in the table - this includes both
/// genuinely-unknown imports and the long tail of registered
/// helpers that aren't worth a description for analyst purposes
/// (sorting, string-mangling, math, etc.).
pub fn inno_api_description(name: &str) -> Option<&'static str> {
// Lookup is **case-insensitive ASCII**. PascalScript hashes
// identifiers via `MakeHash` (`uPSUtils.pas:701-708`), which
// is itself case-sensitive — but Inno's own registration path
// is itself case-sensitive - but Inno's own registration path
// upper-cases names before they hit the wire, so a `[Code]`
// import for `ShellExec` materializes as `SHELLEXEC`. The
// table is hand-keyed mixed-case for readability; the
Expand Down
4 changes: 2 additions & 2 deletions src/analysis/compiledcode/mod.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
//! Inno-side wrapper for the embedded `[Code]` PascalScript blob.
//!
//! The structural parser lives in the standalone
//! [`pascalscript`] crate (sibling repo at `../pascalscript/`) —
//! [`pascalscript`] crate (sibling repo at `../pascalscript/`) -
//! fully host-agnostic, since RemObjects PascalScript III is
//! used by many Delphi applications beyond Inno Setup. This
//! module re-exports the parser's [`Container`] type for
//! analyst convenience and adds one Inno-specific helper:
//! [`inno_api_description`] — a table mapping
//! [`inno_api_description`] - a table mapping
//! Inno's runtime-registered PascalScript externals (the names a
//! `[Code]` script can call) to short descriptions.

Expand Down
6 changes: 3 additions & 3 deletions src/analysis/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ use crate::records::run::RunEntry;
/// Whether a command runs at install time or at uninstall time.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ExecPhase {
/// `[Run]` — fires after files are copied.
/// `[Run]` - fires after files are copied.
Install,
/// `[UninstallRun]` — fires before files are removed.
/// `[UninstallRun]` - fires before files are removed.
Uninstall,
}

Expand All @@ -35,7 +35,7 @@ pub struct ExecCommand<'a> {
}

impl<'a> ExecCommand<'a> {
/// Returns the `Filename:` directive — the executable, batch
/// Returns the `Filename:` directive - the executable, batch
/// file, or shell URL to invoke. Inno calls this field `Name`
/// internally; the analyst-friendly view exposes it as
/// `filename` to match the `[Run]` directive name.
Expand Down
8 changes: 4 additions & 4 deletions src/analysis/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
//! parsed records. The submodules here re-shape those records into
//! analyst-friendly views:
//!
//! - [`exec`] — install + uninstall command execution.
//! - [`registryop`] — registry mutations classified by operation
//! - [`exec`] - install + uninstall command execution.
//! - [`registryop`] - registry mutations classified by operation
//! kind.
//! - [`shortcut`] — `[Icons]` entries joined to their `[Files]`
//! - [`shortcut`] - `[Icons]` entries joined to their `[Files]`
//! targets where possible.
//! - [`compiledcode`] — IFPS container fingerprint for the
//! - [`compiledcode`] - IFPS container fingerprint for the
//! compiled `[Code]` blob (header-only at the moment).
//!
//! Every view here borrows from the underlying record or buffer;
Expand Down
4 changes: 2 additions & 2 deletions src/analysis/shortcut.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
//!
//! Inno's `[Icons]` directive declares shortcut metadata
//! (`Filename:`, `Parameters:`, `WorkingDir:`, `IconFile:`) but
//! doesn't itself materialize the binary the shortcut points at —
//! doesn't itself materialize the binary the shortcut points at -
//! that comes from a separate `[Files]` entry. For analyst code
//! that wants to know "which executables in this installer get a
//! shortcut?", the natural shape is the join.
Expand All @@ -24,7 +24,7 @@ pub struct Shortcut<'a> {
/// The icon entry that defines the shortcut.
pub icon: &'a IconEntry,
/// The matched [`FileEntry`] whose `destination` equals
/// `icon.filename`, or `None` if no `[Files]` entry matches —
/// `icon.filename`, or `None` if no `[Files]` entry matches -
/// e.g. system-path shortcuts (`{sys}\notepad.exe`), URLs, or
/// shortcuts to files installed by an `[Code]` script.
pub target: Option<&'a FileEntry>,
Expand Down
6 changes: 3 additions & 3 deletions src/crypto/arc4.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
//! in a separate crate because:
//!
//! - The algorithm is tiny and trivially auditable.
//! - It's only used for legacy chunk decryption — no other crate
//! - It's only used for legacy chunk decryption - no other crate
//! in our dep graph wants it.
//! - Innoextract takes the same approach (`research/src/crypto/arc4.cpp`).
//!
Expand Down Expand Up @@ -64,13 +64,13 @@ impl Rc4 {

/// Advances the keystream by `n` bytes without applying them
/// to any buffer. Inno's pre-6.4 chunk decryption discards the
/// first 1000 bytes of the keystream after KSA — biased early
/// first 1000 bytes of the keystream after KSA - biased early
/// bytes were the WEP attack surface, and ISCrypt.dll mirrors
/// the standard RC4-drop hardening (`research/src/stream/chunk.cpp:72`,
/// `arc4.discard(1000)`). Skipping the discard produces a
/// keystream that XORs cleanly through password verification
/// (which doesn't use the chunk RC4) but yields garbage at the
/// chunk-decrypt step — observable as malformed LZMA streams.
/// chunk-decrypt step - observable as malformed LZMA streams.
pub(crate) fn discard(&mut self, n: usize) {
for _ in 0..n {
self.i = self.i.wrapping_add(1);
Expand Down
Loading
Loading