This repository contains ToCode, a Python-only binary exporter. ToCode takes one binary, IDA database, Android APK, or .NET assembly/bundle/package path and writes one source-like project directory for reverse-engineering agents.
- Keep the project focused on the exporter CLI and Python library modules.
- Do not add web UI, server, container wrapper, shortcut, installer, or background service behavior.
- Do not add chat, report generation, or secondary analysis stages.
- Export one project tree from one binary or IDA database: raw decompiler output, assembly, summaries, section data, JSON metadata, optional IDA database, and optional scanner-friendly C.
- Generated export trees must include their own
AGENTS.mdfor agents analyzing that exported binary.
src/tocode/cli.py: command-line entry point fortocode.src/tocode/__init__.py: package version andexport_from_binaryview(), the in-UI Binary Ninja library entry.src/tocode/analysis.py: backend-neutral binary inventory and call graph normalization.src/tocode/backends/: IDA Domain, radare2, angr, and Binary Ninja (binja.py) session adapters, plus the ASC/droidasc APK backend (asc.py: APK set discovery, DEX inventory, worker-side class decompilation).src/tocode/backends/dotnet.py/dotnet_pe.py: .NET backend (input discovery for assemblies, apphosts, single-file bundles and NuGet packages; dnlib inventory; pure-Python PE/CLR/bundle parsing and IL token scan; worker-side C#/IL decompilation).backends/dotnet_libs.pydownloads dnlib and ICSharpCode.Decompiler on demand (ask once, hash-pinned) into a per-user folder.src/tocode/dotnet.py: .NET export pipeline;dotnet_metadata.py: .NET JSON documents and the generatedAGENTS.md.src/tocode/apk.py: APK export pipeline (extraction, decompile pool, resource decoding, metadata);apk_metadata.py: manifest parsing and Android JSON documents;apk_native.py: extraction and background native export of every.so.src/tocode/exporter.py: project writer, function rendering, worker-session rendering, generated exportAGENTS.md.src/tocode/metadata.py: JSON metadata and triage documents.src/tocode/cluster.py: call-graph clustering.src/tocode/parallel.py: worker-count selection.src/tocode/schema.py: dataclasses shared across the exporter.tests/: unit tests for algorithms, CLI helpers, and export tree generation.
The CLI accepts a regular binary file or IDA database and writes a project directory containing:
src/raw/**/*.csrc/raw/**/*.asmsrc/raw/**/*.summaryinclude/*.hdata/*.bindata/variables.jsondata/variables_interesting.jsonfunction-index.jsonfunctions.jsonsections.jsonstrings.jsonimports.jsonexports.jsonrelocations.jsonreachable.jsoncluster-graph.jsontriage.jsonproject.jsonexport-manifest.jsontocode.log- generated export
AGENTS.md - generated export
CLAUDE.md
When the IDA backend is used, the export also contains:
<binary>.i64or<binary>.idb
When --tree is passed, the export also contains:
src/tree/**/*.cfunction-index-tree.json
APK input (.apk, .apks, .xapk) uses the ASC backend and writes instead:
src/raw/<package>/**/*.java(one file per class, folders are Java packages; no clusters, no summaries)AndroidManifest.xml,manifest.json,classes.json,package-graph.json,native-libs.jsonfunctions.json,function-index.json,strings.json,imports.json,exports.json,sections.json,reachable.json,triage.json,project.json,export-manifest.json(same shapes as the native export where applicable)lib/<abi>/*.so(every native library, always extracted) andnative/<abi>/<lib>/(a full nested ToCode export per library, all ABIs, unless--no-native)data/apk/**(all other entries verbatim),data/res/**/*.xml,data/resources.jsontocode.log, generatedAGENTS.mdandCLAUDE.md
base.apk merges sibling split_*.apk files unless --no-splits; bundles are unpacked and merged. --backend selects the native backend for the .so exports (binja is rejected).
.NET input (a managed PE, an apphost launcher with its .dll, a single-file bundle, or a .nupkg; detected by content, --as-native opts out) uses the .NET backend and writes instead:
src/raw/<Assembly>/<Namespace>/**/<Type>.csand<Type>.il(C# and IL with raw bytecode per top-level type; nested types inside),src/raw/<Assembly>/Properties/AssemblyInfo.csandManifest.il,src/raw/<Assembly>/<Assembly>.csprojassemblies.json,types.json,namespace-graph.json,resources.json,container.json,native-libs.jsonfunctions.json,function-index.json(c= C#,asm= IL),strings.json,imports.json,exports.json,sections.json,reachable.json,triage.json,project.json,export-manifest.jsondata/resources/<Assembly>/*(embedded resources,.resourcesdecoded to JSON),data/assemblies/*anddata/<bundle|nupkg>/*(extracted container entries)lib/<arch>/*andnative/<arch>/<lib>/for mixed-mode code, bundled native libraries, and P/Invoke targets next to the input (unless--no-native)tocode.log, generatedAGENTS.mdandCLAUDE.md
Framework/runtime assemblies and native runtime libraries inside bundles and packages are inventoried but only decompiled with --include-framework.
- Prefer
uvfor local commands. - Package entry point:
tocode. - Keep generated text, environment variables, and user-visible strings branded as ToCode.
- Keep status output concise but informative. Progress bars are handled by
tqdmthroughProgress.bar(...). - Use
apply_patchfor manual edits. - Do not commit generated export directories such as
here/,ls/, or*_decompiler/.
- Runtime dependencies belong in
pyproject.tomlanduv.lock. - IDA Domain is the preferred backend when available.
- radare2/r2pipe is a fallback backend.
- angr is the optional pure-Python fallback backend (
[angr]extra). - ASC (
droidasc, PyPI) is the APK/DEX backend and a core runtime dependency. It pulls androguard.backends/asc.pyis the only module that imports it; it pre-imports the modules ASC would otherwise stub insys.modulesand silences androguard's loguru logging. Class decompilation is not thread-safe, so it runs in spawned worker processes (recycled in rounds; do not usemax_tasks_per_child, it deadlocks spawn pools on some CPython builds). The big per-method/per-class/per-string JSON documents are streamed row by row (apk_metadata.write_json_rows), never built as one object. - APK native libraries are exported by
apk_native.pyon a background thread, eachexport_binaryin its own spawned process so a backend OOM-kill or crash only fails that library (native-libs.jsonstatus). The thread waits forTOCODE_NATIVE_MIN_FREE_MB(default 1024) of free memory before starting each library so it does not starve the DEX pool.TOCODE_WORKER_TMP_DIRalso places the unpacked.apksbundle. - The .NET backend needs
pythonnet(core dependency, pinned per Python version) and a .NET 9+ runtime on the host. Never commit or package DLLs: dnlib 4.5.0 and ICSharpCode.Decompiler 11.1 are downloaded from nuget.org bybackends/dotnet_libs.pyafter the user agrees (asked once, remembered;tocode --setup-dotnetfor non-interactive use). Their.nupkgand DLL SHA-256 hashes are pinned inLIBRARIESand checked at download time only (normal runs check presence;--setup-dotnetre-verifies). To bump a version, update both hashes. ICSharpCode.Decompiler 11 needs System.Reflection.Metadata 9, hence .NET 9+. Decompilation runs in spawned worker processes (recycled per round; a batch that crashes or hangs a worker is retried type by type so only the culprit fails). Tests must not require .NET: usetests/dotnet_fixtures.py(synthetic PE/bundles) and fake sessions; runtime-backed tests skip without it. - Binary Ninja is an opt-in backend (
--backend binja, never auto-selected). Thebinaryninjamodule is supplied by the Binary Ninja install (in-UI) or the remote VM, so it is not a pip dependency.rpyc, the client used for the headless path via binja-headless, is a core runtime dependency so the backend works out of the box. - Pin new dependencies exactly (
==) and capture them inuv.lock. - Keep dependencies minimal and tied to exporting a project.
- Headless (no enterprise license): run binja-headless inside a running Binary
Ninja, then
tocode --backend binja --binja-host <ip>. It exports an already loaded view (the binary positional is optional). Configure the connection with--binja-host/--binja-portorTOCODE_BINJA_HOST/TOCODE_BINJA_PORT(defaults127.0.0.1:18812). - Choosing the view (headless): default is the focused view (the one binja-headless
exposes as
conn.root.bv).--list-binjaprints every open view with an index;--binja-view Nexports that index;--all-viewsexports every open view, each into its own folder under-o(or the cwd). Active-context lookup is Qt-main- thread bound and unavailable over RPyc, so the focused view comes fromconn.root.bv; open views are enumerated throughbinaryninjaui.UIContextvia the service's remoteeval. If no view is open, a binary path argument is remote-loaded as a fallback. - In the Binary Ninja UI: call
tocode.export_from_binaryview(bv, out_dir)from the scripting console (bvis the live view;binaryninjais imported locally). It runs the same export pipeline as the CLI. - The backend renders serially against the live view (
parallel_safe = Falseand excluded fromexporter.TIMEOUT_WORKER_BACKENDS): aBinaryView/ RPyc connection cannot be pickled into a worker process. Do not add it to the spawned-worker path.
Run focused checks after changes:
uv run --extra dev pytest -q
python3 -m compileall src testsRun the full local CI/quality gate when changing shared behavior:
./ci-local.shOn Windows PowerShell:
powershell -ExecutionPolicy Bypass -File .\ci-local.ps1For backend-sensitive changes, also run a real export when IDA is available:
uv run tocode /bin/true -o /tmp/tocode-check --backend auto -j 2Confirm the generated project matches the export contract above.