Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Publish to PyPI

# Runs when a version tag is pushed, e.g. `git tag v0.2.1 && git push origin v0.2.1`.
# The tag must match __version__ in src/tocode/__init__.py.
on:
push:
tags:
- "v*"

permissions:
contents: read

jobs:
build:
name: Test and build
runs-on: ubuntu-24.04
steps:
# actions/checkout@v4
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5

- name: Install uv
# astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@d0d8abe699bfb85fec6de9f7adb5ae17292296ff

- name: Check the tag matches the package version
run: |
version="$(sed -n 's/^__version__ = "\(.*\)"$/\1/p' src/tocode/__init__.py)"
if [[ "${GITHUB_REF_NAME}" != "v${version}" ]]; then
echo "::error::tag ${GITHUB_REF_NAME} does not match __version__ ${version} (expected tag v${version})"
exit 1
fi
echo "Publishing tocode-cli ${version}"

- name: Run tests
run: uv run --locked --extra dev pytest -q

- name: Build sdist and wheel
run: uv build

- name: Check the distributions
run: uvx twine==6.2.0 check --strict dist/*

# actions/upload-artifact@v4
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: dist
path: dist/
if-no-files-found: error

publish:
name: Publish
needs: build
runs-on: ubuntu-24.04
environment:
name: pypi
url: https://pypi.org/project/tocode-cli/
permissions:
id-token: write # PyPI trusted publishing (no API token stored)
steps:
# actions/download-artifact@v4
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
with:
name: dist
path: dist/

# pypa/gh-action-pypi-publish@release/v1
- uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33
30 changes: 17 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,21 +112,16 @@ Other disassemblers may be added in the future.

### .NET assemblies

`tocode App.dll` (also `.exe`, an apphost launcher next to its `.dll`, single-file bundles, and `.nupkg` packages) uses a built-in .NET backend: [dnlib](https://github.com/0xd4d/dnlib) for metadata and [ICSharpCode.Decompiler](https://github.com/icsharpcode/ILSpy) (ILSpy's engine) for C#, loaded through [pythonnet](https://pypi.org/project/pythonnet/). No dnSpy/ILSpy install is needed, only a **.NET 9+ runtime** (e.g. `sudo apt install dotnet-runtime-10.0`, or the SDK; `DOTNET_ROOT` is honoured).
ToCode decompiles .NET assemblies (`.dll`, `.exe`), single-file bundles, apphost launchers, and NuGet packages to C#, and writes the IL of each type next to it. You need a .NET 9 or newer runtime.

ToCode does not ship these two libraries (both MIT). The first .NET export asks once whether to download them from nuget.org and remembers the answer. Non-interactive runs (agents, CI) are never prompted: run `tocode --setup-dotnet` once instead, which downloads without asking, or re-verifies and repairs an existing install. Each download is checked against SHA-256 hashes pinned in `src/tocode/backends/dotnet_libs.py`, for both the `.nupkg` and the extracted DLL, and the files are re-hashed after being written. They are stored per user in `~/.local/share/tocode/dotnet` (macOS: `~/Library/Application Support/tocode/dotnet`, Windows: `%LOCALAPPDATA%\tocode\dotnet`, override: `TOCODE_DOTNET_LIB_DIR`). Delete that folder to undo the install or reset the remembered answer.
Decompilation uses [dnlib](https://github.com/0xd4d/dnlib) and [ICSharpCode.Decompiler](https://github.com/icsharpcode/ILSpy), which ToCode doesn't include. The first .NET export asks once whether to download them from nuget.org and checks them against pinned SHA-256 hashes. Scripts and agents can't answer that prompt, so run `tocode --setup-dotnet` beforehand. The files are stored per user (`~/.local/share/tocode/dotnet` on Linux, or `TOCODE_DOTNET_LIB_DIR`); delete that folder to reset.

- `src/raw/<Assembly>/<Namespace>/.../<Type>.cs`: C# per top-level type, folders follow assemblies and namespaces like a dnSpy/ILSpy project export; `<Type>.il` next to it keeps the bytecode (IL with RVA, raw bytes, and metadata tokens per instruction).
- Per-method C# and IL line ranges in `functions.json`/`function-index.json` (address `<Assembly>!0x<token>`), with lambdas and async/iterator state machines linked to the method that owns them.
- `assemblies.json`, `types.json`, `strings.json` (user strings with `ldstr` xrefs), `imports.json` (cross-assembly members and P/Invoke), `exports.json`, `reachable.json`, `namespace-graph.json`, `resources.json`, `container.json`, `triage.json` (P/Invoke, suspicious API families, obfuscation hints, strings of interest).
- Native code goes through the regular native backends on a background thread, like APK `.so` files: mixed-mode (C++/CLI) assemblies, native libraries inside bundles/packages, and P/Invoke targets shipped next to the input. `--no-native` skips it.
- Single-file bundles and packages carry the .NET runtime/framework; those assemblies and runtime native libraries are listed but only decompiled with `--include-framework`. NuGet packages decompile each assembly once, from its newest target framework.
- `--as-native` exports a managed PE with the native backend instead (e.g. to look at a ReadyToRun or mixed-mode image in IDA).
Native code in the program (mixed-mode assemblies, bundled libraries, P/Invoke libraries next to the input) goes to the native backend unless you pass `--no-native`. Bundles and packages carry the .NET runtime itself, which is skipped unless you add `--include-framework`. `--as-native` treats a .NET file as a plain PE.

```bash
tocode App.dll # assembly (+ P/Invoke libraries next to it)
tocode publish/App # single-file bundle or apphost launcher
tocode Vendor.Lib.1.0.0.nupkg # NuGet package
tocode App.dll
tocode publish/App # single-file bundle or apphost
tocode Vendor.Lib.1.0.0.nupkg
```

### Android APKs
Expand All @@ -151,7 +146,16 @@ tocode app.apks --no-native -j 4 # bundle, DEX/Android side only

ToCode supports Windows, Linux, and macOS with Python 3.10 or newer.

On Windows PowerShell:
Install it from PyPI:

```bash
pip install tocode-cli # or: uv tool install tocode-cli
pip install "tocode-cli[angr]" # also install the angr fallback backend
```

The command is `tocode`. You still need a backend (IDA, radare2, angr, or Binary Ninja); see [Supported backends](https://github.com/buzzer-re/ToCode#supported-backends).

To install from a clone of the repository instead, on Windows PowerShell:

```powershell
powershell -ExecutionPolicy Bypass -File .\install.ps1
Expand Down Expand Up @@ -246,4 +250,4 @@ export_from_binaryview(bv, "out/")

## Development

See [DEVELOPMENT.md](DEVELOPMENT.md) for setup, tests, the local quality gate, and release steps.
See [DEVELOPMENT.md](https://github.com/buzzer-re/ToCode/blob/main/DEVELOPMENT.md) for setup, tests, the local quality gate, and release steps.
2 changes: 1 addition & 1 deletion src/tocode/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

__all__ = ["__version__", "export_from_binaryview"]

__version__ = "0.1.0"
__version__ = "0.2.1"


def export_from_binaryview(
Expand Down
12 changes: 10 additions & 2 deletions src/tocode/backends/dotnet.py
Original file line number Diff line number Diff line change
Expand Up @@ -540,10 +540,18 @@ def load_runtime() -> dict[str, Any]:
raise ToCodeError(f"cannot load the .NET runtime: {exc}") from exc
import clr # type: ignore[import-not-found]

for spec in dotnet_libs.LIBRARIES:
clr.AddReference(str(library_dir / spec.file_name))
import System # type: ignore[import-not-found]

# Reference by name from a sys.path folder: pythonnet 3.0 (Python 3.10)
# rejects full paths in AddReference; 3.1+ accepts both.
if str(library_dir) not in sys.path:
sys.path.append(str(library_dir))
for spec in dotnet_libs.LIBRARIES:
try:
clr.AddReference(spec.file_name.removesuffix(".dll"))
except Exception:
System.Reflection.Assembly.LoadFrom(str(library_dir / spec.file_name))

actual = str(System.Environment.Version)
if _version_tuple(actual)[:1] < (MIN_RUNTIME_MAJOR,):
raise ToCodeError(
Expand Down
Loading