Metadata-Version: 2.4
Name: pyc-revive
Version: 1.0.3
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. Core pipeline is pure stdlib — no `uncompyle6`, no `pycdc`, no `xdis`. A bundled disassembler handles foreign versions; Node.js + depyo handles 3.14 and 3.15.

## install

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

The Python package itself has no third-party dependencies. Node.js and depyo are only needed for the 3.14+ backend, and they're installed on demand.

On first run, `pyc-revive` checks for Node.js and depyo and installs them if missing. Everything goes under `%LOCALAPPDATA%\pyc_revive` on Windows, or the equivalent user directory elsewhere. No admin rights, no system changes.

- **Node.js ≥ 20** via winget (user scope) if available, otherwise a portable ZIP extracted into the user directory. No MSI, no registry writes.
- **depyo** via npm into a user-writable prefix.
- **optional pip backends** (pychd, byteripper, uncompyle6, decompyle3) installed individually with a `--user` fallback.

```bash
# run installer explicitly
pyc-revive --install-deps

# skip first-run bootstrap entirely
pyc-revive --no-bootstrap Backdoor.pyc
```

From a checkout:

```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, or basename search in the current directory, all subfolders, and the parent folder.

## versions

The magic-number table covers every CPython release from 2.0 through 3.15, with nearest-lower fallback for unrecognized magics and correct header sizes per version (`pymagic.py`). Practical testing to date has focused on **3.9 through 3.15**.

Three backends, one shared pipeline:

- **native** — `.pyc` magic matches the running interpreter. stdlib `marshal` loads the code object, stdlib `dis` disassembles it. No external dependencies at all.
- **pycdas** — anything else from 2.x through 3.13. A bundled disassembler emits a text dump; `pycdas_parser.py` parses it into code trees; `shim.py` adapts them to look like real code objects. The rest of the pipeline (normalize, segment, translate, CFG, emit, verify ×3) runs unchanged on the result.
- **depyo** — 3.14 and 3.15. `depyo_parser.py` runs `depyo --skip-source-gen --asm` and parses the `.pyasm` dump; `depyo_postprocess.py` repairs depyo's mechanical emitter bugs using the pycdas pipeline's output as a repair source when both ran. When the pipeline's output doesn't compile, a fallback path produces source from depyo's own decompiler.

Routing is decided by magic number before any bytecode is touched. A foreign magic goes straight to the appropriate backend. `marshal.loads` can *succeed* on foreign payloads because the marshal format is stable across versions — and then stdlib `dis` mis-decodes unknown opcodes and can crash the interpreter instead of raising. So "try native, fall back on error" is deliberately not used for foreign files.

## benchmark

Reference corpus: **500 randomly sampled `.py` files** drawn from PyPI ecosystem packages — PyTorch internals, sympy, pandas, Django, telegram client libraries, gevent, trio, PyInstaller hooks, CPython's own `Compiler/` directory, generated parser tables, and one 918 KB data module. Compiled with Python **3.11.2**, which is one of the hardest modern targets: exception tables, `PRECALL`/`CALL` split, adaptive specialization, `CACHE` entries between instructions.

Measured with a byte-exact verification harness: compile source → `.pyc` → decompile with pyc-revive → recompile the output → recursive instruction-level comparison against the original (opcode, arg, argval, `co_names`, `co_varnames`, `co_consts`, and nested code objects).

| result | files | percent |
|---|---|---|
| byte-equivalent | 127 | 25.4% |
| structurally equivalent (opcode-name match) | 126 | 25.2% |
| not equivalent | 247 | 49.4% |
| total runtime | 500 files | 67 seconds |

That's approximately **130 ms per file** including process spawn, source compilation, tool invocation, and comparison.

Nothing is skipped. The corpus is the full random sample.

**Honest context on the number.** This corpus is not representative of typical Python code. It is deliberately weighted toward categories that are hard for any decompiler: code generators, machine-generated tables, metaclass-heavy object systems, async generator machinery, and annotation-dense auto-generated API stubs. A corpus of hand-written application code would score higher. A corpus compiled with Python 3.8 would score higher. The number above is what you get when you stack every hard case next to each other on the worst realistic version target.

**Trajectory.** The byte-equivalent rate roughly doubled (12.2% → 25.4%) in a single development session with no change in runtime. The structural rate went from 9.4% to 25.2% over the same period. The tool is under active development and the ceiling has not been reached.

To reproduce the number, the harness is `pyc_strict_bench.py` in the source repository.

## 3.14 and 3.15 support

3.14 was the first foreign 3.11+ target exercised end-to-end, and it required real work beyond extending the 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`.
- `BINARY_OP` opargs ≥ 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`.

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

- `[Locals+Names]` sections read into `co_varnames`.
- `[Exception Table]` entries 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 disassembler land 2 bytes low. The disassembler suppresses a hidden `CACHE` but doesn't account for it in target math. Repaired when the fingerprint is exact: a 3.14+ dump, `NOT_TAKEN` at `branch + 4`, and `target + 2` a real instruction offset.
- 3.14 replaced `BEFORE_WITH` with an inline prologue (`COPY, LOAD_SPECIAL, SWAP, SWAP, LOAD_SPECIAL, CALL`). The shim folds it back to one synthetic `BEFORE_WITH` so the shared with-region machinery applies.
- `LOAD_FAST_BORROW` index resolution, including the pair form.

**Region detection:**

- `with` bodies are scoped by their exception-table range, so fallthrough code after the body dedents correctly.
- `try` interiors stop at the body end, so 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 are paired by twin match: an unpaired `RERAISE`-ended handler whose leading statements duplicate a post-try block's leading statements is the exceptional finally copy, and the tails decide the split.

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 that depyo drops 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 or depyo backend (`loader.py`, `pycdas_parser.py`, `depyo_parser.py`, `shim.py`)
4. disassemble and abstract (`bytecode.py`: `Inst`, `EditableBytecode`, opcode/oparg/value-stack/metadata tables)
5. normalize and mask constants and names (`normalizer.py`)
6. opcode-aware B/I/E segmentation (`segmenter.py`)
7. statement translation (`translator.py`: stack machine, defaults recovery, inline lambdas, 2.x print/exec/class ops)
8. control-flow graph from jumps (`cfg.py`: if/for/while/try/except-as including bare `except:`/with/finally, clause and cluster detection, 3.11+ exception table, `SETUP`-region splits)
9. emit and repair (`emitter.py`: source-order splicing onto def/class stub lines, base-class recovery, decorator restoration, `global` declarations, condition chains, ternaries, nested if/else, loop ranges, break/continue recovery, indent and suite-guard passes)
10. equivalence verification ×3 sweeps (`verifier.py`: recompile revived source, compare opcode structure, repair, repeat)
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 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 dependency 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 (Windows)
```

## what this is and isn't

**What it is:** a bytecode-to-source reconstructor. It takes a compiled Python file and produces Python source that, when recompiled, matches the original as closely as the pipeline can get. It runs offline, does not execute the input, and does not require the input to be safe to open.

**What it isn't:** a malware detector. It does not scan files, does not flag behavior, and does not tell you whether a file is dangerous. It reconstructs source from bytecode you already have. In a malware-analysis workflow it comes *after* something else extracts the `.pyc` from an executable or archive — it is the step that turns that `.pyc` back into readable source.

**What it will do to your machine:** nothing on its own. The pipeline compiles the revived source to check that it parses (`compile()`), but never executes it (`exec()` is not called anywhere in the pipeline). If you run it on a hostile `.pyc`, it will try to decompile it and will not run any of the code inside it.

**What it will not do for you:** guarantee a reconstruction is safe to run. If you decompile malware and then run the output yourself, that's on you. The tool never runs the source it produces.

## known limits

- Inline comprehensions and generator expressions render as `func(...)` placeholders in some cases.
- `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 `def`s. 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 onward 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 has appeared in any 3.14 sample so far. If the bundled disassembler hides caches after `FOR_ITER` the same way it does for conditional branches, loop joins may land 2 bytes off.
- The bundled `pycdas.exe` is a Windows binary. On Linux and macOS the native and depyo backends still work, but foreign-version support through pycdas requires a platform-appropriate build of that disassembler, which is not currently shipped.

## changelog

**1.0.3** — depyo backend wired end-to-end. First-run bootstrap installs Node ≥ 20 and depyo without admin rights. `--install-deps` and `--no-bootstrap` flags added. `depyo_postprocess.py` added: drops unresolved name masks, recovers dropped imports from companion source, strips orphan returns, fixes wrong `from X import Y`. 3.14 rc1 magic added (rc1 and rc3 have different magic bytes). Byte-exact rate on the reference corpus: 12.2% → 25.4%.

**1.0.2** — 3.14 support beyond the opcode table. Exception-table parsing, `[Locals+Names]` sections, hidden-`CACHE` target repair, `BEFORE_WITH` folding, region scoping from exception tables.

**1.0.1** — first PyPI release. Native and pycdas backends, versions 2.0–3.13 through the magic table, practical testing focused on 3.9–3.13.

## license

MIT
