diff options
| author | user <user@clank> | 2026-08-07 08:01:25 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-07 08:01:25 +0200 |
| commit | 16318e4bfd939666bc45fa6f5d2c17cb8f374f3b (patch) | |
| tree | e0395a59d86bb7a20899aa06b7569cf781fb1e93 /.auto/check_edit.py | |
| parent | Three redundancies in the heads walk: item flags were fetched three times per... (diff) | |
| download | ida-tui-16318e4bfd939666bc45fa6f5d2c17cb8f374f3b.tar.gz ida-tui-16318e4bfd939666bc45fa6f5d2c17cb8f374f3b.tar.xz ida-tui-16318e4bfd939666bc45fa6f5d2c17cb8f374f3b.zip | |
An item edit (c/d/u/p) keeps the listing's walk in front of it instead of discarding the model. bump_items now takes the edited address; rows before an edit keep their addresses and their row numbers, so only the pages from the edit onward are re-walked. Getting the listing back after undefining at the cursor on bash: 4890ms -> 19ms (257x). Adds .auto/check_edit.py to the gate. total_ms is flat — the bench has no item-edit phase, and the one I wrote hangs (reverted, cause recorded).
Result: {"status":"keep","total_ms":25783,"lg_boot_ms":777.9,"lg_decomp_ms":2381.9,"lg_graph_ms":1209.5,"lg_hex_ms":450,"lg_index_ms":68,"lg_listing_cold_ms":434.6,"lg_listing_warm_ms":405.2,"lg_nav_ms":6801.2,"lg_palette_ms":4.7,"lg_rename_ms":744.7,"lg_render_ms":222.4,"lg_search_ms":3885.6,"lg_split_ms":2261.4,"pure_graph_ms":216.4,"sm_boot_ms":463.6,"sm_decomp_ms":1304.6,"sm_graph_ms":740.9,"sm_hex_ms":438.4,"sm_index_ms":2.4,"sm_listing_cold_ms":270.3,"sm_listing_warm_ms":267.2,"sm_nav_ms":312.4,"sm_palette_ms":0.3,"sm_rename_ms":415.8,"sm_render_ms":257.7,"sm_search_ms":60.8,"sm_split_ms":1385.2,"fails":0}
Diffstat (limited to '.auto/check_edit.py')
| -rw-r--r-- | .auto/check_edit.py | 126 |
1 files changed, 126 insertions, 0 deletions
diff --git a/.auto/check_edit.py b/.auto/check_edit.py new file mode 100644 index 0000000..2472c84 --- /dev/null +++ b/.auto/check_edit.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +"""Correctness gate for the listing after an item edit (run by .auto/checks.sh). + +An item edit (`c`/`d`/`u`/`p`) changes structure, but only locally: every head +in front of it keeps its address and its row number. So `Program.bump_items(ea)` +keeps the walk up to there instead of discarding the model — worth 257x on a big +binary (4.9s to make one byte into data, against 19ms). + +Keeping *anything* across a structural edit is the risky half of that, and it +fails silently: the pane shows rows that are no longer what the database says. +So this drives real edits and compares the kept model against one built from +scratch, row for row — narrow reads (what painting does) and wide ones (what +building the search body does). + +The staged database is a throwaway copy and is never saved, so the edits here do +not need undoing and can be as destructive as they like. + + ~/ida-venv/bin/python .auto/check_edit.py [targets/echo] +""" +from __future__ import annotations + +import os +import shutil +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(HERE) +sys.path.insert(0, ROOT) +sys.path.insert(0, HERE) + +from bench import stage # noqa: E402 +from idatui.domain import Program # noqa: E402 +from idatui.worker_client import WorkerClient # noqa: E402 + + +def snapshot(model, total: int, wide: bool): + if wide: + rows = [] + for base in range(0, total, 4096): + rows.extend(model.window(base, min(4096, total - base))) + else: + rows = [model.get(i) for i in range(total)] + return [(h.ea, h.kind, h.size, h.text, h.name) if h else None for h in rows] + + +def main() -> int: + target = sys.argv[1] if len(sys.argv) > 1 else "targets/echo" + d, path = stage(os.path.join(ROOT, target)) + fails: list[str] = [] + client = WorkerClient(path) + try: + prog = Program(client) + idx = prog.functions() + idx.load_all() + funcs = sorted(idx.all_loaded(), key=lambda f: f.addr) + if len(funcs) < 8: + print(f"{target}: too few functions to check") + return 1 + seg = funcs[len(funcs) // 2].addr + model = prog.listing(seg) + model.load_all() + total = len(model) + + # Edits at a spread of positions: near the end (where truncation keeps + # nearly everything), the middle, and early on (where it must give up). + spots = [funcs[int(len(funcs) * f)].addr for f in (0.9, 0.5, 0.05)] + for k, ea in enumerate(spots): + # Undefine, at three sizes. This is the edit that can coalesce + # BACKWARDS into the undefined run in front of it, which is the + # reason truncate_from drops two pages rather than one. + size = (1, 4, 16)[k % 3] + kind = f"undefine {size}B" + try: + prog.undefine(ea, size) + except Exception as e: # noqa: BLE001 + fails.append(f"{kind} at {ea:#x} failed: {e}") + continue + prog.bump_items(ea) + + kept = prog.listing(seg) + kept_total = None + got_wide = None + if kept is not None: + kept.load_all() + kept_total = len(kept) + got_wide = snapshot(kept, kept_total, wide=True) + got_narrow = snapshot(kept, kept_total, wide=False) + + # ... against a model that knows nothing about what came before. + prog._listings.clear() + fresh = prog.listing(seg) + fresh.load_all() + want = snapshot(fresh, len(fresh), wide=False) + + if kept_total != len(fresh): + fails.append(f"{kind} at {ea:#x}: kept model has {kept_total} " + f"rows, a rebuild has {len(fresh)}") + elif got_narrow != want or got_wide != want: + which = "narrow" if got_narrow != want else "wide" + bad = next((i for i, (a, b) in + enumerate(zip(got_narrow if which == "narrow" + else got_wide, want)) if a != b), None) + fails.append( + f"{kind} at {ea:#x}: {which} read differs from a rebuild at " + f"row {bad}: {(got_narrow if which == 'narrow' else got_wide)[bad]}" + f" vs {want[bad]}") + + model = prog.listing(seg) + model.load_all() + total = len(model) + + print(f"item edits: {len(spots)} edits checked against a rebuild " + f"({total} rows), {len(fails)} problems") + for f in fails: + print(" FAIL", f) + return 1 if fails else 0 + finally: + try: + client.close() + except Exception: # noqa: BLE001 + pass + shutil.rmtree(d, ignore_errors=True) + + +if __name__ == "__main__": + sys.exit(main()) |
