#!/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())