Metadata-Version: 2.4
Name: pyc-revive
Version: 1.0.2
Summary: Stdlib-only .pyc -> .py reconstructor with native, pycdas, and depyo 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. Pure stdlib. No `uncompyle6`. No `pycdc`. No `xdis`.

Handles every CPython from 2.0 through 3.15, with a bundled disassembler
fallback for versions that don't match the running interpreter, and a
portable Node.js + depyo path for the newest bytecode (3.14+).

## install

```bash
pip install pyc-revive
```

The Python package is stdlib-only. On **first run**, `pyc-revive`
checks for Node.js and depyo, and installs them if missing — no admin,
no system changes, everything under `%LOCALAPPDATA%\pyc_revive` (or the
equivalent user dir on non-Windows):

- **Node.js ≥ 20** via winget (user scope) if available, otherwise a
  portable ZIP extracted into the user dir. No MSI, no registry writes.
- **depyo** via npm into a user-writable prefix
  (`%LOCALAPPDATA%\pyc_revive\npm-global`).
- **optional pip backends** (pychd, byteripper, uncompyle6, decompyle3)
  installed individually with `--user` fallback when the system site
  isn't writable.

To run the installer explicitly:

```bash
pyc-revive --install-deps
```

To skip the first-run bootstrap entirely:

```bash
pyc-revive --no-bootstrap Backdoor.pyc
```

From a checkout (not pip-installed):

```bash
python -m pyc_revive my file.pyc
python -m pyc_revive ./dist/app.pyc --out ./revived.py
python -m pyc_revive --install-deps
```

Input handling: exact path, spaces with no quotes, basename search in
cwd + all subfolders + parent folder.

## versions

Every CPython magic from 2.0 through 3.15 resolves (`pymagic.py`: full
table + nearest fallback + header sizes).

Three backends, one pipeline:

- **native** — `.pyc` magic matches the running interpreter. stdlib
  `marshal` loads the code object, stdlib `dis` disassembles it. Zero
  dependencies.
- **pycdas** — anything else 2.x–3.13. 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×3) runs
  unchanged.
- **depyo** — 3.14+. `depyo_parser.py` runs `depyo --skip-source-gen
  --asm` and parses the `.pyasm` dump; `depyo_postprocess.py` fixes
  depyo's mechanical emitter bugs (dropped statements, mangled imports,
  wrong keywords) using the pycdas pipeline's output as a repair source
  when both ran. When the pipeline fails to compile, `depyo-direct`
  produces working source from depyo's own decompiler.

Routing is by magic number, decided before any bytecode is touched: a
foreign magic goes straight to the appropriate backend. `marshal.loads`
can *succeed* on foreign payloads since the format is stable, and then
stdlib `dis` mis-decodes the unknown opcodes and can take the whole
process down instead of raising — so "try native, fall back on error"
is not used for foreign files.

## 3.14 and 3.15 support

3.14 was the first foreign 3.11+ target exercised end-to-end, and it
needed real work beyond a new opcode table.

**Translator additions:**

- `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).
- `BINARY_OP` oparg ≥ 26 (3.14 renumbers the table; 26 = `NB_SUBSCR`,
  27 = inplace).
- `KW_NAMES` — pycdas 3.13 sometimes prints it as `LOAD_CONST <i> //
  Value: ('a', 'b')`, a tuple of key names. `shim.py` rewrites the
  opname when the argval is a string tuple immediately followed by a
  `CALL_KW`, so the translator's kwarg split runs.

**Parser additions (`pycdas_parser.py`):**

- `[Locals+Names]` sections read into `co_varnames`.
- `[Exception Table]` entries (`S to E -> T [depth] (lasti?)`) parsed
  and re-encoded to `co_exceptiontable` bytes on the shim, so the
  shared try/with-region machinery runs unchanged.

**Shim additions (`shim.py`):**

- 3.14 conditional-branch targets from the bundled pycdas land 2 bytes
  low (it suppresses a hidden CACHE but forgets it in target math —
  proven three ways: dead else bodies, inverted with-cleanup,
  stack-underflow `RETURN`). Repaired 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`.
- `LOAD_FAST_BORROW` index resolution, including the pair form.

**Region detection:**

- with-bodies are scoped by their exception-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:`).
- 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.

3.15 goes through the same depyo path. depyo's unresolved name masks
(`##NAME_N##`, `##ERROR##`, `__common_const_N__`) are collapsed or
substituted in `depyo_postprocess.py`; stdlib imports dropped by depyo
are reinserted from a canonical table.

## pipeline

1. find file (`finder.py`)
2. detect version via magic (`pymagic.py`)
3. load: native marshal, else pycdas / depyo backend (`loader.py`,
   `pycdas_parser.py`, `depyo_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 ×3)
11. self-validation (`autofix.py`: compile the output, diagnose the
    first error, apply a targeted repair — orphan handler, empty
    suite, bad indent, orphan `try:` — until it compiles or the budget
    runs out; ships the closest parse)
12. backend fallback (`reconstructor.py`: if the pipeline's output
    doesn't compile and depyo is available, depyo-direct runs with the
    pipeline's source as a repair companion, then post-processing
    fixes the mechanical bugs)

## layout

```
pyc_revive/
  finder.py            file resolution
  pymagic.py           magic-number table + version detection
  loader.py            native marshal loading
  pycdas_parser.py     bundled-disassembler dump parser
  depyo_parser.py      depyo --asm dump parser
  depyo_postprocess.py repair pass for depyo output
  shim.py              foreign-version code trees behave like code objects
  bytecode.py          Inst + EditableBytecode
  normalizer.py        const/name masking
  segmenter.py         B/I/E opcode segmentation
  translator.py        opcode stream -> statements
  cfg.py               basic blocks + structured control flow
  emitter.py           source emission + structural repairs
  verifier.py          recompile + compare x3
  autofix.py           compile-repair loop
  reconstructor.py     orchestrator
  cli.py               interface
  bootstrap.py         first-run dep check
  install_deps.py      Node + depyo + pip backend installer
  deps.py              optional-backend reporter
  debug_log.py         verbose logging (PYC_REVIVE_VERBOSE=1)
  __main__.py
  pycdas.exe           bundled disassembler
```

## 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_FUNCTION` 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.

## license

MIT
