From ddb0ad9f0024e0039d77eb9eeccd05ad18546259 Mon Sep 17 00:00:00 2001 From: buzzer-re <22428720+buzzer-re@users.noreply.github.com> Date: Tue, 6 Oct 2026 09:08:55 +0900 Subject: [PATCH 1/4] Prepare 0.2.0 for PyPI - Bump the version to 0.2.0. - Load the .NET libraries by name from a sys.path folder: pythonnet 3.0 (used on Python 3.10) rejects full paths in clr.AddReference, so .NET exports failed there. Checked from the built wheel on Python 3.10-3.14. - Link DEVELOPMENT.md with an absolute URL so it works on the PyPI page. Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- src/tocode/__init__.py | 2 +- src/tocode/backends/dotnet.py | 12 ++++++++++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c024798..61e10e6 100644 --- a/README.md +++ b/README.md @@ -246,4 +246,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. diff --git a/src/tocode/__init__.py b/src/tocode/__init__.py index e4bed5a..4d7b4cb 100644 --- a/src/tocode/__init__.py +++ b/src/tocode/__init__.py @@ -7,7 +7,7 @@ __all__ = ["__version__", "export_from_binaryview"] -__version__ = "0.1.0" +__version__ = "0.2.0" def export_from_binaryview( diff --git a/src/tocode/backends/dotnet.py b/src/tocode/backends/dotnet.py index 3a3ef1b..323fc08 100644 --- a/src/tocode/backends/dotnet.py +++ b/src/tocode/backends/dotnet.py @@ -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( From ace5efc1c1dbf52e9f22e1f3b4733c75d0dd5a01 Mon Sep 17 00:00:00 2001 From: buzzer-re <22428720+buzzer-re@users.noreply.github.com> Date: Tue, 6 Oct 2026 09:31:00 +0900 Subject: [PATCH 2/4] Shorten the README .NET section and add the PyPI install The .NET section drops the output file list (already in the export layout table) and implementation details, keeping what a user needs: supported inputs, the .NET 9+ runtime, the one-time library download, and the flags. The install guide now starts with `pip install tocode-cli`. Co-Authored-By: Claude Opus 5.5 --- README.md | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 61e10e6..463b594 100644 --- a/README.md +++ b/README.md @@ -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///.../.cs`: C# per top-level type, folders follow assemblies and namespaces like a dnSpy/ILSpy project export; `.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 `!0x`), 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 @@ -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 From 7aa9c7ac69a2d92ae7a7e26b1bcee6a5e4f475ac Mon Sep 17 00:00:00 2001 From: buzzer-re <22428720+buzzer-re@users.noreply.github.com> Date: Tue, 6 Oct 2026 09:33:26 +0900 Subject: [PATCH 3/4] Publish to PyPI from version tags Pushing a tag like v0.2.1 checks that it matches __version__, runs the tests, builds and strictly checks the sdist and wheel, and publishes them with PyPI trusted publishing (no stored token) through the `pypi` environment. Actions are pinned to commit SHAs like ci.yml. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/publish.yml | 67 +++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..38a8c8d --- /dev/null +++ b/.github/workflows/publish.yml @@ -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 From 123cab4af9a6301593f0ca16ef1f7d852c267550 Mon Sep 17 00:00:00 2001 From: buzzer-re <22428720+buzzer-re@users.noreply.github.com> Date: Tue, 6 Oct 2026 09:33:26 +0900 Subject: [PATCH 4/4] Release 0.2.1 Ships the shorter README (.NET section, PyPI install) to the PyPI page. Co-Authored-By: Claude Opus 5.5 --- src/tocode/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tocode/__init__.py b/src/tocode/__init__.py index 4d7b4cb..72e9618 100644 --- a/src/tocode/__init__.py +++ b/src/tocode/__init__.py @@ -7,7 +7,7 @@ __all__ = ["__version__", "export_from_binaryview"] -__version__ = "0.2.0" +__version__ = "0.2.1" def export_from_binaryview(