Metadata-Version: 2.4
Name: pyc-revive
Version: 1.0.1
Summary: Stdlib-only .pyc -> .py reconstructor with native and pycdas backends
Author: opsecalyzer
License-Expression: MIT
Project-URL: Homepage, https://github.com/opsecalyzer/pyc_revive
Project-URL: Source, https://github.com/opsecalyzer/pyc_revive
Keywords: pyc,decompiler,bytecode,reverse-engineering,malware-analysis
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Information Technology
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Disassemblers
Classifier: Operating System :: OS Independent
Requires-Python: >=3.9
Description-Content-Type: text/markdown

# pyc_revive

`.pyc` -> `.py` reconstructor. no uncompyle6. no pycdc. no xdis.

## use

```bash
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: `.pyc` matches the running interpreter. stdlib marshal loads
  the code object, stdlib dis disassembles. zero dependencies.
- pycdas: anything else. the bundled `pycdas.exe` disassembles the
  foreign version, `pycdas_parser.py` parses the dump into code trees,
  `shim.py` adapts 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 like `CALL_FUNCTION_KW`, NULL-skipped).
- `pycdas_parser.py` reads `[Locals+Names]` sections into varnames and
  parses `[Exception Table]` entries (`S to E -> T [depth] (lasti?)`),
  which are re-encoded to `co_exceptiontable` bytes 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.py` repairs them when the fingerprint is exact
  (3.14+ dump, `NOT_TAKEN` at branch+4, target+2 is a real offset).
- 3.14 dropped `BEFORE_WITH` for an inline prologue
  (`COPY, LOAD_SPECIAL, SWAP, SWAP, LOAD_SPECIAL, CALL`); the shim folds
  it back to one synthetic `BEFORE_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 under `try:`), and
  post-try joins stranded textually before out-of-line handlers are
  deferred past the `except` clause. RERAISE-ended handlers that pair
  with nothing are skipped as implicit-cleanup residue.
- duplicated `finally` bodies 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 into `finally:` + rest.
  the body dedents instead of hiding under `with`), try interiors stop
  at the body end (post-try joins don't indent under `try:`), and
  post-try joins stranded textually before out-of-line handlers are
  deferred past the `except` clause. 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 like `CALL_FUNCTION_KW`, NULL-skipped).
- `pycdas_parser.py` reads `[Locals+Names]` sections into varnames and
  parses `[Exception Table]` entries (`S to E -> T [depth] (lasti?)`),
  which are re-encoded to `co_exceptiontable` bytes 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.py` repairs them when the fingerprint is exact
  (3.14+ dump, `NOT_TAKEN` at branch+4, target+2 is a real offset).
- 3.14 dropped `BEFORE_WITH` for an inline prologue
  (`COPY, LOAD_SPECIAL, SWAP, SWAP, LOAD_SPECIAL, CALL`); the shim folds
  it back to one synthetic `BEFORE_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 under `try:`), and
  post-try joins stranded textually before out-of-line handlers are
  deferred past the `except` clause. RERAISE-ended handlers that pair
  with nothing are skipped as implicit-cleanup residue.
- duplicated `finally` bodies 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 into `finally:` + rest.
  the body dedents instead of hiding under `with`), try interiors stop
  at the body end (post-try joins don't indent under `try:`), and
  post-try joins stranded textually before out-of-line handlers are
  deferred past the `except` clause. RERAISE-ended handlers that pair
  with nothing are skipped as implicit-cleanup residue.
## pipeline

1. find file (`finder.py`)
2. detect version via magic (`pymagic.py`)
3. load: native marshal, else pycdas backend (`loader.py`,
   `pycdas_parser.py`, `shim.py`)
4. disassemble + abstract (`bytecode.py`: `Inst`, `EditableBytecode`,
   opcode/oparg/value-stack/metadata tables)
5. normalize / mask consts + names (`normalizer.py`)
6. opcode-aware B/I/E segmentation (`segmenter.py`)
7. statement translation (`translator.py`: stack machine, defaults
   recovery, lambdas inline, 2.x print/exec/class ops)
8. control-flow graph from jumps (`cfg.py`: if/for/while/try/except-as
   (bare `except:` too)/with/finally, clauses + clusters, 3.11+
   exception table, SETUP-region splits)
9. emit + repairs (`emitter.py`: source-order splicing onto def/class
   stub lines, bases recovery, `@decorator` restore, `global` decls,
   cond chains, ternaries, nested if/else, loop ranges, break/continue
   recovery, indent + suite-guard passes)
10. 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.
- `try` bodies 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_ITER` targets 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.
