nanodesu 1.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,255 @@
1
+ Metadata-Version: 2.4
2
+ Name: nanodesu
3
+ Version: 1.3.0
4
+ Summary: Unpack and repack PyInstaller single-file executables (standard library only)
5
+ License: MIT
6
+ Keywords: pyinstaller,unpacker,reverse-engineering,carchive,pyz
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Dynamic: license-file
11
+
12
+ # Nanodesu!
13
+
14
+ [![tests](https://github.com/Nesarf/Nanodesu/actions/workflows/tests.yml/badge.svg)](https://github.com/Nesarf/Nanodesu/actions/workflows/tests.yml)
15
+
16
+ Unpack and repack PyInstaller single-file executables — all the way down.
17
+
18
+ Standard library only. No network access, and the target program is never
19
+ executed. The name is a fan catchphrase; it carries no technical meaning.
20
+
21
+ **New here?** → [`QUICKSTART.md`](QUICKSTART.md) opens with the situation this tool is for
22
+ and three commands you can run right now.
23
+
24
+ ---
25
+
26
+ ## What it does
27
+
28
+ A PyInstaller onefile executable is laid out like this:
29
+
30
+ ```
31
+ [ bootloader stub ][ CArchive: header + payload + table of contents ][ 88-byte cookie ]
32
+ ```
33
+
34
+ `Nanodesu!` reads that structure, takes every embedded file out of it, and can
35
+ put the whole thing back together again — every entry's **contents** identical,
36
+ with the table of contents preserved in its original shape.
37
+
38
+ Two different things get called "faithful", and they are worth separating:
39
+
40
+ | Claim | True? |
41
+ |---|---|
42
+ | every entry decompresses to the same bytes, TOC order/types/flags preserved, repacked exe runs | **yes** — this is what the tests check |
43
+ | the repacked file is **bit-identical** to the original | **no** — the payload is recompressed with zlib level 9, so the compressed bytes differ even when nothing was modified |
44
+
45
+ The distinction matters when you are comparing a repacked sample to a reference by hash: the
46
+ contents will match and the file will not.
47
+
48
+ | Command | Purpose |
49
+ |---|---|
50
+ | `info` | summarise the archive: python version, sizes, entry count |
51
+ | `ls` | list the table of contents (filter, sort, top-N) |
52
+ | `tree` | directory tree with per-branch sizes |
53
+ | `search` | search entry names, optionally entry contents |
54
+ | `cat` | write one entry to stdout or a file |
55
+ | `extract` | unpack everything and rebuild the runtime directory layout |
56
+ | `verify` | decompress every entry and check its length |
57
+ | `pyz` | unpack a PYZ archive into per-module bytecode |
58
+ | `build` | repack an extracted directory back into an executable |
59
+
60
+ ## Install
61
+
62
+ Nothing to install: it is a single file using only the standard library.
63
+
64
+ ```bash
65
+ python nanodesu.py info path/to/app.exe
66
+ ```
67
+
68
+ Requires Python 3.9 or newer.
69
+
70
+ ## Usage
71
+
72
+ ```bash
73
+ # overview
74
+ python nanodesu.py info path/to/app.exe
75
+
76
+ # explore
77
+ python nanodesu.py tree path/to/app.exe --depth 2
78
+ python nanodesu.py ls path/to/app.exe --sort size --top 30
79
+ python nanodesu.py search path/to/app.exe "PySide6" --content
80
+ python nanodesu.py cat path/to/app.exe main -o main.pyc
81
+
82
+ # unpack: rebuilds the runtime layout and writes _archive_manifest.json
83
+ python nanodesu.py extract path/to/app.exe -o out/app --pyc
84
+
85
+ # check the archive is intact
86
+ python nanodesu.py verify path/to/app.exe
87
+
88
+ # unpack the PYZ archive inside it (if the build has one)
89
+ # modules come out as valid .pyc files; --bare keeps the stored bytes untouched
90
+ python nanodesu.py pyz out/app/PYZ.pyz -o out/app/_pyz_modules
91
+
92
+ # put it back together
93
+ python nanodesu.py build out/app -o out/app_repacked.exe --pylib python312.dll
94
+ ```
95
+
96
+ `extract` writes `_archive_manifest.json` alongside the files. `build` prefers
97
+ that manifest, which is what makes the repack faithful: original names, original
98
+ type codes, original compression flags and the original entry order. Without a
99
+ manifest it falls back to inferring everything from the directory.
100
+
101
+ `build` takes two optional switches, both recovered automatically when omitted:
102
+
103
+ | Switch | Meaning | Default |
104
+ |---|---|---|
105
+ | `--pylib NAME` | value written into the cookie's python library field | the `python3*.dll` found in the tree |
106
+ | `--pyver N` | python version, as `major*100 + minor` (312 for 3.12) | parsed from that library name |
107
+
108
+ Both are recorded in the manifest, so repacking an `extract` output needs no
109
+ flags at all.
110
+
111
+ ### The `--pyc` flag
112
+
113
+ Archives store python modules as bare marshalled code objects, with no `.pyc`
114
+ header. `extract --pyc` prepends a valid header so decompilers and other tooling
115
+ accept them directly; the same applies to `pyz`, which derives the header from
116
+ the PYZ archive's own bytecode magic. The number of prepended bytes is recorded in the manifest, and
117
+ `build` strips exactly that many again — so the same extracted tree both feeds
118
+ decompilers and repacks cleanly.
119
+
120
+ To find the exact bytecode magic for a target version, the tool looks for a
121
+ matching interpreter on `PATH` or in a few common locations, then falls back to
122
+ a built-in table. Point it somewhere specific with:
123
+
124
+ ```bash
125
+ NANODESU_PYTHON="C:/tools/python-{ver}/python.exe" python nanodesu.py extract ...
126
+ ```
127
+
128
+ `{ver}` is replaced by the version string, e.g. `3.12`.
129
+
130
+ ## As a library
131
+
132
+ The commands print prose for a person and return an exit code. That is the right shape for a
133
+ CLI and the wrong shape for a caller — anything that captured that output and parsed it back
134
+ would be coupled to the wording of a report. So there is a small API that returns data instead:
135
+
136
+ ```python
137
+ import nanodesu
138
+
139
+ nanodesu.inspect_archive("sample.exe")
140
+ # {'entry_count': 836, 'python_version': '3.12', 'stub_size': 382464,
141
+ # 'integrity': [], 'total_uncompressed': 240251704, ...}
142
+
143
+ nanodesu.list_entries("sample.exe") # [{'name': 'struct', 'tcode': 'm', ...}, ...]
144
+ nanodesu.read_file("sample.exe", "struct") # b'...' (KeyError if there is no such entry)
145
+ nanodesu.verify_archive("sample.exe") # {'ok': True, 'decompressed': 836, 'problems': []}
146
+ nanodesu.extract("sample.exe", "out/") # {'ok': True, 'written': 836, 'confined': [], ...}
147
+ ```
148
+
149
+ | function | returns |
150
+ |---|---|
151
+ | `inspect_archive(target)` | the facts `info` prints, as a dict |
152
+ | `list_entries(target)` | the table of contents, in archive order |
153
+ | `read_file(target, name)` | one entry's bytes |
154
+ | `verify_archive(target, read=True)` | what `verify` reports, as data (`read=False` skips decompression) |
155
+ | `extract(target, out_dir, pyc=True, ...)` | a summary, having written a repackable tree |
156
+
157
+ Where a command would print a message and exit, these raise **`PyInstallerError`** — which
158
+ carries the path it was given — so a caller can tell "this is not an archive" from "this file
159
+ is missing". `read_file` raises `KeyError` for an absent name.
160
+
161
+ `extract()` writes the same `_archive_manifest.json` the command writes, and the manifest it
162
+ produces is the one `build` accepts: extracting through the API and repacking through the CLI
163
+ round-trips. That is a contract, and a test holds it.
164
+
165
+ Importing the module prints nothing. Nothing here executes the target.
166
+
167
+ ## Verified behaviour
168
+
169
+ Against a 138.8 MB onefile build (PyInstaller 6.x, Python 3.12, PySide6):
170
+
171
+ * **836 entries extracted, 0 failures**
172
+ * `verify` decompresses all 836 entries and their lengths match
173
+ * unpack → repack → unpack again reproduces **all 836 entry contents byte for byte**
174
+ (the repacked file itself is not bit-identical: see the note above)
175
+ (SHA-256 identical, including the embedded PYZ archive)
176
+ * the repacked executable **starts and runs normally**
177
+
178
+ ## Testing
179
+
180
+ The test suite builds small synthetic archives in a temporary directory, so it
181
+ runs in well under a second and needs no sample executable:
182
+
183
+ ```bash
184
+ python -m unittest discover -s test -v
185
+ ```
186
+
187
+ It covers archive parsing, the `extract` → `build` round trip, the `--pyc`
188
+ header handling, contents-directory placement, PYZ extraction, and the error
189
+ paths. The three format details listed under "Three details that break
190
+ everything if changed back" each have a dedicated regression test.
191
+
192
+ Continuous integration runs the suite on Linux and Windows across Python
193
+ 3.9, 3.12, 3.13 and 3.14 (see `.github/workflows/tests.yml`).
194
+
195
+ ## Format notes
196
+
197
+ The format below was derived from the on-disk layout. The repacking rules are the
198
+ constraints that make the write path correct: each one corresponds to a way a repack
199
+ fails, so none of them is optional.
200
+
201
+ | Item | Detail |
202
+ |---|---|
203
+ | cookie | `!8sIIII64s`, 88 bytes: `magic(8) pkg_len(4) toc_off(4) toc_len(4) pyver(4) pylib(64)` |
204
+ | archive start | `base = cookie_pos + 88 - pkg_len` — `pkg_len` **includes** the 88-byte cookie |
205
+ | TOC entry | `!IIIIBc`: 18-byte header followed by a NUL-padded name |
206
+ | `entry_length` | total size of the record including the 18-byte header, padded to a multiple of 16 |
207
+ | `offset` | measured from the archive start, as is `toc_offset` |
208
+ | compression | flag `0` = stored, `1` = zlib over the whole blob |
209
+ | type codes | `b` binary, `x` dependency, `m` module, `s` source, `z` **PYZ archive**, `Z` plain zipfile, `o` option, `d` data, `n` symlink |
210
+ | option entries | value lives in the name, payload length is zero, e.g. `pyi-contents-directory _internal` |
211
+ | module payloads | bare `marshal` code objects; `extract --pyc` and `pyz` add a valid `.pyc` header |
212
+ | PYZ archive | `PYZ\0` + 4-byte bytecode magic + `int32` TOC offset + 5 reserved bytes; TOC is a marshalled list; item types `0` module, `1` package, `2` legacy data, `3` namespace package |
213
+ | repacking | `pkg_len = len(PKG body) + 88`; `toc_off = 88 + payload length`; entry offsets are relative to the PKG start |
214
+
215
+ ### Three details that break everything if changed back
216
+
217
+ 1. **A PYZ archive uses lowercase `z`.** Uppercase `Z` is a plain zipfile entry.
218
+ The parser must accept lowercase `z`. If it does not, the table walk stops
219
+ before its final entry; the repacked executable then has no PYZ archive and
220
+ refuses to start with `PYZ archive entry not found in the TOC!`.
221
+ 2. **PYZ item type `0` is an ordinary module.** Type `3` is the namespace
222
+ package with no code object. Skipping type `0` as if it were a namespace
223
+ package silently reduces the output to package `__init__` files only.
224
+ 3. **Headers prepended by `--pyc` must be stripped on repack**, using the
225
+ per-entry `pyc_header` length from the manifest. Otherwise the marshalled
226
+ code object stored in the archive is corrupted.
227
+
228
+ ## Notes and limits
229
+
230
+ * **Onefile builds only.** The file must end with the `MEI` cookie. A onedir
231
+ executable carries the same PKG section, but only the bootstrap half of it.
232
+ * Other packers (Nuitka, py2exe, cx_Freeze, ...) are not supported.
233
+ * Decompiling bytecode is a separate problem: code objects are tied to the
234
+ interpreter version that produced them. `--pyc` produces valid input for
235
+ decompilers, but the output of any decompiler should be treated as a
236
+ hypothesis, not as the original source.
237
+ * Repacking a large archive is I/O bound and can take a few minutes on slow
238
+ storage.
239
+ * A module and a package can claim the same path inside a PYZ archive (for
240
+ example `utils` and `utils.sub`). The extractor detects that and writes the
241
+ later one under a flattened name rather than overwriting it.
242
+ * `pyz` writes valid `.pyc` files (the header is derived from the PYZ bytecode
243
+ magic, so nothing is guessed). Use `--bare` for the untouched stored bytes.
244
+ * Non-archive input, missing paths and directories all fail with a one-line
245
+ message and exit status 1; Python tracebacks are not shown to the CLI user.
246
+
247
+ ## Tools
248
+
249
+ * `tools/charlayer.py` — a layered character-cell canvas for Qt, used for
250
+ per-character text effects (jitter, displacement, scanlines, reveal). It has
251
+ a Qt-free logic self-test: `python tools/charlayer.py`.
252
+
253
+ ## License
254
+
255
+ MIT — see `LICENSE`.
@@ -0,0 +1,7 @@
1
+ nanodesu.py,sha256=RueeRHuL-WBCS17Kil5hI0ShJ-VjYK_X-b1c_A9Z_9I,55794
2
+ nanodesu-1.3.0.dist-info/licenses/LICENSE,sha256=QEqPPBeae-W0t32KPBhjFAHunonqftQI29mXoBSWDBw,1079
3
+ nanodesu-1.3.0.dist-info/METADATA,sha256=bKnTITWq4n8kCR75dZwXAP7QoLWg3S4Gv6tLw4SPA-M,11814
4
+ nanodesu-1.3.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
5
+ nanodesu-1.3.0.dist-info/entry_points.txt,sha256=UFPQzkpv3TYr5tfuRYKV-v6Xtu6TK0nVEmSXjeB7ogE,43
6
+ nanodesu-1.3.0.dist-info/top_level.txt,sha256=XGRNVFp8yLFdc5JlZeE2KpMTKsxEy8TeBX2smpzz9jw,9
7
+ nanodesu-1.3.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ nanodesu = nanodesu:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nanodesu! contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ nanodesu