pyc_revive
.pyc -> .py reconstructor. no uncompyle6. no pycdc. no xdis.
use
python -m pyc_revive my file.pyc
python -m pyc_revive ./dist/app.pyc --out ./revived.py
input handling: exact path, spaces with no quotes, basename search in cwd + all subfolders + parent folder.
versions
every CPython from 2.0 through 3.15 resolves by magic number
(pymagic.py, full table + nearest fallback + header sizes).
two backends, one pipeline:
- native:
.pycmatches the running interpreter. stdlib marshal loads the code object, stdlib dis disassembles. zero dependencies. - pycdas: anything else. the bundled
pycdas.exedisassembles the foreign version,pycdas_parser.pyparses the dump into code trees,shim.pyadapts them to the pipeline. everything downstream (normalize, segment, translate, CFG, emit, verify x3) runs unchanged.
routing is by magic number, decided before any bytecode is touched: a foreign magic goes straight to pycdas. marshal can load foreign payloads without complaint (the format is stable), and letting stdlib dis chew on foreign opcodes mis-decodes jumps and can kill the process instead of raising -- so "try native, fall back on error" is not used for foreign files. verified live on 3.14 (see below).
3.14 support
3.14 was the first foreign 3.11+ target exercised end to end, and it needed real work beyond the opcode table:
- new ops in the translator:
LOAD_SMALL_INT,LOAD_FAST_BORROW,LOAD_FAST_BORROW_LOAD_FAST_BORROW(double 4-bit index, high nibble first),LOAD_SPECIAL,FORMAT_SIMPLE,TO_BOOL(transparent),CALL_KW(kwargs split likeCALL_FUNCTION_KW, NULL-skipped). pycdas_parser.pyreads[Locals+Names]sections into varnames and parses[Exception Table]entries (S to E -> T [depth] (lasti?)), which are re-encoded toco_exceptiontablebytes on the shim so the shared try/with-region machinery runs unchanged.- the bundled pycdas prints 3.14 conditional-branch targets 2 bytes low
(hidden CACHE it suppresses but forgets in target math -- proven three
ways: dead else bodies, inverted with-cleanup, stack-underflow
RETURN).shim.pyrepairs them when the fingerprint is exact (3.14+ dump,NOT_TAKENat branch+4, target+2 is a real offset). - 3.14 dropped
BEFORE_WITHfor an inline prologue (COPY, LOAD_SPECIAL, SWAP, SWAP, LOAD_SPECIAL, CALL); the shim folds it back to one syntheticBEFORE_WITH. - with-bodies are scoped by their table range (fallthrough code after
the body dedents instead of hiding under
with), try interiors stop at the body end (post-try joins don't indent undertry:), and post-try joins stranded textually before out-of-line handlers are deferred past theexceptclause. RERAISE-ended handlers that pair with nothing are skipped as implicit-cleanup residue. - duplicated
finallybodies pair by twin match: an unpaired RERAISE-ended handler whose leading statements duplicate a post-try block's leading statements is the exceptional finally copy (tails decide the split --[.., return]vs[.., raise]means the return is post-statement). The normal copy splits intofinally:+ rest. the body dedents instead of hiding underwith), try interiors stop at the body end (post-try joins don't indent undertry:), and post-try joins stranded textually before out-of-line handlers are deferred past theexceptclause. RERAISE-ended handlers that pair with nothing are skipped as implicit-cleanup residue.
routing is by magic number, decided before any bytecode is touched: a foreign magic goes straight to pycdas. marshal can load foreign payloads without complaint (the format is stable), and letting stdlib dis chew on foreign opcodes mis-decodes jumps and can kill the process instead of raising -- so "try native, fall back on error" is not used for foreign files. verified live on 3.14 (see below).
3.14 support
3.14 was the first foreign 3.11+ target exercised end to end, and it needed real work beyond the opcode table:
- new ops in the translator:
LOAD_SMALL_INT,LOAD_FAST_BORROW,LOAD_FAST_BORROW_LOAD_FAST_BORROW(double 4-bit index, high nibble first),LOAD_SPECIAL,FORMAT_SIMPLE,TO_BOOL(transparent),CALL_KW(kwargs split likeCALL_FUNCTION_KW, NULL-skipped). pycdas_parser.pyreads[Locals+Names]sections into varnames and parses[Exception Table]entries (S to E -> T [depth] (lasti?)), which are re-encoded toco_exceptiontablebytes on the shim so the shared try/with-region machinery runs unchanged.- the bundled pycdas prints 3.14 conditional-branch targets 2 bytes low
(hidden CACHE it suppresses but forgets in target math -- proven three
ways: dead else bodies, inverted with-cleanup, stack-underflow
RETURN).shim.pyrepairs them when the fingerprint is exact (3.14+ dump,NOT_TAKENat branch+4, target+2 is a real offset). - 3.14 dropped
BEFORE_WITHfor an inline prologue (COPY, LOAD_SPECIAL, SWAP, SWAP, LOAD_SPECIAL, CALL); the shim folds it back to one syntheticBEFORE_WITH. - with-bodies are scoped by their table range (fallthrough code after
the body dedents instead of hiding under
with), try interiors stop at the body end (post-try joins don't indent undertry:), and post-try joins stranded textually before out-of-line handlers are deferred past theexceptclause. RERAISE-ended handlers that pair with nothing are skipped as implicit-cleanup residue. - duplicated
finallybodies pair by twin match: an unpaired RERAISE-ended handler whose leading statements duplicate a post-try block's leading statements is the exceptional finally copy (tails decide the split --[.., return]vs[.., raise]means the return is post-statement). The normal copy splits intofinally:+ rest. the body dedents instead of hiding underwith), try interiors stop at the body end (post-try joins don't indent undertry:), and post-try joins stranded textually before out-of-line handlers are deferred past theexceptclause. RERAISE-ended handlers that pair with nothing are skipped as implicit-cleanup residue.
pipeline
- find file (
finder.py) - detect version via magic (
pymagic.py) - load: native marshal, else pycdas backend (
loader.py,pycdas_parser.py,shim.py) - disassemble + abstract (
bytecode.py:Inst,EditableBytecode, opcode/oparg/value-stack/metadata tables) - normalize / mask consts + names (
normalizer.py) - opcode-aware B/I/E segmentation (
segmenter.py) - statement translation (
translator.py: stack machine, defaults recovery, lambdas inline, 2.x print/exec/class ops) - control-flow graph from jumps (
cfg.py: if/for/while/try/except-as (bareexcept:too)/with/finally, clauses + clusters, 3.11+ exception table, SETUP-region splits) - emit + repairs (
emitter.py: source-order splicing onto def/class stub lines, bases recovery,@decoratorrestore,globaldecls, cond chains, ternaries, nested if/else, loop ranges, break/continue recovery, indent + suite-guard passes) - equivalence verify, 3 sweeps (
verifier.py: recompile revived source, compare opcode structure, repair, repeat x3)
layout
pyc_revive/
finder.py pymagic.py loader.py pycdas_parser.py shim.py
bytecode.py normalizer.py segmenter.py translator.py cfg.py
emitter.py verifier.py autofix.py reconstructor.py cli.py
deps.py __main__.py pycdas.exe
known limits
-
inline comprehensions/genexps render as
func(...)placeholders. -
trybodies that fall through into always-run handlers keep flat order instead of guarded flow, unless an exception table is available (native 3.11+ and foreign dumps with[Exception Table]sections scope bodies, pair handlers, and defer stranded post-try joins properly). -
exotic decorators reduce to bare defs; exotic call shapes degrade to commented disassembly rather than failing.
-
pre-3.8 default-argument recovery is best-effort (flag-style MAKE from 3.8+ recovers exactly).
-
verifier scores dip across versions: opcode sets drift between releases, so cross-version similarity reads lower than same-version.
-
3.14
FOR_ITERtargets are untested (no loop in any 3.14 sample yet); if the bundled pycdas hides caches after it the same way it does for conditional branches, loop joins may land 2 bytes off.
Metadata
Release files for pyc-revive 1.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pyc_revive-1.0.1.tar.gz | 122.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pyc_revive-1.0.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 247.6 kB
Release files / pyc_revive-1.0.1.tar.gz
| Download URL | pyc_revive-1.0.1.tar.gz |
|---|---|
| Size | 122.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c8068f46fcec8c926ffdcc9af5536eb09764da795d04bdd40ef73866c6e6aeab
|
|
BLAKE2b-256 checksum How to use checksums |
cc5a6119b52b3c83923e015adb8735d6aec77807e16215d8fd48cee8d9fbe82e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.2
|
Release files / pyc_revive-1.0.1-py3-none-any.whl
| Download URL | pyc_revive-1.0.1-py3-none-any.whl |
|---|---|
| Size | 125.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
50b04db29a79a254c3a5cf2a6e85b7dec320adca7236d793a55059d22987d2df
|
|
BLAKE2b-256 checksum How to use checksums |
2a863d243cf94eeb95112759383a01723b1ac4247ccf56a293ebef4b06b640ba
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.2
|