diff --git a/.agents/skills/reverse-engineer-anything/SKILL.md b/.agents/skills/reverse-engineer-anything/SKILL.md
index e283e35b62..ef06652078 100644
--- a/.agents/skills/reverse-engineer-anything/SKILL.md
+++ b/.agents/skills/reverse-engineer-anything/SKILL.md
@@ -91,9 +91,15 @@ explicit path or endpoint and do not need it.
- ASAR or extracted JavaScript/Electron tree:
`analyze_javascript_application`.
-- Archive/package member inventory (ZIP/APK/IPA/MSIX/AppX or DMG):
- `open_binary` with the supplied local path; use `inspect_artifact` when its
- graph and findings help answer the question.
+- Archive/package member inventory (ZIP/APK/IPA/MSIX/AppX, DMG, or a PyInstaller
+ executable): `open_binary` with the supplied local path; use `inspect_artifact`
+ when its graph and findings help answer the question. A PyInstaller CArchive
+ is the application layer; the bootloader is only the native prefix. Start with
+ `detail: "summary"` for large inventories, then use retained inventory views
+ for selected member fields/pages. A complete small inventory can cost less
+ than extra calls and schemas. Runtime options are metadata, not files.
+ Compact delivery still inventories and hashes members. See the
+ [Python packaged applications guide](https://github.com/morluto/rea/blob/main/docs/python-packaged-applications.md).
- Apple app plist, Interface Builder, asset catalog, or keyed archive resources:
`open_bundle_resources`, then the selected resource tool. This does not
require `CFBundleExecutable` or a deep provider. Resource sessions have no
diff --git a/README.md b/README.md
index a4da4bb818..ff4777a55b 100644
--- a/README.md
+++ b/README.md
@@ -174,35 +174,36 @@ CLI commands use the same workflows.
REA requires Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+, plus npm.
Additional tools and host support depend on the target:
-| Target | What REA returns | Requirements and guide |
-| ---------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
-| Desktop installations | Runtime candidates, supporting files, component markers and next probes | Built-in bounded directory/header inspection; [desktop applications](docs/desktop-applications.md) |
-| Native binaries | Pseudocode, assembly, strings, symbols, calls and references | Hopper, Ghidra or IDA; [native analysis](docs/native-investigation.md) |
-| Offline ELF layout | Sections, segments, original symbols/relocations and static mitigation candidates | Caller-supplied pwntools on Linux x64; [binary diagnostics](docs/binary-diagnostics.md) |
-| Go binaries | Embedded compiler version, modules, replacements and build settings | Built-in static ELF/PE/thin Mach-O inspection; no Go installation required |
-| Model artifacts | GGUF / safetensors metadata, tensor declarations and derived totals | Built-in header inspection; optional full-file hash; [model files](docs/model-artifacts.md) |
-| IL2CPP metadata | Unity global-metadata.dat assembly images, type and method names and index ranges | Built-in static decoding of versions 24, 27, 29, 31 and 39; no Unity required |
-| Godot PCK | Archive inventory and extraction, original header/directory facts and selected content identities | Built-in static formats 2, 3 and 4; no Godot required |
-| Rust binaries | Embedded cargo-auditable crate names, versions, sources and dependency edges | Built-in static ELF/PE/thin Mach-O inspection; [Rust binaries](docs/rust-binaries.md) |
-| WASM artifact | Validation, sections, exact import/export WAT forms and decoded WAT | Caller-supplied WABT 1.0.42; `inspect_wasm_artifact` |
-| EVM bytecode | Dispatch selectors, byte offsets, inferred arguments and mutability | Local raw/hex carrier on Linux x64; [offline EVM guide](docs/evm-bytecode.md) |
-| SQLite databases | Tables, columns, indexes, views, triggers and selected ordinary-table rows | Local database/WAL snapshot; Node.js 24.x >=24.15 or 26+ with native SQLite limits |
-| Recorded Linux crashes | Raw notes, every recorded thread's registers/signals and optional mapping candidates | Caller-supplied pwntools on Linux x64; optional GDB/pwndbg; [recorded crashes](docs/recorded-crashes.md) |
-| JavaScript / Electron | Modules, imports, source maps, routes, IPC and native add-on relationships | Node.js and npm; Linux: procps-compatible `ps`; [application analysis](docs/javascript-application-workflows.md) |
-| Websites | Page structure, scripts, network observations and requested screenshots | A Chrome-family browser; [browser analysis](docs/browser-observation.md) |
-| Saved network captures | Requests, responses, exposed payloads and source locations | HAR or Fiddler Everywhere SAZ; mitmdump on Linux for native mitmproxy; [capture guide](docs/web-network-captures.md) |
-| .NET assemblies | Metadata, CIL instructions, declared native dependencies and build comparisons | None (static only); [managed-code guide](docs/managed-code-analysis.md) |
-| Android APKs | Manifest declarations, classes, decompiled methods and references | Headless JADX and a full JDK on Linux/macOS/Windows x64; [Android guide](docs/android-analysis.md) |
-| Android devices | Devices, packages, processes, logs, dumps, screen captures, and APK/file transfers | Caller-supplied adb (emulator or hardware); [ADB guide](docs/adb-device-analysis.md) |
-| JEB projects | Engine-backed project units, artifact digests and type/method pseudo-code | A caller-started JEB client serving MCP, selected with REA_JEB_MCP_URL; [JEB guide](docs/jeb-analysis.md) |
-| Android resources | Decoded manifest, string tables, locales and version facts | Caller-supplied Apktool launcher; [resource guide](docs/apktool-resource-analysis.md) |
-| Flutter apps | Dart snapshot hash, per-ABI library digests and engine build provenance | None (static APK parsing); [Flutter guide](docs/flutter-analysis.md) |
-| Firmware | Regions, extraction results and native-analysis handoffs | Binwalk / Unblob on Linux; [firmware guide](docs/firmware-analysis.md) |
-| Recorded packets | Selected fields, repeated values, filters and decode-as rules | Linux: TShark 4.x + prlimit; [capture guide](docs/packet-captures.md) |
-| Packet summaries | Endpoints, DNS, TLS SNI, protocol distribution and HTTP requests | Caller-supplied tshark and capinfos (Linux-verified); [pcap guide](docs/pcap-analysis.md) |
-| Recorded logic signals | Decoder annotations, sample ranges and diagnostics | Linux: sigrok-cli 0.7.2 + prlimit; [signal recordings](docs/signal-recordings.md) |
-| Packages and resources | File inventories, digests, plists, Apple bundle anatomy and extracted resources | [Artifact and JavaScript guide](docs/javascript-artifact-reconstruction.md), [Apple applications](docs/apple-application-analysis.md) |
-| Process behavior | Terminal output, interactions, exit and filesystem observations, and run comparisons | Linux/macOS with a native PTY; [process capture](docs/process-capture.md) |
+| Target | What REA returns | Requirements and guide |
+| ----------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
+| Desktop installations | Runtime candidates, supporting files, component markers and next probes | Built-in bounded directory/header inspection; [desktop applications](docs/desktop-applications.md) |
+| Native binaries | Pseudocode, assembly, strings, symbols, calls and references | Hopper, Ghidra or IDA; [native analysis](docs/native-investigation.md) |
+| Offline ELF layout | Sections, segments, original symbols/relocations and static mitigation candidates | Caller-supplied pwntools on Linux x64; [binary diagnostics](docs/binary-diagnostics.md) |
+| Go binaries | Embedded compiler version, modules, replacements and build settings | Built-in static ELF/PE/thin Mach-O inspection; no Go installation required |
+| Model artifacts | GGUF / safetensors metadata, tensor declarations and derived totals | Built-in header inspection; optional full-file hash; [model files](docs/model-artifacts.md) |
+| IL2CPP metadata | Unity global-metadata.dat assembly images, type and method names and index ranges | Built-in static decoding of versions 24, 27, 29, 31 and 39; no Unity required |
+| Godot PCK | Archive inventory and extraction, original header/directory facts and selected content identities | Built-in static formats 2, 3 and 4; no Godot required |
+| Rust binaries | Embedded cargo-auditable crate names, versions, sources and dependency edges | Built-in static ELF/PE/thin Mach-O inspection; [Rust binaries](docs/rust-binaries.md) |
+| WASM artifact | Validation, sections, exact import/export WAT forms and decoded WAT | Caller-supplied WABT 1.0.42; `inspect_wasm_artifact` |
+| EVM bytecode | Dispatch selectors, byte offsets, inferred arguments and mutability | Local raw/hex carrier on Linux x64; [offline EVM guide](docs/evm-bytecode.md) |
+| SQLite databases | Tables, columns, indexes, views, triggers and selected ordinary-table rows | Local database/WAL snapshot; Node.js 24.x >=24.15 or 26+ with native SQLite limits |
+| Recorded Linux crashes | Raw notes, every recorded thread's registers/signals and optional mapping candidates | Caller-supplied pwntools on Linux x64; optional GDB/pwndbg; [recorded crashes](docs/recorded-crashes.md) |
+| JavaScript / Electron | Modules, imports, source maps, routes, IPC and native add-on relationships | Node.js and npm; Linux: procps-compatible `ps`; [application analysis](docs/javascript-application-workflows.md) |
+| Websites | Page structure, scripts, network observations and requested screenshots | A Chrome-family browser; [browser analysis](docs/browser-observation.md) |
+| Saved network captures | Requests, responses, exposed payloads and source locations | HAR or Fiddler Everywhere SAZ; mitmdump on Linux for native mitmproxy; [capture guide](docs/web-network-captures.md) |
+| .NET assemblies | Metadata, CIL instructions, declared native dependencies and build comparisons | None (static only); [managed-code guide](docs/managed-code-analysis.md) |
+| Android APKs | Manifest declarations, classes, decompiled methods and references | Headless JADX and a full JDK on Linux/macOS/Windows x64; [Android guide](docs/android-analysis.md) |
+| Android devices | Devices, packages, processes, logs, dumps, screen captures, and APK/file transfers | Caller-supplied adb (emulator or hardware); [ADB guide](docs/adb-device-analysis.md) |
+| JEB projects | Engine-backed project units, artifact digests and type/method pseudo-code | A caller-started JEB client serving MCP, selected with REA_JEB_MCP_URL; [JEB guide](docs/jeb-analysis.md) |
+| Android resources | Decoded manifest, string tables, locales and version facts | Caller-supplied Apktool launcher; [resource guide](docs/apktool-resource-analysis.md) |
+| Flutter apps | Dart snapshot hash, per-ABI library digests and engine build provenance | None (static APK parsing); [Flutter guide](docs/flutter-analysis.md) |
+| Firmware | Regions, extraction results and native-analysis handoffs | Binwalk / Unblob on Linux; [firmware guide](docs/firmware-analysis.md) |
+| Recorded packets | Selected fields, repeated values, filters and decode-as rules | Linux: TShark 4.x + prlimit; [capture guide](docs/packet-captures.md) |
+| Packet summaries | Endpoints, DNS, TLS SNI, protocol distribution and HTTP requests | Caller-supplied tshark and capinfos (Linux-verified); [pcap guide](docs/pcap-analysis.md) |
+| Recorded logic signals | Decoder annotations, sample ranges and diagnostics | Linux: sigrok-cli 0.7.2 + prlimit; [signal recordings](docs/signal-recordings.md) |
+| Packages and resources | File inventories, digests, plists, Apple bundle anatomy and extracted resources | [Artifact and JavaScript guide](docs/javascript-artifact-reconstruction.md), [Apple applications](docs/apple-application-analysis.md) |
+| Process behavior | Terminal output, interactions, exit and filesystem observations, and run comparisons | Linux/macOS with a native PTY; [process capture](docs/process-capture.md) |
+| PyInstaller executables | CArchive members, Python version, entry scripts and extracted bytecode | None (static only); [Python packaged applications](docs/python-packaged-applications.md) |
Static JavaScript and .NET inspection read the supplied files without running
the application. Runtime capture runs or interacts with the selected target
diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
index 561b9f2213..86d825c13d 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -50,6 +50,10 @@ export default defineConfig({
{ text: "Electron fuses", link: "/electron-fuses" },
{ text: "Process capture", link: "/process-capture" },
{ text: ".NET assemblies", link: "/managed-code-analysis" },
+ {
+ text: "Python packaged applications",
+ link: "/python-packaged-applications",
+ },
{ text: "Android applications", link: "/android-analysis" },
{ text: "Apple applications", link: "/apple-application-analysis" },
{ text: "Desktop installations", link: "/desktop-applications" },
diff --git a/docs/index.md b/docs/index.md
index 74c72ef059..070b8aad51 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -47,19 +47,19 @@ For runtime instrumentation, see the [Frida guide](./frida-instrumentation.md).
## Choose a guide
-| Task | Guides |
-| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| Identify a desktop installation | [Runtime candidates, evidence and follow-up files](desktop-applications.md) |
-| Inspect native functions, references, or types | [Native binaries](native-investigation.md), [Go metadata](go-binaries.md), [Rust dependencies](rust-binaries.md), [offline ELF layout](binary-diagnostics.md), [recorded crashes](recorded-crashes.md) |
-| Select a native provider or import format | [Ghidra semantics](ghidra-provider.md), [Windows](windows-ghidra-p0.md), [DOS](ghidra-dos.md), [raw bytes](raw-binary.md), [MIPS](ghidra-mips.md), [PSP](ghidra-psp.md), [NativeAOT](ghidra-nativeaot.md), [IDA](ida-provider.md), [Kuna](kuna-provider.md), [JEB](jeb-analysis.md) |
-| Analyze JavaScript or Electron artifacts | [Static analysis](javascript-artifact-reconstruction.md), [tracing and comparisons](javascript-application-workflows.md), [application graph](javascript-application-graph.md), [source recovery](javascript-recovery.md) |
-| Compare static JavaScript with runtime observations | [Runtime attribution](javascript-runtime-observation.md), [reconciliation](javascript-runtime-reconciliation.md), [Electron observation](electron-observation.md), [Electron fuses](electron-fuses.md) |
-| Inspect assemblies, packages, or firmware | [Godot PCK](godot-pck.md), [.NET](managed-code-analysis.md), [Android](android-analysis.md), [Apktool resources](apktool-resource-analysis.md), [ADB acquisition](adb-device-analysis.md), [Apple](apple-application-analysis.md), [Harmony](harmony-analysis.md), [firmware](firmware-analysis.md), [WASM](wasm-artifacts.md), [EVM](evm-bytecode.md) |
-| Observe or compare a browser scenario | [CDP observation](browser-observation.md), [scenarios](browser-scenario-contract.md), [runtime evidence](web-runtime.md), [historical captures](web-network-captures.md) |
-| Serve recorded behavior as an agent environment | [Agent environments](agent-environments.md) |
-| Trace captured website source | [Source locations](web-source-location.md), [module traces](website-module-trace.md), [script export](website-script-export.md) |
-| Capture and compare a command | [Process capture](process-capture.md) |
-| Use CLI/MCP and retain evidence | [CLI](cli.md), [MCP contracts](mcp-contracts.md), [agent prompts](mcp-prompts.md), [obligation ledgers](reconstruction-obligation-ledgers.md) |
+| Task | Guides |
+| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Identify a desktop installation | [Runtime candidates, evidence and follow-up files](desktop-applications.md) |
+| Inspect native functions, references, or types | [Native binaries](native-investigation.md), [Go metadata](go-binaries.md), [Rust dependencies](rust-binaries.md), [offline ELF layout](binary-diagnostics.md), [recorded crashes](recorded-crashes.md) |
+| Select a native provider or import format | [Ghidra semantics](ghidra-provider.md), [Windows](windows-ghidra-p0.md), [DOS](ghidra-dos.md), [raw bytes](raw-binary.md), [MIPS](ghidra-mips.md), [PSP](ghidra-psp.md), [NativeAOT](ghidra-nativeaot.md), [IDA](ida-provider.md), [Kuna](kuna-provider.md), [JEB](jeb-analysis.md) |
+| Analyze JavaScript or Electron artifacts | [Static analysis](javascript-artifact-reconstruction.md), [tracing and comparisons](javascript-application-workflows.md), [application graph](javascript-application-graph.md), [source recovery](javascript-recovery.md) |
+| Compare static JavaScript with runtime observations | [Runtime attribution](javascript-runtime-observation.md), [reconciliation](javascript-runtime-reconciliation.md), [Electron observation](electron-observation.md), [Electron fuses](electron-fuses.md) |
+| Inspect assemblies, packages, or firmware | [Godot PCK](godot-pck.md), [.NET](managed-code-analysis.md), [Python packages](python-packaged-applications.md), [Android](android-analysis.md), [Apktool resources](apktool-resource-analysis.md), [ADB acquisition](adb-device-analysis.md), [Apple](apple-application-analysis.md), [Harmony](harmony-analysis.md), [firmware](firmware-analysis.md), [WASM](wasm-artifacts.md), [EVM](evm-bytecode.md) |
+| Observe or compare a browser scenario | [CDP observation](browser-observation.md), [scenarios](browser-scenario-contract.md), [runtime evidence](web-runtime.md), [historical captures](web-network-captures.md) |
+| Serve recorded behavior as an agent environment | [Agent environments](agent-environments.md) |
+| Trace captured website source | [Source locations](web-source-location.md), [module traces](website-module-trace.md), [script export](website-script-export.md) |
+| Capture and compare a command | [Process capture](process-capture.md) |
+| Use CLI/MCP and retain evidence | [CLI](cli.md), [MCP contracts](mcp-contracts.md), [agent prompts](mcp-prompts.md), [obligation ledgers](reconstruction-obligation-ledgers.md) |
For development, use [testing](testing.md), [client verification](client-verification.md),
[tool design](tool-design.md), and [releasing](releasing.md).
diff --git a/docs/python-packaged-applications.md b/docs/python-packaged-applications.md
new file mode 100644
index 0000000000..5785def1fb
--- /dev/null
+++ b/docs/python-packaged-applications.md
@@ -0,0 +1,128 @@
+# Python packaged applications
+
+REA statically inventories PyInstaller executables with an appended CArchive.
+`inspect_artifact` and `extract_artifact` share the same reader through CLI and
+MCP. They read archive bytes without launching the application. Bytecode
+source decompilation, encrypted PYZ recovery, zipapp and PEX analysis are outside
+this capability.
+
+## Facts and focused queries
+
+The root occurrence's `format_metadata` records the CArchive layout (2.0 or
+2.1), archive and cookie offsets, table extent, entry count, raw Python version,
+normalized major/minor when known, library name and encryption marker. Member
+metadata retains the original name, table index, type, compression flag,
+relative offset and known directory record extent. Logical paths use archive
+path rules; the source name retains its spelling, including backslashes.
+`relative_offset` is relative to the CArchive start for CArchive rows, or to
+the decoded PYZ image for PYZ rows. A CArchive directory record starts at
+`archive_start + toc_offset + directory_record_offset`; its size is
+`directory_record_size`. PYZ row directory extents remain unknown.
+
+Namespace packages remain directories. Their `relative_offset` and
+`compressed_size` preserve any payload declared by older producers, while
+`declared_size` is zero because no bytecode is materialized for a namespace.
+Every declared span must end before the PYZ table of contents.
+PYZ `compression_flag` is derived from storage: nonempty payloads are compressed,
+including legacy namespaces; an empty namespace has no compressed payload.
+
+CArchive type `o` entries are runtime options. They are metadata occurrences,
+with no content artifact or extractable file. Their query paths are
+`__pyinstaller__/runtime-options/
`; the option text is
+`format_metadata.source_name`. Repeated declarations retain distinct occurrence
+identities. These paths do not claim output filesystem names.
+
+Type `d` entries are multipackage dependencies. They are also metadata, under
+`__pyinstaller__/dependencies/
`, and never materialize an empty file.
+The reader preserves the raw reference and splits its first colon into
+`dependency_archive` and `dependency_member`. `dependency_status` remains
+`unresolved`, or `invalid-reference` with unknown parts for malformed references.
+`external_dependency_count` summarizes these declarations. REA does not open
+another executable or claim its contents are available. A real file with the
+same logical path as metadata remains independently extractable.
+Both declaration types require empty payloads and no compression. Their query
+paths do not participate in filesystem collision checks, directory ancestry or
+source-map target lookup; their parent identifies the declaring container.
+Complete authenticated inventory page sets preserve producer identities in any
+merge order, including metadata/file aliases. Malformed TOC diagnostics name the
+table index; original names remain available in the supplied artifact.
+Path failures use bounded escaped previews and report the original length;
+accepted paths and retained source names are not shortened.
+
+Request `detail: "summary"` for a compact inspection, then query retained
+Evidence with `inspect_analysis_view`. An inventory view can select
+`collection: "occurrences"`, `entry_kinds: ["metadata"]`, exact logical paths or
+occurrence IDs, requested `fields`, and `offset`/`limit`. Filters intersect;
+identities, parent Evidence and coverage survive projection. Full inventory
+Evidence remains available for export. A compact reply reduces returned context;
+it still performs the complete member inventory and hashing work.
+Focused views add calls and tool-schema costs. A complete small inventory can
+cost less overall; prefer compact delivery when the full member graph would
+overwhelm the answer.
+
+```bash
+rea inspect-artifact /absolute/path/hello --detail summary --evidence_output /absolute/new/evidence.json --json
+rea extract-artifact /absolute/path/hello --json
+```
+
+## Decoding and evidence limits
+
+Inventory hashes members individually through bounded streams. Stored PYZ
+images use descriptor ranges; outer-compressed images stream to private files,
+which the reader owns through cleanup and any cleanup retry. Extraction binds
+the selected carrier to its inventoried digest and checks source identity and
+state before and after reading. It does not retain expanded module bodies.
+Opaque outer-compressed images reuse that verified private image when opened,
+instead of inflating the carrier again. Direct source spans describe stored
+CArchive bytes; decompressed content has no direct span even when its encoded
+and decoded sizes coincide. Original offsets and compression remain metadata.
+
+Only the PYZ header and marshal directory are loaded for listing. PYZ tables
+have a 16 MiB budget; CArchive tables have a 32 MiB budget. Parsed facts have a
+32 MiB serialized budget. Projected entries share a 64 MiB retained budget,
+including metadata, bookkeeping and conservative reservations for namespace
+objects and possible case-collision annotations at each path component. These
+reservations happen before normalization or trie allocation.
+Outer-compressed PYZ images have a separate per-reader capacity of 128 private
+files, bounding retained inodes and image identities. The complete image count
+is admitted before any temporary file is created; excess images return a
+capacity failure. This is a resource policy, not a PyInstaller format limit.
+Only a zero-byte read establishes EOF; short successful descriptor reads fill
+the bounded range. Compressed members must consume their complete declared
+input span; trailing junk and concatenated streams are refused.
+Plaintext expansion has an aggregate archive-derived budget. Exceeding a budget
+returns a resource failure.
+
+Supported final Python bytecode magic values cover Python 3.7 through 3.14.
+Supported marshal directory values are lists, tuples, strings, integers and
+bytes. An unsupported magic or directory stays an opaque member with a
+limitation and `pyz_status: "opaque"`; its module contents remain unknown.
+Any opaque PYZ makes producer traversal coverage unknown, including retained
+summaries and exhausted queries. Per-archive reasons remain on the opaque
+occurrences; the coverage observation summarizes their count without repeating
+every limitation. An empty result does not establish module absence.
+Observed `pyz_python_magic` bytes remain available even when their Python version
+is unknown.
+The directory declares compressed module lengths, so uncompressed declared
+sizes and source spans inside decoded data remain unknown.
+
+PYZ modules receive a derived 16-byte `.pyc` header from their own supported
+magic. CArchive bytecode receives that header only when every sibling PYZ magic
+and the cookie's Python version agree. Otherwise it retains its original bytes
+and `bytecode_header: "unknown"`. `bytecode_magic` and graph derivation edges
+identify synthesized headers; member digests cover the resulting derived files.
+These headers do not establish successful execution or decompilation.
+
+The nonempty CArchive Python module `pyimod00_crypto_key` (`m`/`M`) is recorded
+as an encryption marker; a same-named data file or script is not. Affected
+PYZ blobs are reported without decoding; extraction refuses encrypted entries.
+The raw PYZ container remains readable and hashable, including when its module
+table is opaque. Its marker stays in metadata and opaque child coverage stays
+unknown; extracting the container does not decrypt its payloads.
+The marker also applies to nonempty legacy namespace payloads. Empty namespaces
+have no payload to encrypt and retain a null encryption source.
+
+Detection searches the last 16 MiB for a cookie, allowing a trailing macOS code
+signature within that bound. Member offsets are relative to the archive start:
+cookie position plus cookie size minus package length. Native bootloader
+analysis remains available through the native tools.
diff --git a/docs/readme/README_ar.md b/docs/readme/README_ar.md
index 99e7614769..86d7943131 100644
--- a/docs/readme/README_ar.md
+++ b/docs/readme/README_ar.md
@@ -152,6 +152,7 @@ npx rea-agents@latest setup
| تسجيلات الشبكة المحفوظة | الطلبات، والاستجابات، ومحتويات الحمولة المتاحة، ومواضعها في المصدر | HAR؛ وmitmdump على Linux لتسجيلات mitmproxy بصيغتها الأصلية؛ [دليل تسجيلات الشبكة](../web-network-captures.md) |
| التقاط الحزم | نقاط النهاية وDNS وTLS SNI وتوزيع البروتوكولات وطلبات HTTP | tshark وcapinfos يوفّرهما المُستدعي؛ [دليل pcap](../pcap-analysis.md) |
| تجميعات .NET | البيانات الوصفية، وتعليمات CIL، واعتماديات الشيفرة الأصلية المعلنة، ومقارنات البناء | فحص ثابت؛ [دليل الشيفرة المُدارة](../managed-code-analysis.md) |
+| PyInstaller | CArchive, PYZ, Python bytecode | Static inspection; [Python packaged applications](../python-packaged-applications.md) |
| حزم Android APK | تصريحات ملف manifest، والأصناف، والدوال الناتجة عن فك الترجمة، والمراجع | JADX دون واجهة رسومية وJDK كامل على Linux/macOS/Windows x64؛ [دليل Android](../android-analysis.md) |
| أجهزة Android | الأجهزة والحزم والعمليات والسجلات والدامبات ولقطات الشاشة ونقل APK/الملفات | adb يوفّره المُستدعي (محاكي أو جهاز)؛ [دليل ADB](../adb-device-analysis.md) |
| مشاريع JEB | وحدات المشروع من المحرك وبصمات المصنّفات والكود الزائف للأنواع/الدوال | عميل JEB يبدأه المُستدعي ويقدّم MCP، ويُختار عبر REA_JEB_MCP_URL؛ [دليل JEB](../jeb-analysis.md) |
diff --git a/docs/readme/README_de.md b/docs/readme/README_de.md
index 42cb9eaec1..13c5a56592 100644
--- a/docs/readme/README_de.md
+++ b/docs/readme/README_de.md
@@ -152,6 +152,7 @@ REA benötigt Node.js 22.x (>=22.19), 24.x (>=24.11) oder 26+ sowie npm. Zusätz
| Gespeicherte Netzwerkaufzeichnungen | Anfragen, Antworten, zugängliche Nutzdaten und Quellpositionen | HAR; mitmdump unter Linux für Aufzeichnungen im nativen mitmproxy-Format; [Leitfaden zu Netzwerkaufzeichnungen](../web-network-captures.md) |
| Paketerfassungen | Endpunkte, DNS, TLS SNI, Protokollverteilung und HTTP-Anfragen | Vom Aufrufer bereitgestellte tshark- und capinfos-Binaries; [pcap-Leitfaden](../pcap-analysis.md) |
| .NET-Assemblies | Metadaten, CIL-Anweisungen, deklarierte native Abhängigkeiten und Build-Vergleiche | Statische Untersuchung; [Leitfaden für verwalteten Code](../managed-code-analysis.md) |
+| PyInstaller | CArchive, PYZ, Python bytecode | Static inspection; [Python packaged applications](../python-packaged-applications.md) |
| Android-APKs | Manifest-Deklarationen, Klassen, dekompilierte Methoden und Verweise | JADX ohne grafische Oberfläche und ein vollständiges JDK unter Linux/macOS/Windows x64; [Android-Leitfaden](../android-analysis.md) |
| Android-Geräte | Geräte, Pakete, Prozesse, Logs, Dumps, Bildschirmaufnahmen sowie APK- und Dateiübertragungen | Vom Aufrufer bereitgestelltes adb (Emulator oder Gerät); [ADB-Leitfaden](../adb-device-analysis.md) |
| JEB-Projekte | Engine-gestützte Projekteinheiten, Artefakt-Prüfsummen und Pseudokode von Typen/Methoden | Ein vom Aufrufer gestarteter JEB-Client mit MCP, ausgewählt über REA_JEB_MCP_URL; [JEB-Leitfaden](../jeb-analysis.md) |
diff --git a/docs/readme/README_el.md b/docs/readme/README_el.md
index 9912a8fea9..b7e9e19f7b 100644
--- a/docs/readme/README_el.md
+++ b/docs/readme/README_el.md
@@ -158,6 +158,7 @@ npx rea-agents@latest setup
| Ιστότοποι | Δομή σελίδων, scripts, παρατηρήσεις δικτύου και ζητούμενα στιγμιότυπα οθόνης | Πρόγραμμα περιήγησης της οικογένειας Chrome· [ανάλυση μέσω προγράμματος περιήγησης](../browser-observation.md) |
| Αποθηκευμένες καταγραφές δικτύου | Αιτήματα, αποκρίσεις, διαθέσιμα δεδομένα φορτίου και θέσεις στην πηγή | HAR ή SAZ του Fiddler Everywhere· mitmdump σε Linux για την εγγενή μορφή mitmproxy· [οδηγός καταγραφών](../web-network-captures.md) |
| Assemblies .NET | Μεταδεδομένα, εντολές CIL, δηλωμένες εξαρτήσεις από κώδικα μηχανής και συγκρίσεις builds | Καμία πρόσθετη απαίτηση (μόνο στατική ανάλυση)· [οδηγός διαχειριζόμενου κώδικα](../managed-code-analysis.md) |
+| PyInstaller | CArchive, PYZ, Python bytecode | Static inspection; [Python packaged applications](../python-packaged-applications.md) |
| APK Android | Δηλώσεις manifest, κλάσεις, απομεταγλωττισμένες μέθοδοι και αναφορές | JADX χωρίς γραφικό περιβάλλον και πλήρες JDK σε Linux/macOS/Windows x64· [οδηγός Android](../android-analysis.md) |
| Συσκευές Android | Συσκευές, πακέτα, διεργασίες, logs, dumps, στιγμιότυπα οθόνης και μεταφορές APK/αρχείων | adb που παρέχει ο χρήστης (εξομοιωτής ή φυσική συσκευή)· [οδηγός ADB](../adb-device-analysis.md) |
| Έργα JEB | Μονάδες έργου από τη μηχανή ανάλυσης, ψηφιακά αποτυπώματα αρχείων και ψευδοκώδικα τύπων/μεθόδων | Client JEB που έχει ξεκινήσει ο χρήστης και παρέχει MCP, επιλεγμένος μέσω REA_JEB_MCP_URL· [οδηγός JEB](../jeb-analysis.md) |
diff --git a/docs/readme/README_es.md b/docs/readme/README_es.md
index d7a79ade22..26f197ff62 100644
--- a/docs/readme/README_es.md
+++ b/docs/readme/README_es.md
@@ -152,6 +152,7 @@ REA requiere Node.js 22.x (>=22.19), 24.x (>=24.11) o 26+, además de npm. Las h
| Capturas de red guardadas | Solicitudes, respuestas, contenido accesible de las cargas útiles y ubicaciones de origen | HAR; mitmdump en Linux para capturas en formato nativo de mitmproxy; [guía de capturas](../web-network-captures.md) |
| Capturas de paquetes | Endpoints, DNS, TLS SNI, distribución de protocolos y solicitudes HTTP | tshark y capinfos suministrados por el autor de la llamada; [guía pcap](../pcap-analysis.md) |
| Ensamblados .NET | Metadatos, instrucciones CIL, dependencias nativas declaradas y comparaciones de compilaciones | Inspección estática; [guía de código administrado](../managed-code-analysis.md) |
+| PyInstaller | CArchive, PYZ, Python bytecode | Static inspection; [Python packaged applications](../python-packaged-applications.md) |
| APK de Android | Declaraciones del manifiesto, clases, métodos descompilados y referencias | JADX sin interfaz gráfica y un JDK completo en Linux/macOS/Windows x64; [guía de Android](../android-analysis.md) |
| Dispositivos Android | Dispositivos, paquetes, procesos, registros, volcados, capturas de pantalla y transferencias de APK/archivos | adb suministrado por el autor de la llamada (emulador o dispositivo); [guía de ADB](../adb-device-analysis.md) |
| Proyectos JEB | Unidades de proyecto del motor, resúmenes de artefactos y pseudocódigo de tipos/métodos | Un cliente JEB iniciado por el autor de la llamada que sirve MCP, seleccionado con REA_JEB_MCP_URL; [guía de JEB](../jeb-analysis.md) |
diff --git a/docs/readme/README_fa.md b/docs/readme/README_fa.md
index 8f88b2e4e1..fa23c15949 100644
--- a/docs/readme/README_fa.md
+++ b/docs/readme/README_fa.md
@@ -143,6 +143,7 @@ REA به Node.js 22.x (>=22.19)، 24.x (>=24.11) یا 26+، بههمراه np
| وبسایتها | ساختار صفحه، اسکریپتها، مشاهدات شبکه و اسکرینشاتهای درخواستی | مرورگر مبتنی بر Chrome؛ [تحلیل مرورگر](../browser-observation.md) |
| دادههای ضبطشدهٔ شبکه | درخواستها، پاسخها، payloadهای آشکارشده و مکان آنها در منبع | HAR؛ ابزار mitmdump در لینوکس برای ضبطهای بومی mitmproxy؛ [راهنمای ضبط شبکه](../web-network-captures.md) |
| اسمبلیهای .NET | فراداده، دستورالعملهای CIL، وابستگیهای بومی اعلامشده و مقایسهٔ buildها | بررسی ایستا؛ [راهنمای کد مدیریتشده](../managed-code-analysis.md) |
+| PyInstaller | CArchive, PYZ, Python bytecode | Static inspection; [Python packaged applications](../python-packaged-applications.md) |
| فایلهای APK اندروید | تعاریف Manifest، کلاسها، متدهای دیکامپایلشده و ارجاعات | JADX بدون رابط گرافیکی و JDK کامل در Linux/macOS/Windows x64؛ [راهنمای اندروید](../android-analysis.md) |
| دستگاههای اندروید | دستگاهها، بستهها، فرایندها، لاگها، دامپها، اسکرینشاتها و انتقال APK/فایل | adb تأمینشده توسط فراخوانکننده (شبیهساز یا دستگاه)؛ [راهنمای ADB](../adb-device-analysis.md) |
| پروژههای JEB | واحدهای پروژه از موتور، هشهای مصنوع و شبهکدِ نوع/متد | یک کلاینت JEB که توسط فراخوانکننده اجرا شده و MCP ارائه میدهد و با REA_JEB_MCP_URL انتخاب میشود؛ [راهنمای JEB](../jeb-analysis.md) |
diff --git a/docs/readme/README_fr.md b/docs/readme/README_fr.md
index c912ffb02a..db2a7051bb 100644
--- a/docs/readme/README_fr.md
+++ b/docs/readme/README_fr.md
@@ -156,6 +156,7 @@ REA nécessite Node.js 22.x (>=22.19), 24.x (>=24.11) ou 26+, ainsi que npm. Les
| Captures réseau enregistrées | Requêtes, réponses, payloads exposés et emplacements dans le code source | HAR ; mitmdump sous Linux pour les captures mitmproxy natives ; [guide des captures](../web-network-captures.md) |
| Captures de paquets | Points de terminaison, DNS, TLS SNI, distribution des protocoles et requêtes HTTP | tshark et capinfos fournis par l'appelant ; [guide pcap](../pcap-analysis.md) |
| Assemblys .NET | Métadonnées, instructions CIL, dépendances natives déclarées et comparaisons de builds | Inspection statique ; [guide du code managé](../managed-code-analysis.md) |
+| PyInstaller | CArchive, PYZ, Python bytecode | Static inspection; [Python packaged applications](../python-packaged-applications.md) |
| APK Android | Déclarations du manifeste, classes, méthodes décompilées et références | JADX headless et un JDK complet sous Linux/macOS/Windows x64 ; [guide Android](../android-analysis.md) |
| Appareils Android | Appareils, paquets, processus, journaux, dumps, captures d'écran et transferts d'APK/fichiers | adb fourni par l'appelant (émulateur ou appareil) ; [guide ADB](../adb-device-analysis.md) |
| Projets JEB | Unités de projet fournies par le moteur, condensés d'artefacts et pseudo-code de types/méthodes | Un client JEB démarré par l'appelant servant MCP, sélectionné via REA_JEB_MCP_URL ; [guide JEB](../jeb-analysis.md) |
diff --git a/docs/readme/README_id.md b/docs/readme/README_id.md
index d4eafc7881..d824477757 100644
--- a/docs/readme/README_id.md
+++ b/docs/readme/README_id.md
@@ -159,6 +159,7 @@ Alat tambahan dan dukungan sistem host bergantung pada target yang dianalisis:
| Rekaman jaringan tersimpan | Permintaan, respons, payload yang terekspos, dan lokasi sumber | HAR; mitmdump di Linux untuk rekaman native mitmproxy; [panduan rekaman](../web-network-captures.md) |
| Capturan paket | Endpoint, DNS, TLS SNI, distribusi protokol, dan permintaan HTTP | tshark dan capinfos buatan pemanggil; [panduan pcap](../pcap-analysis.md) |
| Assembly .NET | Metadata, instruksi CIL, dependensi native yang dideklarasikan, dan perbandingan build | Tidak ada (hanya analisis statis); [panduan kode terkelola](../managed-code-analysis.md) |
+| PyInstaller | CArchive, PYZ, Python bytecode | Static inspection; [Python packaged applications](../python-packaged-applications.md) |
| APK Android | Deklarasi manifest, kelas, metode hasil dekompilasi, dan referensi | JADX mode headless dan JDK lengkap di Linux/macOS/Windows x64; [panduan Android](../android-analysis.md) |
| Perangkat Android | Perangkat, paket, proses, log, dump, tangkapan layar, serta transfer APK/berkas | adb buatan pemanggil (emulator atau perangkat); [panduan ADB](../adb-device-analysis.md) |
| Proyek JEB | Unit proyek bertenaga mesin, hash artefak, dan pseudokode tipe/metode | Klien JEB yang dijalankan pemanggil dan menyajikan MCP, dipilih dengan REA_JEB_MCP_URL; [panduan JEB](../jeb-analysis.md) |
diff --git a/docs/readme/README_it.md b/docs/readme/README_it.md
index 5678c813c1..7f951545a2 100644
--- a/docs/readme/README_it.md
+++ b/docs/readme/README_it.md
@@ -156,6 +156,7 @@ REA richiede Node.js 22.x (>=22.19), 24.x (>=24.11) o 26+, oltre a npm. Strument
| Acquisizioni di rete salvate | Richieste, risposte, payload esposti e posizioni sorgente | HAR; mitmdump su Linux per acquisizioni mitmproxy native; [guida alle acquisizioni](../web-network-captures.md) |
| Captured di pacchetti | Endpoint, DNS, TLS SNI, distribuzione dei protocolli e richieste HTTP | tshark e capinfos forniti dal chiamante; [guida pcap](../pcap-analysis.md) |
| Assembly .NET | Metadati, istruzioni CIL, dipendenze native dichiarate e confronti di build | Nessuno (solo statico); [guida managed-code](../managed-code-analysis.md) |
+| PyInstaller | CArchive, PYZ, Python bytecode | Static inspection; [Python packaged applications](../python-packaged-applications.md) |
| APK Android | Dichiarazioni manifest, classi, metodi decompilati e riferimenti | JADX headless e un JDK completo su Linux/macOS/Windows x64; [guida Android](../android-analysis.md) |
| Dispositivi Android | Dispositivi, pacchetti, processi, log, dump, screenshot e trasferimenti APK/file | adb fornito dal chiamante (emulatore o hardware); [guida ADB](../adb-device-analysis.md) |
| Progetti JEB | Unità di progetto dal motore, digest degli artifact e pseudocodice di tipi/metodi | Client JEB avviato dal chiamante via MCP, selezionato con REA_JEB_MCP_URL; [guida JEB](../jeb-analysis.md) |
diff --git a/docs/readme/README_ja.md b/docs/readme/README_ja.md
index 2022f088f0..f4595b6824 100644
--- a/docs/readme/README_ja.md
+++ b/docs/readme/README_ja.md
@@ -152,6 +152,7 @@ REA には Node.js 22.x(>=22.19)、24.x(>=24.11)、または 26+ と npm
| 保存済みのネットワークキャプチャ | リクエスト、レスポンス、取得可能なペイロード、ソース内の位置 | HAR;mitmproxy ネイティブ形式のキャプチャには Linux 上の mitmdump;[キャプチャガイド](../web-network-captures.md) |
| パケットキャプチャ | エンドポイント、DNS、TLS SNI、プロトコル分布、HTTPリクエスト | 呼び出し側が用意した tshark と capinfos;[pcapガイド](../pcap-analysis.md) |
| .NET アセンブリ | メタデータ、CIL 命令、宣言されたネイティブ依存関係、ビルドの比較 | 静的な検査;[マネージドコードのガイド](../managed-code-analysis.md) |
+| PyInstaller | CArchive, PYZ, Python bytecode | Static inspection; [Python packaged applications](../python-packaged-applications.md) |
| Android APK | マニフェストの宣言、クラス、逆コンパイルされたメソッド、参照 | Linux/macOS/Windows x64 上のヘッドレス JADX と完全な JDK;[Android ガイド](../android-analysis.md) |
| Android デバイス | デバイス、パッケージ、プロセス、ログ、ダンプ、スクリーンキャプチャ、APK/ファイル転送 | 呼び出し側が用意した adb(エミュレーターまたは実機);[ADB ガイド](../adb-device-analysis.md) |
| JEBプロジェクト | エンジンによるプロジェクトユニット、アーティファクトのダイジェスト、型/メソッドの擬似コード | 呼び出し側が起動し MCP を提供する JEB クライアント(REA_JEB_MCP_URL で選択);[JEB ガイド](../jeb-analysis.md) |
diff --git a/docs/readme/README_ko.md b/docs/readme/README_ko.md
index 14ad6cbb96..3fa5f2569a 100644
--- a/docs/readme/README_ko.md
+++ b/docs/readme/README_ko.md
@@ -173,6 +173,7 @@ REA에는 Node.js 22.x(>=22.19), 24.x(>=24.11) 또는 26+와 npm이 필요합니
| 기록된 논리 신호 | 디코더 주석, 샘플 범위, 진단 정보 | Linux: sigrok-cli 0.7.2 + prlimit; [신호 기록 가이드](../signal-recordings.md) |
| 패키지 및 리소스 | 파일 목록, 다이제스트, plist, Apple 번들 구조, 추출한 리소스 | [아티팩트 및 JavaScript 가이드](../javascript-artifact-reconstruction.md), [Apple 애플리케이션](../apple-application-analysis.md) |
| 프로세스 동작 | 터미널 출력, 상호작용, 종료 및 파일 시스템 관찰 결과, 실행 비교 | 네이티브 PTY를 지원하는 Linux/macOS; [프로세스 캡처](../process-capture.md) |
+| PyInstaller | CArchive, PYZ, Python bytecode | Static inspection; [Python packaged applications](../python-packaged-applications.md) |
정적 JavaScript 및 .NET 검사는 제공된 파일을 읽으며 애플리케이션을 실행하지 않습니다. 런타임 캡처는 사용자의 권한으로 선택한 대상을 실행하거나 대상과 상호작용합니다. 각 런타임 가이드에서 그 영향을 설명합니다.
diff --git a/docs/readme/README_ml.md b/docs/readme/README_ml.md
index 1d715eb2b8..5e07f3663f 100644
--- a/docs/readme/README_ml.md
+++ b/docs/readme/README_ml.md
@@ -160,6 +160,7 @@ REA-യ്ക്ക് Node.js 22.x (>=22.19), 24.x (>=24.11), അല്ലെ
| Websites | Page structure, scripts, network observations, ആവശ്യപ്പെട്ട screenshots | Chrome-family browser; [browser analysis](../browser-observation.md) |
| Saved network captures | Requests, responses, പുറത്തുകാണുന്ന payload-ുകൾ, source locations | HAR അല്ലെങ്കിൽ Fiddler Everywhere SAZ; native mitmproxy-നായി Linux-ൽ mitmdump; [capture guide](../web-network-captures.md) |
| .NET assemblies | Metadata, CIL instructions, പ്രഖ്യാപിച്ച native dependencies, build comparisons | ഒന്നും ആവശ്യമില്ല (static മാത്രം); [managed-code guide](../managed-code-analysis.md) |
+| Python packaged apps | CArchive/PYZ entries, Python version, bytecode, resources and runtime options | Static PyInstaller parsing; [Python guide](../python-packaged-applications.md) |
| Android APKs | Manifest declarations, classes, decompiled methods, references | Linux/macOS/Windows x64-ൽ Headless JADX-ഉം full JDK-ഉം; [Android guide](../android-analysis.md) |
| Android devices | Devices, packages, processes, logs, dumps, screen captures, APK/file transfers | Caller നൽകുന്ന adb (emulator അല്ലെങ്കിൽ hardware); [ADB guide](../adb-device-analysis.md) |
| JEB projects | Engine-backed project units, artifact digests, type/method pseudo-code | MCP സേവനം നൽകുന്ന caller ആരംഭിച്ച JEB client; REA_JEB_MCP_URL ഉപയോഗിച്ച് തിരഞ്ഞെടുക്കുക; [JEB guide](../jeb-analysis.md) |
diff --git a/docs/readme/README_pl.md b/docs/readme/README_pl.md
index e19413fd87..4c692d2364 100644
--- a/docs/readme/README_pl.md
+++ b/docs/readme/README_pl.md
@@ -152,6 +152,7 @@ REA wymaga Node.js 22.x (>=22.19), 24.x (>=24.11) lub 26+ oraz npm. Dodatkowe na
| Zapisane przechwycenia ruchu sieciowego | Żądania, odpowiedzi, dostępne dane przesyłane i lokalizacje źródłowe | HAR; mitmdump w systemie Linux dla natywnych przechwyceń mitmproxy; [przewodnik przechwytywania](../web-network-captures.md) |
| Przechwytywanie pakietów | Punkty końcowe, DNS, TLS SNI, dystrybucja protokołów i żądania HTTP | Dostarczone przez wywołującego tshark i capinfos; [przewodnik pcap](../pcap-analysis.md) |
| Zestawy .NET | Metadane, instrukcje CIL, zadeklarowane zależności natywne i porównania kompilacji | Analiza statyczna; [przewodnik kodu zarządzanego](../managed-code-analysis.md) |
+| PyInstaller | CArchive, PYZ, Python bytecode | Static inspection; [Python packaged applications](../python-packaged-applications.md) |
| Pakiety APK dla Androida | Deklaracje manifestu, klasy, zdekompilowane metody i odwołania | JADX bez interfejsu graficznego i pełny JDK w systemach Linux/macOS/Windows x64; [przewodnik Androida](../android-analysis.md) |
| Urządzenia z Androidem | Urządzenia, pakiety, procesy, logi, zrzuty, zrzuty ekranu oraz transfery APK/plików | adb dostarczony przez wywołującego (emulator lub urządzenie); [przewodnik ADB](../adb-device-analysis.md) |
| Projekty JEB | Jednostki projektu obsługiwane przez silnik, skróty artefaktów i pseudokod typów/metod | Uruchomiony przez wywołującego klient JEB udostępniający MCP, wybrany przez REA_JEB_MCP_URL; [przewodnik JEB](../jeb-analysis.md) |
diff --git a/docs/readme/README_pt-BR.md b/docs/readme/README_pt-BR.md
index efbd4e9b41..019bba1129 100644
--- a/docs/readme/README_pt-BR.md
+++ b/docs/readme/README_pt-BR.md
@@ -152,6 +152,7 @@ O REA requer Node.js 22.x (>=22.19), 24.x (>=24.11) ou 26+, além do npm. As fer
| Capturas de rede salvas | Requisições, respostas, payloads acessíveis e localizações de origem | HAR; mitmdump no Linux para capturas no formato nativo do mitmproxy; [guia de capturas](../web-network-captures.md) |
| Capturas de pacotes | Endpoints, DNS, TLS SNI, distribuição de protocolos e requisições HTTP | tshark e capinfos fornecidos pelo chamador; [guia pcap](../pcap-analysis.md) |
| Assemblies .NET | Metadados, instruções CIL, dependências nativas declaradas e comparações de builds | Inspeção estática; [guia de código gerenciado](../managed-code-analysis.md) |
+| PyInstaller | CArchive, PYZ, Python bytecode | Static inspection; [Python packaged applications](../python-packaged-applications.md) |
| APKs Android | Declarações do manifesto, classes, métodos descompilados e referências | JADX sem interface gráfica e um JDK completo no Linux/macOS/Windows x64; [guia de Android](../android-analysis.md) |
| Dispositivos Android | Dispositivos, pacotes, processos, logs, despejos, capturas de tela e transferências de APK/arquivos | adb fornecido pelo chamador (emulador ou aparelho); [guia de ADB](../adb-device-analysis.md) |
| Projetos JEB | Unidades de projeto do motor, hashes de artefatos e pseudocódigo de tipos/métodos | Um cliente JEB iniciado pelo chamador servindo MCP, selecionado com REA_JEB_MCP_URL; [guia de JEB](../jeb-analysis.md) |
diff --git a/docs/readme/README_ru.md b/docs/readme/README_ru.md
index 12f06df7bf..22ff426b8a 100644
--- a/docs/readme/README_ru.md
+++ b/docs/readme/README_ru.md
@@ -162,6 +162,7 @@ REA требует Node.js 22.x (>=22.19), 24.x (>=24.11) или 26+, а так
| Записанные логические сигналы | Аннотации декодера, диапазоны отсчётов и диагностика | Linux: sigrok-cli 0.7.2 + prlimit; [руководство по сигналам](../signal-recordings.md) |
| Пакеты и ресурсы | Списки файлов, хеши, plist, структура пакетов Apple и извлечённые ресурсы | [Руководство по артефактам и JavaScript](../javascript-artifact-reconstruction.md), [приложения Apple](../apple-application-analysis.md) |
| Поведение процессов | Вывод терминала, взаимодействия, наблюдения за завершением и файловой системой, сравнения запусков | Linux/macOS с нативным PTY; [захват процессов](../process-capture.md) |
+| PyInstaller | CArchive, PYZ, Python bytecode | Static inspection; [Python packaged applications](../python-packaged-applications.md) |
Статическое исследование JavaScript и .NET читает предоставленные файлы, не запуская приложение. Захват во время выполнения запускает выбранную цель или взаимодействует с ней с правами вашего пользователя; соответствующие руководства описывают последствия.
diff --git a/docs/readme/README_th.md b/docs/readme/README_th.md
index 304e0d5437..fb8c3ff604 100644
--- a/docs/readme/README_th.md
+++ b/docs/readme/README_th.md
@@ -152,6 +152,7 @@ REA ต้องใช้ Node.js 22.x (>=22.19), 24.x (>=24.11) หรือ 26
| ข้อมูลเครือข่ายที่บันทึกไว้ | คำขอ คำตอบ เพย์โหลดที่ปรากฏ และตำแหน่งต้นทาง | HAR; mitmdump บน Linux สำหรับข้อมูลที่บันทึกในรูปแบบเนทีฟของ mitmproxy; [คู่มือการบันทึกข้อมูล](../web-network-captures.md) |
| การจับแพ็กเก็ต | จุดเชื่อมต่อ, DNS, TLS SNI, การกระจายโปรโตคอล และคำขอ HTTP | tshark และ capinfos ที่ผู้เรียกจัดเตรียม; [คู่มือ pcap](../pcap-analysis.md) |
| แอสเซมบลี .NET | เมทาดาทา คำสั่ง CIL การพึ่งพาเนทีฟที่ประกาศไว้ และการเปรียบเทียบผลการบิลด์ | การตรวจสอบแบบสถิต; [คู่มือโค้ดแบบจัดการ](../managed-code-analysis.md) |
+| PyInstaller | CArchive, PYZ, Python bytecode | Static inspection; [Python packaged applications](../python-packaged-applications.md) |
| APK ของ Android | การประกาศใน manifest คลาส เมธอดที่ดีคอมไพล์ และการอ้างอิง | JADX แบบไม่มี GUI และ JDK แบบเต็มบน Linux/macOS/Windows x64; [คู่มือ Android](../android-analysis.md) |
| อุปกรณ์ Android | อุปกรณ์ แพ็กเกจ โพรเซส บันทึก ดัมพ์ ภาพหน้าจอ และการถ่ายโอน APK/ไฟล์ | adb ที่ผู้เรียกจัดเตรียม (โปรแกรมจำลองหรืออุปกรณ์จริง); [คู่มือ ADB](../adb-device-analysis.md) |
| โปรเจกต์ JEB | หน่วยโปรเจกต์จากเอนจิน, แฮชของอาร์ติแฟกต์ และซูโดโค้ดของไทป์/เมท็อด | ไคลเอนต์ JEB ที่ผู้เรียกเริ่มและให้บริการ MCP เลือกด้วย REA_JEB_MCP_URL; [คู่มือ JEB](../jeb-analysis.md) |
diff --git a/docs/readme/README_tr.md b/docs/readme/README_tr.md
index b0ba8f343b..2cb89a5496 100644
--- a/docs/readme/README_tr.md
+++ b/docs/readme/README_tr.md
@@ -156,6 +156,7 @@ REA; Node.js 22.x (>=22.19), 24.x (>=24.11) veya 26+ ve npm gerektirir. Ek araç
| Web siteleri | Sayfa yapısı, betikler, ağ gözlemleri ve istenen ekran görüntüleri | Chrome ailesinden bir tarayıcı; [tarayıcı analizi](../browser-observation.md) |
| Kaydedilmiş ağ yakalamaları | İstekler, yanıtlar, erişilebilir yük içerikleri ve kaynak konumları | HAR veya Fiddler Everywhere SAZ; Linux'ta native mitmproxy için mitmdump; [ağ yakalama rehberi](../web-network-captures.md) |
| .NET derlemeleri | Meta veriler, CIL talimatları, bildirilmiş yerel bağımlılıklar ve derleme karşılaştırmaları | Statik inceleme; [yönetilen kod rehberi](../managed-code-analysis.md) |
+| PyInstaller | CArchive, PYZ, Python bytecode | Static inspection; [Python packaged applications](../python-packaged-applications.md) |
| Android APK'leri | Manifest bildirimleri, sınıflar, tersine derlenmiş metotlar ve referanslar | Linux/macOS/Windows x64 üzerinde arayüzsüz JADX ve tam bir JDK; [Android rehberi](../android-analysis.md) |
| Android cihazları | Cihazlar, paketler, süreçler, günlükler, dökümler, ekran görüntüleri ve APK/dosya aktarımları | Çağıranın sağladığı adb (emülatör veya cihaz); [ADB rehberi](../adb-device-analysis.md) |
| JEB projeleri | Motor destekli proje birimleri, artefakt özetleri ve tür/metot sözdekodu | Çağıranın başlattığı, MCP sunan bir JEB istemcisi, REA_JEB_MCP_URL ile seçilir; [JEB rehberi](../jeb-analysis.md) |
diff --git a/docs/readme/README_uk.md b/docs/readme/README_uk.md
index 4a33ac96d4..eac65f273e 100644
--- a/docs/readme/README_uk.md
+++ b/docs/readme/README_uk.md
@@ -152,6 +152,7 @@ REA потребує Node.js 22.x (>=22.19), 24.x (>=24.11) або 26+, а та
| Збережені мережеві захоплення | Запити, відповіді, доступні корисні дані та місця в джерелі | HAR; mitmdump у Linux для захоплень у власному форматі mitmproxy; [посібник мережевих захоплень](../web-network-captures.md) |
| Захоплення пакетів | Кінцеві точки, DNS, TLS SNI, розподіл протоколів та HTTP-запити | Надані викликаючою стороною tshark та capinfos; [підручник pcap](../pcap-analysis.md) |
| Збірки .NET | Метадані, інструкції CIL, оголошені нативні залежності й порівняння збірок | Статична перевірка; [посібник керованого коду](../managed-code-analysis.md) |
+| PyInstaller | CArchive, PYZ, Python bytecode | Static inspection; [Python packaged applications](../python-packaged-applications.md) |
| Android APK | Оголошення маніфесту, класи, декомпільовані методи й посилання | JADX без графічного інтерфейсу й повний JDK у Linux/macOS/Windows x64; [посібник Android](../android-analysis.md) |
| Пристрої Android | Пристрої, пакети, процеси, журнали, дампи, знімки екрана та передавання APK/файлів | adb, наданий викликаючою стороною (емулятор або пристрій); [підручник ADB](../adb-device-analysis.md) |
| Проєкти JEB | Єдиниці проєкту від рушія, дайджести артефактів і псевдокод типів/методів | Запущений викликаною стороною клієнт JEB з MCP, обирається через REA_JEB_MCP_URL; [посібник JEB](../jeb-analysis.md) |
diff --git a/docs/readme/README_vi.md b/docs/readme/README_vi.md
index 19f18c8ca2..cfba9f3c47 100644
--- a/docs/readme/README_vi.md
+++ b/docs/readme/README_vi.md
@@ -162,6 +162,7 @@ REA yêu cầu Node.js 22.x (>=22.19), 24.x (>=24.11) hoặc 26+, cùng với np
| Tín hiệu logic đã ghi | Chú thích bộ giải mã, phạm vi mẫu và chẩn đoán | Linux: sigrok-cli 0.7.2 + prlimit; [hướng dẫn tín hiệu](../signal-recordings.md) |
| Gói và tài nguyên | Danh sách tệp, giá trị băm, plist, cấu trúc bundle Apple và tài nguyên được trích xuất | [Hướng dẫn hiện vật phần mềm và JavaScript](../javascript-artifact-reconstruction.md), [ứng dụng Apple](../apple-application-analysis.md) |
| Hành vi tiến trình | Đầu ra terminal, tương tác, quan sát khi thoát và hệ thống tệp, cùng so sánh các lần chạy | Linux/macOS có PTY gốc; [ghi lại tiến trình](../process-capture.md) |
+| PyInstaller | CArchive, PYZ, Python bytecode | Static inspection; [Python packaged applications](../python-packaged-applications.md) |
Kiểm tra tĩnh JavaScript và .NET đọc các tệp được cung cấp mà không chạy ứng dụng. Ghi lại khi chạy sẽ khởi chạy hoặc tương tác với mục tiêu đã chọn bằng quyền người dùng của bạn; từng hướng dẫn mô tả tác động của nó.
diff --git a/docs/readme/README_zh-TW.md b/docs/readme/README_zh-TW.md
index 403cf3d2a4..635bcbc9af 100644
--- a/docs/readme/README_zh-TW.md
+++ b/docs/readme/README_zh-TW.md
@@ -156,6 +156,7 @@ REA 需要 Node.js 22.x(>=22.19)、24.x(>=24.11)或 26+,以及 npm。
| 已保存的網路擷取 | 請求、回應、可訪問的酬載和來源位置 | HAR;原生 mitmproxy 擷取需要 Linux 上的 mitmdump;[擷取指南](../web-network-captures.md) |
| 封包擷取 | 端點、DNS、TLS SNI、通訊協定分佈和 HTTP 請求 | 呼叫端提供的 tshark 和 capinfos;[pcap 指南](../pcap-analysis.md) |
| .NET 程式集 | 中繼資料、CIL 指令、聲明的原生依賴和建置對比 | 靜態檢查;[托管程式碼指南](../managed-code-analysis.md) |
+| PyInstaller | CArchive, PYZ, Python bytecode | Static inspection; [Python packaged applications](../python-packaged-applications.md) |
| Android APK | 清單聲明、類別、反編譯的方法和引用 | Linux/macOS/Windows x64 上的無界面 JADX 和完整 JDK;[Android 指南](../android-analysis.md) |
| Android 裝置 | 裝置、套件、程序、日誌、傾印、螢幕擷取以及 APK/檔案傳輸 | 呼叫端提供的 adb(模擬器或實體裝置);[ADB 指南](../adb-device-analysis.md) |
| JEB 專案 | 引擎支援的專案單元、成品摘要和型別/方法的虛擬碼 | 由呼叫端啟動、提供 MCP 的 JEB 用戶端,透過 REA_JEB_MCP_URL 選擇;[JEB 指南](../jeb-analysis.md) |
diff --git a/docs/readme/README_zh.md b/docs/readme/README_zh.md
index 51275f4c08..313262ae53 100644
--- a/docs/readme/README_zh.md
+++ b/docs/readme/README_zh.md
@@ -156,6 +156,7 @@ REA 需要 Node.js 22.x(>=22.19)、24.x(>=24.11)或 26+,以及 npm。
| 已保存的网络捕获 | 请求、响应、可访问的载荷和来源位置 | HAR;原生 mitmproxy 捕获需要 Linux 上的 mitmdump;[捕获指南](../web-network-captures.md) |
| 数据包捕获 | 端点、DNS、TLS SNI、协议分布和 HTTP 请求 | 调用方提供的 tshark 和 capinfos;[pcap 指南](../pcap-analysis.md) |
| .NET 程序集 | 元数据、CIL 指令、声明的原生依赖和构建对比 | 静态检查;[托管代码指南](../managed-code-analysis.md) |
+| PyInstaller | CArchive, PYZ, Python bytecode | Static inspection; [Python packaged applications](../python-packaged-applications.md) |
| Android APK | 清单声明、类、反编译的方法和引用 | Linux/macOS/Windows x64 上的无界面 JADX 和完整 JDK;[Android 指南](../android-analysis.md) |
| Android 设备 | 设备、软件包、进程、日志、转储、屏幕截图以及 APK/文件传输 | 调用方提供的 adb(模拟器或实体设备);[ADB 指南](../adb-device-analysis.md) |
| JEB 项目 | 引擎支持的项目单元、工件摘要和类型/方法的伪代码 | 由调用方启动、提供 MCP 的 JEB 客户端,通过 REA_JEB_MCP_URL 选择;[JEB 指南](../jeb-analysis.md) |
diff --git a/src/artifacts/ArtifactPaths.test.ts b/src/artifacts/ArtifactPaths.test.ts
index 5957bb825e..b57a37b3f1 100644
--- a/src/artifacts/ArtifactPaths.test.ts
+++ b/src/artifacts/ArtifactPaths.test.ts
@@ -4,13 +4,44 @@ import {
ArtifactPathRegistry,
annotateEnglishUnicodeCaseCollisions,
ENGLISH_UNICODE_CASE_INVENTORY_LIMITATION,
+ normalizeArtifactPath,
} from "./ArtifactPaths.js";
const NOTE =
"a case-insensitive destination cannot store both. This comparison is not a filesystem observation.";
+it.each(["drive", "escaped", "normalization", "prefix", "collision"])(
+ "bounds path failure diagnostics at the owning boundary: %s",
+ (kind) => {
+ const name = "x".repeat(1024 * 1024);
+ const registry = new ArtifactPathRegistry();
+ const fail = () => {
+ if (kind === "drive") normalizeArtifactPath("C:" + name);
+ else if (kind === "escaped")
+ normalizeArtifactPath("\0".repeat(name.length));
+ else if (kind === "normalization")
+ normalizeArtifactPath(name + "/../child");
+ else {
+ registry.add(name, "file");
+ registry.add(kind === "prefix" ? name + "/child" : name, "directory");
+ }
+ };
+ const failure = (() => {
+ try {
+ fail();
+ } catch (cause: unknown) {
+ return cause;
+ }
+ return undefined;
+ })();
+ expect(failure).toMatchObject({ reason: "path" });
+ expect((failure as Error).message.length).toBeLessThan(1024);
+ },
+);
+
const annotate = (paths: readonly string[]) => {
const occurrences = paths.map((logical_path) => ({
+ entry_kind: "file" as const,
logical_path,
limitations: [] as string[],
}));
diff --git a/src/artifacts/ArtifactPaths.ts b/src/artifacts/ArtifactPaths.ts
index 2bc22f7642..f29c1faa9f 100644
--- a/src/artifacts/ArtifactPaths.ts
+++ b/src/artifacts/ArtifactPaths.ts
@@ -2,10 +2,25 @@ import { posix } from "node:path";
import { logicalPathNfc } from "../domain/artifactIdentity.js";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
-import { ArtifactReaderFailure } from "./ArtifactReader.js";
+import { ArtifactReaderFailure, type ArtifactEntry } from "./ArtifactReader.js";
const DRIVE_PREFIX = /^[A-Za-z]:/u;
+// Bound diagnostic JSON expansion to 768 characters before formatting. This is
+// a presentation budget, never a limit on accepted paths or retained facts.
+const PATH_DIAGNOSTIC_CODE_UNITS = 128;
+const pathDiagnostic = (path: string): string =>
+ path.length <= PATH_DIAGNOSTIC_CODE_UNITS
+ ? JSON.stringify(path)
+ : `${JSON.stringify(path.slice(0, PATH_DIAGNOSTIC_CODE_UNITS))}… (${String(path.length)} UTF-16 code units)`;
+
+type DestinationEntryKind = Exclude;
+
+/** Declarations have query identities, but never claim a destination path. */
+export const hasDestinationPath = (
+ kind: ArtifactEntry["kind"],
+): kind is DestinationEntryKind => kind !== "metadata";
+
/**
* English Unicode case folding used only to describe logical names a
* case-insensitive destination might collapse. It is not a filesystem case table.
@@ -25,7 +40,7 @@ export const normalizeArtifactPath = (input: string): string => {
if (input.includes("\0") || input.startsWith("/") || DRIVE_PREFIX.test(input))
throw new ArtifactReaderFailure(
"path",
- `Artifact path is absolute or unsafe: ${JSON.stringify(input)}`,
+ `Artifact path is absolute or unsafe: ${pathDiagnostic(input)}`,
);
const normalized = logicalPathNfc(input).replace(/\/+$/u, "");
const parts = normalized.split("/");
@@ -36,7 +51,7 @@ export const normalizeArtifactPath = (input: string): string => {
)
throw new ArtifactReaderFailure(
"path",
- `Artifact path is not normalized: ${JSON.stringify(input)}`,
+ `Artifact path is not normalized: ${pathDiagnostic(input)}`,
);
return normalized;
};
@@ -45,14 +60,15 @@ export const normalizeArtifactPath = (input: string): string => {
export class ArtifactPathRegistry {
readonly #root: PathTrieNode = { kind: undefined, children: new Map() };
- add(path: string, kind: "file" | "directory" | "symlink" | "slice"): void {
+ add(path: string, kind: ArtifactEntry["kind"]): void {
+ if (!hasDestinationPath(kind)) return;
const parts = path.split("/");
let node = this.#root;
for (const [index, part] of parts.entries()) {
if (node.kind !== undefined && node.kind !== "directory")
throw new ArtifactReaderFailure(
"path",
- `Artifact prefix conflict: ${path}`,
+ `Artifact prefix conflict: ${pathDiagnostic(path)}`,
);
let child = node.children.get(part);
if (child === undefined) {
@@ -64,12 +80,12 @@ export class ArtifactPathRegistry {
if (node.kind !== undefined && node.kind !== kind)
throw new ArtifactReaderFailure(
"path",
- `Artifact path collision: ${path}`,
+ `Artifact path collision: ${pathDiagnostic(path)}`,
);
if (kind !== "directory" && node.children.size > 0)
throw new ArtifactReaderFailure(
"path",
- `Artifact prefix conflict: ${path}`,
+ `Artifact prefix conflict: ${pathDiagnostic(path)}`,
);
}
}
@@ -78,7 +94,7 @@ export class ArtifactPathRegistry {
}
interface PathTrieNode {
- kind: "file" | "directory" | "symlink" | "slice" | undefined;
+ kind: DestinationEntryKind | undefined;
readonly children: Map;
}
@@ -103,15 +119,19 @@ const englishCaseFold = (part: string): string =>
*/
export const annotateEnglishUnicodeCaseCollisions = (
occurrences: readonly {
+ readonly entry_kind: ArtifactEntry["kind"];
readonly logical_path: string;
limitations: string[];
}[],
): string | undefined => {
+ const destinations = occurrences.filter(({ entry_kind }) =>
+ hasDestinationPath(entry_kind),
+ );
const collisions = englishUnicodeCaseCollisions(
- occurrences.map(({ logical_path }) => logical_path),
+ destinations.map(({ logical_path }) => logical_path),
);
if (collisions.size === 0) return undefined;
- for (const occurrence of occurrences)
+ for (const occurrence of destinations)
for (const collision of collisions.get(occurrence.logical_path) ?? [])
occurrence.limitations.push(
caseCollisionLimitation(occurrence.logical_path, collision),
diff --git a/src/artifacts/ArtifactReader.ts b/src/artifacts/ArtifactReader.ts
index 1a2a5a6147..33cb771ea2 100644
--- a/src/artifacts/ArtifactReader.ts
+++ b/src/artifacts/ArtifactReader.ts
@@ -33,7 +33,7 @@ export interface ArtifactReaderFailureOptions extends ErrorOptions {
/** Archive-neutral entry metadata. Reader adapters never choose output paths. */
export interface ArtifactEntry {
readonly path: string;
- readonly kind: "file" | "directory" | "symlink" | "slice";
+ readonly kind: "file" | "directory" | "symlink" | "slice" | "metadata";
readonly declaredSize: number | null;
readonly compressedSize: number | null;
readonly executable: boolean;
@@ -167,6 +167,7 @@ export interface ArtifactReader {
| ZipPackageFormat
| "asar"
| "godot-pck"
+ | "pyinstaller"
| "file";
entries(
signal?: AbortSignal,
diff --git a/src/artifacts/PyInstallerArtifactReader.ts b/src/artifacts/PyInstallerArtifactReader.ts
new file mode 100644
index 0000000000..9f79d5f513
--- /dev/null
+++ b/src/artifacts/PyInstallerArtifactReader.ts
@@ -0,0 +1,608 @@
+import type { Readable } from "node:stream";
+import { finished } from "node:stream/promises";
+import { setImmediate } from "node:timers/promises";
+import { jsonValueByteLength } from "../domain/jsonValueByteLength.js";
+import type { JsonValue } from "../domain/jsonValue.js";
+import { normalizeArtifactPath } from "./ArtifactPaths.js";
+import { lstat, type FileHandle } from "node:fs/promises";
+
+import type { PyInstallerMetadata } from "../domain/artifactFormatMetadata.js";
+import {
+ cookieMetadata,
+ scriptPythonMagic,
+} from "./pyinstaller/PyInstallerMetadata.js";
+import { artifactExpansionBudgetBytes } from "../domain/artifactExpansionBudget.js";
+import { OwnedFileHandle } from "../filesystem/OwnedFileHandle.js";
+import {
+ RegularFileAdmissionFailure,
+ openRegularFile,
+ sameRegularFileState,
+ type StableRegularFileDescriptor,
+} from "../filesystem/RegularFile.js";
+import {
+ ArtifactReaderFailure,
+ sameArtifactEntry,
+ type ArtifactEntry,
+ type ArtifactReader,
+ type ArtifactReaderTraversal,
+ type ArtifactTraversalOptions,
+} from "./ArtifactReader.js";
+import { abortIfNeeded, hashReadable } from "./ArtifactHash.js";
+import { readArtifactFileChunks } from "./readArtifactFileChunks.js";
+import { ArtifactResourceScope } from "./ArtifactResourceScope.js";
+import { artifactIntegrityDetails } from "../domain/artifactIntegrity.js";
+import { PyInstallerPyzStore } from "./pyinstaller/PyInstallerPyzStore.js";
+import { pyInstallerMemberStream } from "./pyinstaller/PyInstallerMemberStream.js";
+import {
+ findPyInstallerCookie,
+ readCArchiveToc,
+ type CArchiveEntry,
+ type PyInstallerCookie,
+} from "./pyinstaller/CArchiveLayout.js";
+import {
+ hasEncryptionMarker,
+ projectPyzContainer,
+ projectPyzMember,
+ projectStoredMember,
+ sealMember,
+ type PyInstallerMember,
+ type PyzImage,
+} from "./pyinstaller/CArchiveMembers.js";
+import { readPyzArchive } from "./pyinstaller/PyzArchive.js";
+import type { PyInstallerFailure } from "./pyinstaller/PyInstallerFailure.js";
+import { pyInstallerFileAdmissionFailure } from "./pyinstaller/PyInstallerFileAdmission.js";
+
+const MAX_PARSED_FACT_BYTES = 32 * 1024 * 1024;
+const MAX_RETAINED_ENTRY_BYTES = 64 * 1024 * 1024;
+const RECORD_OWNERSHIP_RESERVATION = 256;
+// Conservative allowance for registry/case trie nodes, maps and traversal state.
+// Repeated prefixes are charged again rather than underestimating retained work.
+const PATH_COMPONENT_RESERVATION = 1024;
+
+interface LoadedArchive {
+ readonly members: readonly PyInstallerMember[];
+ readonly traversal: ArtifactReaderTraversal;
+}
+
+/**
+ * Read-only inventory of a PyInstaller CArchive appended to a native executable.
+ * Encrypted PYZ entries are reported and not decoded. Code members are not decompiled.
+ */
+export class PyInstallerArtifactReader implements ArtifactReader {
+ readonly format = "pyinstaller" as const;
+ #source: StableRegularFileDescriptor | undefined;
+ #opening: Promise | undefined;
+ #closed = false;
+ readonly #scope = new ArtifactResourceScope();
+ readonly #cleanup = new ArtifactResourceScope();
+ readonly #lifecycle = new AbortController();
+ #boundSha256: string | undefined;
+ #loaded: Promise | undefined;
+ #budget = 0;
+ #charged = 0;
+ #parsedFactBytes = 0;
+ #retainedEntryBytes = 0;
+ #metadata: PyInstallerMetadata | undefined;
+ readonly #pyzStore = new PyInstallerPyzStore();
+ readonly #members = new Map();
+ readonly #openedPyzSizes = new Map();
+
+ constructor(
+ private readonly path: string,
+ source?: StableRegularFileDescriptor,
+ ) {
+ this.#source = source;
+ this.#cleanup.retain({
+ kind: "lease",
+ resource: "PyInstaller private PYZ images",
+ lease: { release: () => this.#pyzStore.close() },
+ });
+ }
+
+ formatMetadata(): PyInstallerMetadata | undefined {
+ return this.#metadata === undefined ? undefined : { ...this.#metadata };
+ }
+
+ async prepareContainer(
+ expectedSha256: string,
+ signal?: AbortSignal,
+ ): Promise {
+ const active = this.#signal(signal);
+ await this.#scope.run(async () => {
+ const handle = await this.#openHandle(active);
+ await this.#verifySource();
+ const observed =
+ this.#boundSha256 ??
+ (
+ await hashReadable(
+ readArtifactFileChunks(handle, { signal: active }),
+ active,
+ )
+ ).sha256;
+ await this.#verifySource();
+ abortIfNeeded(active);
+ if (observed !== expectedSha256)
+ throw new ArtifactReaderFailure(
+ "integrity",
+ "PyInstaller carrier changed after inventory",
+ undefined,
+ artifactIntegrityDetails(
+ ".",
+ { algorithm: "sha256", digest: expectedSha256 },
+ observed,
+ false,
+ ),
+ );
+ this.#boundSha256 = observed;
+ });
+ }
+
+ async *entries(
+ signal?: AbortSignal,
+ options?: ArtifactTraversalOptions,
+ ): AsyncIterable {
+ const active = this.#signal(signal);
+ const loaded = await this.#scope.run(async () => {
+ const value = await this.#load(active);
+ await this.#verifySource();
+ this.#members.clear();
+ return value;
+ });
+ options?.onCoverage?.(loaded.traversal);
+ for (const member of loaded.members) {
+ abortIfNeeded(active);
+ const sealed = sealMember(member);
+ this.#members.set(sealed.entry.adapterKey, sealed);
+ yield sealed.entry;
+ }
+ }
+
+ async open(entry: ArtifactEntry, signal?: AbortSignal): Promise {
+ const active = this.#signal(signal);
+ return this.#scope.run(async () => {
+ const stored = this.#members.get(entry.adapterKey);
+ if (stored === undefined || !sameArtifactEntry(entry, stored.entry))
+ throw new ArtifactReaderFailure(
+ "integrity",
+ `PyInstaller entry was not produced by this reader: ${entry.path}`,
+ );
+ if (entry.kind !== "file")
+ throw new ArtifactReaderFailure(
+ "format",
+ `PyInstaller entry is not a regular file: ${entry.path}`,
+ );
+ if (entry.encrypted)
+ throw new ArtifactReaderFailure(
+ "unavailable",
+ `PYZ entry is encrypted and was not decoded: ${entry.path}`,
+ );
+ const read = stored.read;
+ if (read === undefined)
+ throw new ArtifactReaderFailure(
+ "format",
+ `PyInstaller entry has no readable bytes: ${entry.path}`,
+ );
+ await this.#verifySource();
+ const knownSize =
+ read.kind === "pyz"
+ ? (read.uncompressedSize ??
+ this.#openedPyzSizes.get(entry.adapterKey))
+ : undefined;
+ if (read.kind === "pyz" && knownSize === undefined)
+ this.#charge(read.header?.length ?? 0);
+ const parts = await this.#pyzStore.memberSource(
+ read,
+ this.#source?.handle,
+ active,
+ );
+ if (active.aborted) {
+ parts.source.destroy();
+ await finished(parts.source, { cleanup: true }).catch(() => undefined);
+ abortIfNeeded(active);
+ }
+ // Keep one close attempt in this read. Failed owners stay in the store
+ // for reader.close() to retry, without losing the original failure.
+ let closeAttempt: Promise | undefined;
+ const closeSource = (primary?: unknown) =>
+ (closeAttempt ??= parts.close?.(primary));
+ const stream = pyInstallerMemberStream(parts.source, {
+ sourceBytes:
+ read.kind === "carchive" ? read.source.compressedSize : read.length,
+ compressed:
+ read.kind === "pyz"
+ ? read.compressed
+ : read.source.compressionFlag === 1,
+ header: read.header,
+ expectedBytes:
+ read.kind === "carchive" ? read.source.uncompressedSize : knownSize,
+ maxBytes:
+ read.kind === "carchive"
+ ? read.source.uncompressedSize
+ : (knownSize ?? this.#budget - this.#charged),
+ signal: active,
+ ...(read.kind === "pyz" && knownSize === undefined
+ ? { charge: (bytes: number) => this.#charge(bytes) }
+ : {}),
+ complete: async (bytes) => {
+ try {
+ await parts.verify?.();
+ await this.#verifySource();
+ abortIfNeeded(active);
+ } catch (cause: unknown) {
+ await closeSource(cause);
+ throw cause;
+ }
+ await closeSource();
+ abortIfNeeded(active);
+ if (read.kind === "pyz" && knownSize === undefined)
+ this.#openedPyzSizes.set(entry.adapterKey, bytes);
+ },
+ });
+ void this.#scope
+ .run(async () => {
+ let primary: unknown;
+ try {
+ await stream.completion;
+ } catch (cause: unknown) {
+ primary = cause;
+ }
+ await closeSource(primary);
+ if (primary !== undefined) throw primary;
+ })
+ .catch((cause: unknown) =>
+ stream.output.destroy(
+ cause instanceof Error
+ ? cause
+ : new Error("PyInstaller stream failed", { cause }),
+ ),
+ );
+ return stream.output;
+ });
+ }
+
+ provenance(): readonly [] {
+ return [];
+ }
+
+ async close(): Promise {
+ this.#closed = true;
+ const closing = new ArtifactReaderFailure(
+ "unavailable",
+ "PyInstaller reader closed",
+ );
+ this.#lifecycle.abort(closing);
+ await this.#scope.close();
+ await this.#cleanup.close();
+ this.#source = undefined;
+ this.#closed = true;
+ this.#loaded = undefined;
+ this.#members.clear();
+ this.#openedPyzSizes.clear();
+ }
+
+ #signal(signal?: AbortSignal): AbortSignal {
+ abortIfNeeded(signal);
+ if (this.#closed)
+ throw new ArtifactReaderFailure(
+ "unavailable",
+ "PyInstaller archive is closed",
+ );
+ return signal === undefined
+ ? this.#lifecycle.signal
+ : AbortSignal.any([signal, this.#lifecycle.signal]);
+ }
+
+ async #load(signal?: AbortSignal): Promise {
+ this.#loaded ??= this.#parse(signal);
+ return this.#loaded;
+ }
+
+ async #parse(signal?: AbortSignal): Promise {
+ const handle = await this.#openHandle(signal);
+ await this.#verifySource();
+ const size = (await handle.stat()).size;
+ const cookie = await this.#cookie(handle, size, signal);
+ this.#budget = artifactExpansionBudgetBytes(cookie.packageLength);
+ const toc = await this.#toc(handle, cookie, signal);
+ for (const entry of toc) {
+ this.#charge(entry.uncompressedSize);
+ this.#reserveMetadata({ ...entry }, false);
+ }
+ const images = await this.#pyzImages(handle, cookie, toc, signal);
+ const magic = scriptPythonMagic(
+ cookie,
+ [...images.values()].map((image) => image.archive),
+ );
+ const encrypted = hasEncryptionMarker(toc);
+ const members = await this.#project(
+ toc,
+ cookie,
+ images,
+ magic,
+ encrypted,
+ signal,
+ );
+ for (const member of members)
+ if (member.read?.kind === "carchive")
+ this.#charge(member.read.header?.length ?? 0);
+ await this.#verifySource();
+ this.#metadata = cookieMetadata(cookie, toc, encrypted);
+ let opaqueImages = 0;
+ for (const image of images.values())
+ if (image.archive.kind === "opaque") opaqueImages++;
+ return {
+ members,
+ traversal: {
+ status: opaqueImages === 0 ? "complete" : "unknown",
+ limitations:
+ opaqueImages === 0
+ ? []
+ : [
+ `${String(opaqueImages)} PYZ archives remain opaque; module traversal is unknown. See opaque PYZ occurrences for source magic and per-archive limitations.`,
+ ],
+ },
+ };
+ }
+
+ async #openHandle(signal?: AbortSignal): Promise {
+ abortIfNeeded(signal);
+ if (this.#closed)
+ throw new ArtifactReaderFailure(
+ "unavailable",
+ "PyInstaller archive is closed",
+ );
+ if (this.#source !== undefined) return this.#source.handle;
+ // Admission belongs to the reader. A departing caller cannot cancel a peer
+ // or replace a late descriptor owner; every caller joins the same admission.
+ this.#opening ??= this.#admit();
+ let handle: FileHandle;
+ try {
+ handle = await this.#opening;
+ } catch (cause: unknown) {
+ if (
+ cause instanceof ArtifactReaderFailure &&
+ cause.reason === "cancelled" &&
+ signal?.aborted
+ )
+ throw new ArtifactReaderFailure("cancelled", cause.message, {
+ cause: signal.reason,
+ cleanup: cause.cleanup,
+ cleanupLeases: cause.cleanupLeases,
+ });
+ throw cause;
+ }
+ abortIfNeeded(signal);
+ return handle;
+ }
+
+ async #admit(): Promise {
+ try {
+ const handle = await openRegularFile(this.path, {
+ symlinks: "reject",
+ signal: this.#lifecycle.signal,
+ });
+ this.#retainCarrier(new OwnedFileHandle(handle));
+ this.#source = { handle, initial: await handle.stat() };
+ return handle;
+ } catch (cause: unknown) {
+ if (cause instanceof RegularFileAdmissionFailure)
+ this.#retainCarrier(cause.owner);
+ throw pyInstallerFileAdmissionFailure(this.path, cause);
+ }
+ }
+
+ #retainCarrier(handle: OwnedFileHandle): void {
+ this.#cleanup.retain({ kind: "file-handle", resource: this.path, handle });
+ }
+
+ async #cookie(
+ handle: FileHandle,
+ size: number,
+ signal?: AbortSignal,
+ ): Promise {
+ const found = await findPyInstallerCookie(handle, size, signal);
+ if (found === undefined)
+ throw new ArtifactReaderFailure(
+ "format",
+ "PyInstaller CArchive cookie was not found",
+ );
+ if (!found.ok) throw failure(found.error);
+ return found.value;
+ }
+
+ async #toc(
+ handle: FileHandle,
+ cookie: PyInstallerCookie,
+ signal?: AbortSignal,
+ ): Promise {
+ const toc = await readCArchiveToc(handle, cookie, signal);
+ if (!toc.ok) throw failure(toc.error);
+ return toc.value;
+ }
+
+ async #pyzImages(
+ handle: FileHandle,
+ cookie: PyInstallerCookie,
+ toc: readonly CArchiveEntry[],
+ signal: AbortSignal | undefined,
+ ): Promise> {
+ const images = new Map();
+ // Admit the complete filesystem footprint before the first private write.
+ this.#pyzStore.reserveImages(
+ toc.reduce(
+ (count, entry) =>
+ count +
+ (entry.typeCode === "z" && entry.compressionFlag === 1 ? 1 : 0),
+ 0,
+ ),
+ );
+ for (const entry of toc) {
+ if (entry.typeCode !== "z") continue;
+ abortIfNeeded(signal);
+ const spooled =
+ entry.compressionFlag === 0
+ ? undefined
+ : await this.#pyzStore.spool(
+ handle,
+ cookie.archiveStart,
+ entry,
+ signal,
+ );
+ const source = spooled ?? {
+ handle,
+ start: cookie.archiveStart + entry.offset,
+ };
+ const parsed = await readPyzArchive(
+ source.handle,
+ source.start,
+ entry.uncompressedSize,
+ signal,
+ );
+ if (!parsed.ok) throw failure(parsed.error);
+ if (parsed.value.kind === "entries")
+ for (const item of parsed.value.entries)
+ this.#reserveMetadata({ ...item, container: entry.name }, false);
+ if (spooled !== undefined) await this.#pyzStore.release(spooled.handle);
+ images.set(entry, {
+ archive: parsed.value,
+ handle: source.handle,
+ start: source.start,
+ });
+ }
+ return images;
+ }
+
+ async #project(
+ toc: readonly CArchiveEntry[],
+ cookie: PyInstallerCookie,
+ images: ReadonlyMap,
+ magic: Buffer | undefined,
+ encrypted: boolean,
+ signal: AbortSignal | undefined,
+ ): Promise {
+ const members: PyInstallerMember[] = [];
+ const append = (member: PyInstallerMember): void => {
+ this.#reservePathComponents(member.entry.path);
+ const entry = {
+ ...member.entry,
+ path: normalizeArtifactPath(member.entry.path),
+ };
+ this.#reserveMetadata(
+ {
+ path: entry.path,
+ adapter_key: entry.adapterKey,
+ format_metadata: entry.formatMetadata ?? null,
+ limitations: [...entry.limitations],
+ },
+ true,
+ );
+ members.push({ ...member, entry });
+ };
+ for (const entry of toc) {
+ if (members.length % 256 === 0) await setImmediate();
+ abortIfNeeded(signal);
+ const image = entry.typeCode === "z" ? images.get(entry) : undefined;
+ if (image === undefined) {
+ append(projectStoredMember(entry, cookie.archiveStart, magic));
+ continue;
+ }
+ append(projectPyzContainer(entry, cookie.archiveStart, image, encrypted));
+ if (image.archive.kind !== "entries") continue;
+ for (const item of image.archive.entries) {
+ if (members.length % 256 === 0) await setImmediate();
+ abortIfNeeded(signal);
+ append(
+ projectPyzMember(
+ entry,
+ item,
+ encrypted,
+ image.archive.pythonMagic,
+ item.namespace || encrypted
+ ? undefined
+ : {
+ handle: image.handle,
+ offset: image.start + item.offset,
+ length: item.length,
+ },
+ ),
+ );
+ }
+ }
+ return members;
+ }
+
+ async #verifySource(): Promise {
+ const source = this.#source;
+ if (source === undefined)
+ throw new ArtifactReaderFailure(
+ "unavailable",
+ "PyInstaller archive is closed",
+ );
+ const [opened, currentPath] = await Promise.all([
+ source.handle.stat(),
+ lstat(this.path),
+ ]);
+ if (
+ !sameRegularFileState(source.initial, opened) ||
+ !sameRegularFileState(source.initial, currentPath)
+ )
+ throw new ArtifactReaderFailure(
+ "integrity",
+ `PyInstaller source changed during reading: ${this.path}`,
+ );
+ }
+
+ #reservePathComponents(path: string): void {
+ const remaining = MAX_RETAINED_ENTRY_BYTES - this.#retainedEntryBytes;
+ // Case annotations can carry the path and two prefixes at every component.
+ // Reserve escaped text growth as well as registry/case trie nodes and maps.
+ const componentCost =
+ PATH_COMPONENT_RESERVATION + 6 * jsonValueByteLength(path);
+ let reservation = componentCost;
+ for (
+ let index = 0;
+ index < path.length && reservation <= remaining;
+ index++
+ )
+ if (path.charCodeAt(index) === 47) reservation += componentCost;
+ if (reservation > remaining)
+ throw new ArtifactReaderFailure(
+ "limit",
+ "PyInstaller paths exceed their retained namespace budget",
+ { boundary: "memory" },
+ );
+ this.#retainedEntryBytes += reservation;
+ }
+
+ #reserveMetadata(value: JsonValue, projected: boolean): void {
+ const used = projected ? this.#retainedEntryBytes : this.#parsedFactBytes;
+ const limit = projected ? MAX_RETAINED_ENTRY_BYTES : MAX_PARSED_FACT_BYTES;
+ const remaining = limit - used - RECORD_OWNERSHIP_RESERVATION;
+ const bytes = remaining < 0 ? 0 : jsonValueByteLength(value, remaining);
+ if (remaining < 0 || bytes > remaining)
+ throw new ArtifactReaderFailure(
+ "limit",
+ "PyInstaller directory facts exceed their retained metadata budget",
+ { boundary: "memory" },
+ );
+ if (projected)
+ this.#retainedEntryBytes += bytes + RECORD_OWNERSHIP_RESERVATION;
+ else this.#parsedFactBytes += bytes + RECORD_OWNERSHIP_RESERVATION;
+ }
+
+ #charge(bytes: number): void {
+ if (
+ !Number.isSafeInteger(bytes) ||
+ bytes < 0 ||
+ this.#charged + bytes > this.#budget
+ )
+ throw new ArtifactReaderFailure(
+ "limit",
+ `PyInstaller members declare ${String(this.#charged + Math.max(0, bytes))} uncompressed bytes, exceeding the decompression budget`,
+ );
+ this.#charged += bytes;
+ }
+}
+
+const failure = (error: PyInstallerFailure): ArtifactReaderFailure =>
+ new ArtifactReaderFailure(error.reason, error.message);
diff --git a/src/artifacts/extraction/ArtifactExtraction.test.ts b/src/artifacts/extraction/ArtifactExtraction.test.ts
new file mode 100644
index 0000000000..d22855d6e6
--- /dev/null
+++ b/src/artifacts/extraction/ArtifactExtraction.test.ts
@@ -0,0 +1,54 @@
+import { writeFile } from "node:fs/promises";
+import { join } from "node:path";
+
+import { expect, it, vi } from "vitest";
+
+import { ArtifactReaderFailure } from "../ArtifactReader.js";
+import { ArtifactResourceScope } from "../ArtifactResourceScope.js";
+import { OwnedFileHandle } from "../../filesystem/OwnedFileHandle.js";
+import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
+import { extractArtifact } from "./ArtifactExtraction.js";
+
+vi.mock("../pyinstaller/CArchiveLayout.js", async (importOriginal) => ({
+ ...(await importOriginal