aboutsummaryrefslogtreecommitdiffstats
path: root/.auto/check_rename.py
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-07 05:52:49 +0200
committeruser <user@clank>2026-08-07 05:52:49 +0200
commit3ca9e1ea6dacd9cdfd545902eb8c0838dd383eed (patch)
tree84fcfcb5d4a6f5d4dd3b5128d2739a3d424abcaf /.auto/check_rename.py
parentA rename keeps the listing's walk instead of throwing it away. bump_names now... (diff)
downloadida-tui-3ca9e1ea6dacd9cdfd545902eb8c0838dd383eed.tar.gz
ida-tui-3ca9e1ea6dacd9cdfd545902eb8c0838dd383eed.tar.xz
ida-tui-3ca9e1ea6dacd9cdfd545902eb8c0838dd383eed.zip
CORRECTNESS FIX, kept despite a worse metric. The un-chunked refresh was showing STALE NAMES on any wide read: one heads call for a search-sized window overflows the tool's 2000-row cap, the short response fails the sequence check, and the block is left with its old text. Refresh is now done a block at a time. Adds .auto/check_rename.py to the gate, which fails hard on the previous code and passes on this one.
Result: {"status":"keep","total_ms":28651.3,"lg_boot_ms":720.6,"lg_decomp_ms":2427.4,"lg_graph_ms":1222.4,"lg_hex_ms":440.9,"lg_index_ms":72.6,"lg_listing_cold_ms":433.6,"lg_listing_warm_ms":465.9,"lg_nav_ms":6809.7,"lg_palette_ms":4.7,"lg_rename_ms":710.1,"lg_render_ms":228.7,"lg_search_ms":6891.2,"lg_split_ms":2259.2,"pure_graph_ms":214.5,"sm_boot_ms":457.7,"sm_decomp_ms":1303.6,"sm_graph_ms":700.2,"sm_hex_ms":445.4,"sm_index_ms":2.4,"sm_listing_cold_ms":271.4,"sm_listing_warm_ms":270.9,"sm_nav_ms":309.2,"sm_palette_ms":0.3,"sm_rename_ms":386.6,"sm_render_ms":265.8,"sm_search_ms":70.1,"sm_split_ms":1266.2,"fails":0}
Diffstat (limited to '.auto/check_rename.py')
-rw-r--r--.auto/check_rename.py120
1 files changed, 120 insertions, 0 deletions
diff --git a/.auto/check_rename.py b/.auto/check_rename.py
new file mode 100644
index 0000000..c617aa6
--- /dev/null
+++ b/.auto/check_rename.py
@@ -0,0 +1,120 @@
+#!/usr/bin/env python3
+"""Correctness gate for the listing's rename handling (run by .auto/checks.sh).
+
+A rename does not move any listing row, so ``Program.bump_names`` keeps the
+segment's walk and only marks the rendered text stale; ``ListingModel`` re-renders
+a block at a time as rows are read. That is worth 500x on a big binary (the
+alternative re-walks the whole segment to find a row the cursor was already on),
+and it is exactly the kind of optimisation that fails *quietly*: the pane keeps
+showing the old name and nothing errors.
+
+Two things are checked, because they exercise different paths and only the first
+was ever caught by accident:
+
+* a NARROW read (``get`` per row, what painting does), and
+* a WIDE read (``window`` over thousands of rows, what search's body build does)
+
+both have to come back with the new name -- and the whole model has to match one
+built from scratch, row for row.
+
+ ~/ida-venv/bin/python .auto/check_rename.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 rename(client, ea: int, name: str) -> None:
+ client.call("rename", batch={"func": [{"addr": hex(ea), "name": name}]})
+
+
+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 = idx.all_loaded()
+ if len(funcs) < 4:
+ print(f"{target}: too few functions to check")
+ return 1
+ seg = sorted(funcs, key=lambda f: f.addr)[len(funcs) // 2].addr
+ model = prog.listing(seg)
+ model.load_all()
+ total = len(model)
+
+ for k, victim in enumerate(sorted(funcs, key=lambda f: -f.size)[:2]):
+ new = f"_check_rename_{os.getpid()}_{k}"
+ rename(client, victim.addr, new)
+ prog.bump_names()
+
+ # WIDE read -- what building the search body does. This is the one
+ # that used to come back with the old names: an oversized refetch
+ # overflowed the heads tool's row cap, failed its sequence check and
+ # left the block untouched.
+ wide = []
+ for base in range(0, total, 4096):
+ wide.extend(model.window(base, min(4096, total - base)))
+ # "Is the old name gone" is not a sound test -- `main` is a token of
+ # `__libc_start_main` and can legitimately appear in a comment or a
+ # string. That the NEW name arrived proves the refresh ran; that the
+ # model matches a rebuild, below, proves it ran correctly.
+ shown = sum(1 for h in wide if h is not None
+ and (new in (h.text or "") or new == (h.name or "")))
+ if not shown:
+ fails.append(f"wide read after renaming {victim.name} -> {new}: "
+ f"no row shows the new name")
+
+ # NARROW read -- what painting does -- and the whole model against a
+ # rebuild, row for row.
+ kept = [(h.ea, h.kind, h.text, h.name) if h else None
+ for h in (model.get(i) for i in range(total))]
+ prog._listings.clear()
+ fresh_model = prog.listing(seg)
+ fresh_model.load_all()
+ fresh = [(h.ea, h.kind, h.text, h.name) if h else None
+ for h in (fresh_model.get(i) for i in range(len(fresh_model)))]
+ if kept != fresh:
+ bad = next((i for i, (a, b) in enumerate(zip(kept, fresh))
+ if a != b), None)
+ fails.append(f"kept model != rebuilt model after renaming "
+ f"{victim.name}: first difference at row {bad}: "
+ f"{kept[bad] if bad is not None else None} vs "
+ f"{fresh[bad] if bad is not None else None}")
+
+ rename(client, victim.addr, victim.name)
+ prog.bump_names()
+ prog._listings.clear()
+ model = prog.listing(seg)
+ model.load_all()
+ total = len(model)
+
+ print(f"rename handling: {total} rows checked wide and narrow, "
+ f"{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())