Metadata-Version: 2.4
Name: crashlink
Version: 0.0.8
Summary: Just another HashLink decompiler/disassembler.
Author-email: N3rdL0rd <n3rdl0rd@proton.me>
License-Expression: MIT
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Classifier: Topic :: Software Development :: Disassemblers
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pdoc3; extra == "dev"
Requires-Dist: build; extra == "dev"
Requires-Dist: twine; extra == "dev"
Requires-Dist: mypy; extra == "dev"
Requires-Dist: pytest; extra == "dev"
Requires-Dist: tqdm; extra == "dev"
Requires-Dist: snakeviz; extra == "dev"
Requires-Dist: typeguard; extra == "dev"
Requires-Dist: types-tqdm; extra == "dev"
Requires-Dist: dill; extra == "dev"
Requires-Dist: pytest-xdist; extra == "dev"
Requires-Dist: staticjinja; extra == "dev"
Requires-Dist: IPython; extra == "dev"
Requires-Dist: requests; extra == "dev"
Requires-Dist: ruff; extra == "dev"
Requires-Dist: pygments; extra == "dev"
Requires-Dist: types-Pygments; extra == "dev"
Requires-Dist: markupsafe; extra == "dev"
Requires-Dist: lief; extra == "dev"
Requires-Dist: capstone; extra == "dev"
Requires-Dist: mcp; extra == "dev"
Requires-Dist: PySide6; extra == "dev"
Requires-Dist: graphviz; extra == "dev"
Provides-Extra: extras
Requires-Dist: tqdm; extra == "extras"
Requires-Dist: dill; extra == "extras"
Requires-Dist: IPython; extra == "extras"
Requires-Dist: pygments; extra == "extras"
Requires-Dist: lief; extra == "extras"
Requires-Dist: capstone; extra == "extras"
Requires-Dist: mcp; extra == "extras"
Requires-Dist: PySide6; extra == "extras"
Requires-Dist: graphviz; extra == "extras"
Provides-Extra: gui
Requires-Dist: PySide6; extra == "gui"
Requires-Dist: graphviz; extra == "gui"
Dynamic: license-file

# crashlink

![workflow](https://github.com/N3rdL0rd/crashlink/actions/workflows/python-package.yml/badge.svg) ![wakatime](https://wakatime.com/badge/user/959c37b8-6a50-4f37-8cc5-e2b14b687b80/project/7ce1f674-75d5-4525-88f2-ea4e5532e73a.svg) ![PyPI - Version](https://img.shields.io/pypi/v/crashlink)
 ![PyPI - Downloads](https://img.shields.io/pypi/dd/crashlink)

The pure-Python HashLink bytecode Swiss Army knife.

> [!WARNING]
> This project is under active development. Breaking changes may be made to APIs with zero notice.

Join the [Hashlink Modding Community Discord](https://discord.gg/Es8ZpVkPey) for support!

## Features

- Pure Python with zero dependencies, integrates nicely in a lot of places
- Deserialisation, disassembly, and (currently incomplete, but usually functional) decompilation of HashLink bytecode
- Reserialisation and first-class support for patching bytecode assembly
- A GUI with a graphical disassembler/decompiler, embedded CFG viewer, source-location lookups, and in-place patching
- A bytecode assembler for creating HashLink bytecode from scratch
- An HL/C reimplementation to transpile HashLink bytecode straight to C and build it against libhl
- A full-featured CLI (`crashlink`) with both one-shot subcommands and a batteries-included interactive REPL (60+ commands!)
- Xref indexing and source-location mapping
- (Experimental) Tools to extract debug info from compiled HL/C binaries (PDB/DWARF) back into a navigable bytecode image
- A scriptable interface for easy integration into other tools

## Installation

```bash
uvx crashlink # or pip install crashlink
```

Optionally, install `[extras]` for a bunch of additional goodies, including a GUI:

```bash
uv tool install crashlink[extras] # or pip install crashlink[extras]
```

Or, for bleeding-edge features, see the [Development](#development) section.

You also need to have Graphviz installed to generate control flow graphs. On most *nix systems, on Windows (with Chocolatey or Scoop), and on MacOS (with Homebrew), you can install it with your package manager under `graphviz`.

- Windows: `choco install graphviz`
- MacOS: `brew install graphviz`
- Debian: `sudo apt install graphviz`
- Arch: `sudo pacman -S graphviz`
- Fedora: `sudo dnf install graphviz`

In order to work with some of the HL/C utilities, you also need to have Hashlink's core packages installed:

```bash
haxelib git hashlink https://github.com/HaxeFoundation/hashlink.git master other/haxelib/
```

## Usage

Either:

```txt
$ crashlink path/to/file.hl # or python -m crashlink
crashlink> funcs
f@22 static Clazz.main () -> Void (from Clazz.hx)
f@23 Clazz.method (Clazz) -> I32 (from Clazz.hx)
crashlink> fn 22
f@22 static Clazz.main () -> Void (from Clazz.hx)
Reg types:
  0. Void

Ops:
  0. Ret             {'ret': 0}                                       return
```

Or:

```py
from crashlink import *
code = Bytecode.from_path("path/to/file.hl")
if code.fn(22): # 22 and 240 are typical entry points for the compiler to generate
  print(disasm.func(code.fn(22)))
elif code.fn(240):
  print(disasm.func(code.fn(240)))
# > f@22 static $Clazz.main () -> Void (from Clazz.hx)
# > Reg types:
# >   0. Void
# >
# > Ops:
# >   0. Ret             {'ret': 0}                                       return
```

### CLI basics

Running `crashlink <file>` with no subcommand opens the file directly and drops you into the interactive REPL (see below), or runs a single command with `-c`, e.g. `crashlink game.hl -c funcs`. A few flags are useful here:

- `-N` / `--no-constants`: skip constant resolution on load - helpful for malformed or unusually large files
- `-a` / `--assemble`: treat `file` as crashlink assembly and assemble it to bytecode (`-o` sets the output path)
- `-p` / `--patch`: apply a patch module to `file` (see `-o` for where to write the result)
- `-d` / `--debug`: enable extra debug output; `-t` / `--traceback`: print full tracebacks on error

For everything that doesn't need a live session, there are one-shot subcommands instead - each with its own `-h` for detailed usage and examples:

```txt
$ crashlink funcs game.hl              # list functions
$ crashlink disasm game.hl 42          # disassemble f@42
$ crashlink decompile game.hl 42       # decompile f@42 to pseudo-Haxe
$ crashlink info game.hl               # summary info (version, counts, etc.)
$ crashlink search game.hl "password"  # search strings by substring
$ crashlink db info game.cldb          # inspect a .cldb debug-info database
$ crashlink hlc game.hl --build        # transpile to C and compile it
$ crashlink mcp                        # run as an MCP server
```

Run `crashlink --help-all` to print the top-level help plus every subcommand's `-h` output in one go.

### REPL basics

Bytecode objects, functions, and types are addressed by index - `f@<findex>` for functions/natives, `t@<tIndex>` for types, `g@<gIndex>` for globals, `s@<index>` for strings - the same notation used throughout disassembly output and crashlink assembly. A typical session looks like:

```txt
$ crashlink game.hl
crashlink> funcs
f@22 static Clazz.main () -> Void (from Clazz.hx)
f@23 Clazz.method (Clazz) -> I32 (from Clazz.hx)
crashlink> findfunc method          # search by name substring (alias: ff)
f@23 Clazz.method (Clazz) -> I32 (from Clazz.hx)
crashlink> fn 23                    # disassemble a function (alias: f)
f@23 Clazz.method (Clazz) -> I32 (from Clazz.hx)
...
crashlink> decomp 23                # decompile to pseudo-Haxe (aliases: d, pseudo)
...
crashlink> cfg 23                   # render a control flow graph and open it
crashlink> xref func 23             # find every caller of f@23
crashlink> rename 23 0 _ localName  # rename a local for readability
crashlink> save game_patched.hl     # write out your changes
crashlink> exit
```

`help` lists every command grouped with its aliases, and `help <command>` gives its usage and a longer description. Up/down arrows browse command history (persisted across sessions in `~/.crashlink_history`), tab completes command names, and `clear`/`history`/`exit` do what you'd expect.

Read the [API documentation](https://n3rdl0rd.github.io/crashlink/crashlink) for more information.

## Development

> [!NOTE]
> This project is configured for the [just](https://just.systems/) command runner and [uv](https://docs.astral.sh/uv/). If you don't have them installed, you can still run the commands in the `justfile` manually and the `pip` equivalents of the `uv` commands, but I don't recommend it. At the very least, there's zero downside to switching to `uv`.

For development purposes, you can clone the repo, install development dependencies, and run the tests:

```bash
git clone https://github.com/N3rdL0rd/crashlink
cd crashlink
uv sync --extra dev
just test # or pytest
```

Before committing, please run `just dev` to format the code, run tests, and generate documentation in `docs/`. If you're adding new features to the core serialisation/deserialisation code (`core.py`), please also add a test case in `tests/haxe/` for the new language feature you're adding. If you're adding a feature to the decompiler or disassembler, please add a normal test case (in Python) in `tests/` that tests the new feature.

Pull requests are always welcome! For major changes, please open an issue first to discuss what you would like to change.

You can use the following pre-defined commands with `just`:

- `just dev`: Run tests, format code, and generate documentation.
- `just build`: Build the package.
- `just install`: Install development dependencies and the package in editable mode.
- `just build-tests`: Build test samples.
- `just test`: Run tests.
- `just format`: Format code.
- `just docs`: Generate documentation.
- `just check`: Run static analysis/typechecking.
- `just clean`: Clean up build artifacts.
- `just profile`: Run the test suite with cProfile and then open the results in a browser.
- `just serve-docs`: Serve the documentation locally.

### `crashtest` CLI

`crashtest` is a built-in testing system that is used to score the decompiler's output against the original source code. It is used to ensure that the decompiler is working correctly, that the output is correct, that the decompiler is not regressing, and to allow those interested in the project to easily see the state of the decompiler without installing it or running the test suite themselves. You can call it with `crashtest auto` (or `python -m crashtest auto`). Make sure you call it from the root of the repository, since it uses relative paths to find the test files and the output directory.

## Architecture

![Architecture](docs/static/flow.svg)

## Roadmap

- [x] Bytecode parsing
- [x] Opcode disassembly
  - [x] Local resolution and naming
- [x] IR lifter (layer 0)
  - [x] If statements
  - [x] Loops
  - [x] Switch opcode statements
  - [x] Function calls
    - [x] CallClosure
  - [x] Closures, lambdas
- [ ] IR optimization layers
  - [x] Resolve locals from assigns block
  - [x] Trace optimization
  - [x] Nested if/else/if/else -> switch
  - [ ] More to address issues as they arise!
- [x] Haxe pseudocode
- [x] Cross-reference index
- [x] GUI prerequisites
  - [x] Workspace/project abstraction (wraps `Bytecode` with cached analysis state)
  - [x] Incremental/async analysis API (background decompile, progress callbacks)
  - [ ] Patch buffer (in-memory edits, dirty tracking, re-serialisation)
  - [x] Function search index (by name, file, type)
  - [x] Source location API (debug file + line → function/opcode, and reverse)
- [ ] GUI (probably qt6 at this point)
  - [x] Graphical disassembler
  - [x] Embedded CFG viewer through some Graphviz bindings
  - [x] Decompiler
  - [x] Basic local name patching
  - [ ] Other direct patching
  - [ ] Rename other symbols
  - [ ] Persistent patching/export modified bytecode
  - [ ] IR layer viewer
- [ ] Partial recompilation (against stubs of other functions)

## Portability

crashlink is written in pure typed Python with a minimum version of 3.10 (for the `|` operator and `match` statement). It should run on any modern platform, and has been tested heavily on Windows, Linux, and has been tested, but less heavily, on MacOS. As well as this, it is portable to many Python interpreters:

- CPython 3.10+ is the main target
- PyPy also just works
- IronPython and Jython are not supported due to their earlier Python version targets.
- RustPython would work, but it doesn't support `match` statements.
- Pyodide works and you can see a live demo [here](https://n3rdl0rd.github.io/crashlink/demo)

## Credits

- Thank you to [Gui-Yom](https://github.com/Gui-Yom) for writing hlbc and for maintaining documentation on the HashLink bytecode format, as well as for providing tests and helping me during development.
- Thank you to [Haxe Foundation](https://haxe.org/) for creating the HashLink VM and the Haxe programming language.
- Thank you to the Dead Cells community on Discord for providing me with the motivation to start this project.
- And a big thank you to you, dear user, for being at least partially interested in this project.

❤ N3rdL0rd
