Metadata-Version: 2.4
Name: ida-colors
Version: 0.0.4
Summary: Parse IDA Pro color tags into a token tree for lightweight tokenization of IDA disassembly.
License-Expression: MIT
License-File: LICENSE
Requires-Dist: idapro>=0.0.4 ; extra == 'ida'
Requires-Python: >=3.10
Project-URL: Homepage, https://github.com/thebabush/ida-colors
Project-URL: Issues, https://github.com/thebabush/ida-colors/issues
Project-URL: Repository, https://github.com/thebabush/ida-colors
Provides-Extra: ida
Description-Content-Type: text/markdown

# IDA Colors

[![CI](https://github.com/thebabush/ida-colors/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/thebabush/ida-colors/actions/workflows/ci.yml?query=branch%3Amaster)

Parser for color tags in IDA.

## Usage

```sh
$ uv run ida-colors <path-to-idb>
...
    "LDR             X8, [X19,#0x10]"
        Insn("LDR")
        Opnd1(Reg("X8"))
        Opnd2(Symbol("["), Reg("X19"), Symbol(",#"), Number("0x10"), Symbol("]"))
...
```

### Numbers

`numbers()` returns the integers of a parsed line, with the `-` printed in front of each one applied:

```python
>>> from ida_colors import numbers, parse_full
>>> numbers(parse_full(ida_lines.generate_disasm_line(ea, 0)))  # STP XZR, XZR, [X1,#-0xD8]
[Number(value=-216, operand=3, standalone=False)]
```

`standalone` is true when the number is the entire operand, give or take a `#`, `=` or `-`: `#0x10`, `-1` and
`=0xCBF29CE4` are, while `[X1,#0x10]` and `[rsp+28h+var_20]` are not.

`u8()` to `u64()` and `i8()` to `i64()` reinterpret the value at a width, so `-1` and `0xFFFFFFFF` compare equal as
`u32()`, and return `None` when it doesn't fit. Spellings other than plain decimal, `0x10` and `10h` (floats, chars,
other radixes) are skipped rather than guessed.

### Strings

`strings()` returns the strings and chars that IDA shows in a line's automatic comments, as printed: quotes removed,
escapes kept.

```python
>>> from ida_colors import parse_full, strings
>>> strings(parse_full(ida_lines.generate_disasm_line(ea, 0)))  # lea rsi, aSslSession ; "SSL-Session:\n"
[String(text='SSL-Session:\\n', char=False, addr=844343, truncated=False)]
```

`addr` is where the string starts, also when IDA shows it through a pointer (`"/dev/urandom" ...`). `truncated` is
true when IDA cut it short (`"abi.NewName: tag too long: hw.optional."...`). Chars (`sub esi, 35h ; '5'`) have `char`
set and no `addr`. Names shown the same way (`; __gmon_start__`) and anything else that doesn't look exactly like a
string or a char are skipped.

The CLI needs IDA 9+ and the `ida` extra (`idapro`). The parser itself has no dependencies.

## Development

```sh
$ uv run ruff check && uv run ruff format --check
$ uv run ty check
$ uv run pytest
```

`tests/test_ida_constants.py` checks the constants copied from IDA against a local IDA install, and is
skipped when `idapro` can't load one. `tests/test_ida_numbers.py` checks parsed immediates against IDA's operand values
on every `.i64` in `$IDA_COLORS_IDBS` (opened as temporary copies), and `tests/test_ida_strings.py` checks parsed
strings against the string literals there. Both are skipped when it's unset.

`uv run python bench/bench.py` times `parse_full()`, `numbers()` and `strings()` per line on the sample of real
disassembly in `tests/data/lines.json`, by processor. It's not part of CI, as timings on shared runners are too noisy.

`tests/test_oracle.py` checks that trees, results and errors match `tests/ida_colors_ref`, a frozen copy of the
package, on those lines and on mangled versions of them. Speedups must keep it passing. A deliberate behavior change
replaces the copy with the new code.

## Why?

IDA's disassembly isn't just the decoded instruction. Between `decode_insn` and the line you see, the processor module's
printer applies a lot of heuristics and user annotations: offsets, stack variables, struct offsets, enums, chars,
`@PAGE`/`@PAGEOFF` pairs, hidden implicit operands. The printed line is the only place where all of that comes together,
and IDA marks it up with color tags. Parsing the tags gets you what IDA decided, without reimplementing its printer for
every processor.

The textbook alternative, `decode_insn` and `op_t.type` (`o_imm`, `o_mem`, `o_displ`, ...), tells you what the bytes
encode, not what they mean:

| Instruction | `decode_insn` says | IDA prints |
|---|---|---|
| PowerPC `addis r30, r30, (off_11FA38 - loc_1923C)@ha` | `o_imm`, value `0x10` | the high half of an address difference |
| RISC-V `auipc t2, %pcrel_hi(off_C8380)` | `o_imm`, value `0xbc` | a PC-relative reference to `off_C8380` |
| ARM64 `ADRP X16, #qword_11FFF8@PAGE` | `o_imm`, value `0x11f000` | a reference to `qword_11FFF8` |
| SPARC `add %fp, arg_727, %l1` | `o_imm`, value `0x727` | the stack argument `arg_727` |
| x86-64 `mov [rbp+var_30], rax` | `o_displ`, value `0xffffffffffffffd0` | the stack variable `var_30` |
| x86-64 `lea rdi, ds:1[rdx*2]` | `o_mem`, address `0x1` | a plain number |
| RISC-V `nop` | one operand, `o_imm` | no operands |
| PowerPC `mflr r0` | two operands, the second `o_idpspec0` | one operand |
| x86-64 `mul rcx` | two operands, implicit `rax` first | one operand, tagged as operand 2 |
