diff options
| author | blasty <peter@haxx.in> | 2026-09-25 14:28:45 +0200 |
|---|---|---|
| committer | blasty <peter@haxx.in> | 2026-09-25 14:28:45 +0200 |
| commit | 9dea7bc83ff9b3b952a761a2d0f92b75abdd6e7e (patch) | |
| tree | d0ccd92980a96250596a4aaaebccf21f0cffd74d /tests | |
| parent | README: friendlier rewrite; move IDA connection internals to CONTRIBUTING (diff) | |
| parent | Fix parsing of pseudocode tags (diff) | |
| download | ida-tui-9dea7bc83ff9b3b952a761a2d0f92b75abdd6e7e.tar.gz ida-tui-9dea7bc83ff9b3b952a761a2d0f92b75abdd6e7e.tar.xz ida-tui-9dea7bc83ff9b3b952a761a2d0f92b75abdd6e7e.zip | |
Merge PR #2 from mrexodia: fix pseudocode/listing tag parsing, ida-nexus 0.13.0
https://github.com/blasty/ida-tui/pull/2 — pseudocode address anchors are
now split off only as the trailing /*0x…*/ our decompile tool appends, so a
marker-shaped string literal or source comment survives display and cursor
columns. The listing span walker asks the kernel (tag_skipcode) how long
each colour code is instead of assuming two bytes, so semantic group tags
with kind/TID headers stay invisible on kernels that emit them. Bumps
ida-nexus to 0.13.0.
Verified locally on IDA 9.4 + ida-nexus 0.13.0: full suite 974 passed
(955 on main + 19 new pure checks). Old vs new span walker give identical
output over 60k bash heads (no 0x36 tags on this kernel); the new walker
costs 19.8 vs 12.3 us/line, next to 7.6 us for generate_disasm_line.
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test_pseudocode.py | 58 | ||||
| -rw-r--r-- | tests/test_remote_tags.py | 134 |
2 files changed, 192 insertions, 0 deletions
diff --git a/tests/test_pseudocode.py b/tests/test_pseudocode.py new file mode 100644 index 0000000..b3b9556 --- /dev/null +++ b/tests/test_pseudocode.py @@ -0,0 +1,58 @@ +"""IDA-free regressions for pseudocode display text and address annotations.""" + +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from idatui.domain import split_pseudocode_line # noqa: E402 + +NEEDS_IDA = False +PASS = FAIL = 0 + + +def check(name, line, expected): + global PASS, FAIL + actual = split_pseudocode_line(line) + if actual == expected: + PASS += 1 + print(f" ok {name}") + else: + FAIL += 1 + print(f" FAIL {name}: {actual!r} != {expected!r}") + + +def main(): + check( + "trailing annotation", " return 42; /*0x401000*/", (" return 42;", 0x401000) + ) + source = ' printf("before /*0x1234*/ after", 42);' + check("marker-shaped string survives", source + " /*0x401000*/", (source, 0x401000)) + check("string without annotation is not an anchor", source, (source, None)) + source = " x = /*0x1234*/ y;" + check( + "inline source comment survives", source + " /*0x401000*/", (source, 0x401000) + ) + source = " return x; /*0x1234*/" + check( + "only the final annotation is removed", + source + " /*0x401000*/", + (source, 0x401000), + ) + check( + "non-address source comment", + " return x; /* explanation */", + (" return x; /* explanation */", None), + ) + check( + "annotation whitespace and hex casing", " x;\t/* 0xAbCd */ ", (" x;", 0xABCD) + ) + check("address zero is valid", "x; /*0x0*/", ("x;", 0)) + check("blank line", " ", ("", None)) + print(f"\n{PASS} passed, {FAIL} failed") + return bool(FAIL) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_remote_tags.py b/tests/test_remote_tags.py new file mode 100644 index 0000000..2c2b8aa --- /dev/null +++ b/tests/test_remote_tags.py @@ -0,0 +1,134 @@ +"""IDA-free regressions for the source-shipped listing tag parser.""" + +from __future__ import annotations + +import ast +import re +from pathlib import Path +from types import SimpleNamespace + +NEEDS_IDA = False +PASS = FAIL = 0 +ON, OFF, ESC, INV = "\x01", "\x02", "\x03", "\x04" +ADDR, SEM = "\x28", "\x36" +INSN, REG, NUM, OPND = "\x05", "\x29", "\x0c", "\x40" + + +def load_parser(semantic_headers=True): + # remote_tools is shipped as source and imports IDAPython at module scope. + # Compile the actual parser, not a copy, with only its IDA surface faked. + path = Path(__file__).resolve().parents[1] / "idatui" / "remote_tools.py" + tree = ast.parse(path.read_text()) + node = next( + n + for n in tree.body + if isinstance(n, ast.FunctionDef) and n.name == "_idatui_spans" + ) + calls = [] + + def skipcode(at): + calls.append(at) + if at[0] == INV: + return 1 + if at.startswith(ON + ADDR): + return 18 + if semantic_headers and at.startswith(ON + SEM): + return 19 if at[2] == "\x03" else 3 + return 2 + + ns = { + "_re": re, + "ida_lines": SimpleNamespace( + COLOR_ADDR=0x28, COLOR_ADDR_SIZE=16, tag_skipcode=skipcode + ), + "_IDATUI_TAGS": {INSN: "insn", REG: "reg", NUM: "num"}, + "_IDATUI_OPND_TAGS": {OPND: 0}, + "_IDATUI_CTL": None, + "_IDATUI_TAGINFO": None, + } + exec(compile(ast.Module(body=[node], type_ignores=[]), str(path), "exec"), ns) + return ns["_idatui_spans"], calls + + +def tag(t, text): + return ON + t + text + OFF + t + + +def check(name, actual, expected): + global PASS, FAIL + if actual == expected: + PASS += 1 + print(f" ok {name}") + else: + FAIL += 1 + print(f" FAIL {name}: {actual!r} != {expected!r}") + + +def main(): + parse, calls = load_parser() + check( + "ordinary colours and operand coordinates", + parse( + tag(INSN, "mov") + + " " + + tag(OPND, tag(REG, "eax") + ", " + tag(NUM, "42")) + ), + ( + [ + ["insn", "mov"], + ["text", " "], + ["reg", "eax"], + ["text", ", "], + ["num", "42"], + ], + [[4, 11, 0]], + ), + ) + # Semantic metadata can itself contain bytes that look like presentation + # controls. The kernel's header length, not a two-byte regex, owns them. + name = ON + SEM + "\x01" + "main" + OFF + SEM + check( + "semantic name preserves enclosing colour", + parse(tag(INSN, "call " + name)), + ([["insn", "call main"]], []), + ) + typed = ON + SEM + "\x03" + "0123456789ABCDEF" + tag(REG, "MY_TYPE") + OFF + SEM + check( + "type-id payload is invisible and operand survives", + parse(tag(OPND, typed + "+" + tag(NUM, "4"))), + ([["reg", "MY_TYPE"], ["text", "+"], ["num", "4"]], [[0, 9, 0]]), + ) + check( + "address payload is invisible", + parse(tag(REG, "a" + ON + ADDR + "0123456789ABCDEF" + "b")), + ([["reg", "ab"]], []), + ) + check( + "inverse is a one-byte code, including at end", + parse(tag(REG, "a" + INV + "b" + INV)), + ([["reg", "ab"]], []), + ) + check( + "escaped controls are literal", + parse("a" + ESC + ON + "b"), + ([["text", "a" + ON + "b"]], []), + ) + check("kernel skipcode is consulted", bool(calls), True) + old_parse, _ = load_parser(semantic_headers=False) + check( + "headerless semantic group preserves enclosing colour", + old_parse(tag(INSN, "call " + tag(SEM, "main"))), + ([["insn", "call main"]], []), + ) + check( + "unknown presentation tags retain their text", + parse(tag("\x7f", "unknown")), + ([["text", "unknown"]], []), + ) + check("empty line", parse(""), ([], [])) + print(f"\n{PASS} passed, {FAIL} failed") + return bool(FAIL) + + +if __name__ == "__main__": + raise SystemExit(main()) |
