aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorblasty <peter@haxx.in>2026-09-25 14:28:45 +0200
committerblasty <peter@haxx.in>2026-09-25 14:28:45 +0200
commit9dea7bc83ff9b3b952a761a2d0f92b75abdd6e7e (patch)
treed0ccd92980a96250596a4aaaebccf21f0cffd74d /tests
parentREADME: friendlier rewrite; move IDA connection internals to CONTRIBUTING (diff)
parentFix parsing of pseudocode tags (diff)
downloadida-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.py58
-rw-r--r--tests/test_remote_tags.py134
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())