diff options
Diffstat (limited to '.auto/wip-digest.patch')
| -rw-r--r-- | .auto/wip-digest.patch | 162 |
1 files changed, 162 insertions, 0 deletions
diff --git a/.auto/wip-digest.patch b/.auto/wip-digest.patch new file mode 100644 index 0000000..d4d23e8 --- /dev/null +++ b/.auto/wip-digest.patch @@ -0,0 +1,162 @@ +diff --git a/idatui/domain.py b/idatui/domain.py +index 1f8263d..9cf539f 100644 +--- a/idatui/domain.py ++++ b/idatui/domain.py +@@ -673,6 +673,14 @@ class ListingModel: + #: Whether a rename has ever staled this model. Until one has, every + #: read takes exactly the path it always did. + self._renamed = False ++ #: Where each loaded page starts, so a stale-text refresh can ask the ++ #: worker "is this page still what I have?" over exactly the extent the ++ #: worker itself produced. Parallel lists: first head index, the address ++ #: it was fetched from, the digest it came back with, and its row count. ++ self._page_head: list[int] = [] ++ self._page_addr: list[int] = [] ++ self._page_digest: list[object] = [] ++ self._page_rows: list[int] = [] + #: Set if a text refresh came back with a different head sequence, which + #: means something DID move the walk. Program.listing() throws the model + #: away when it sees this, so the next read rebuilds from scratch. +@@ -758,6 +766,11 @@ class ListingModel: + page = self._build_page(rows) + with self._lock: + gen = self._text_gen ++ self._page_head.append(len(self._heads)) ++ self._page_addr.append(frm) ++ self._page_digest.append(payload.get("digest") ++ if isinstance(payload, dict) else None) ++ self._page_rows.append(len(rows)) + for h in page: + # Banner/label rows (function headers, separators, code labels) + # are display-only; don't index them so navigation lands on the +@@ -924,13 +937,65 @@ class ListingModel: + search body asks for thousands at once) would come back short, fail the + sequence check, and condemn the model to a rebuild it did not need. + """ +- blk = self.TEXT_BLOCK + with self._lock: + n = len(self._heads) +- start = (max(j0, 0) // blk) * blk +- while start < min(j1, n): +- self._ensure_text_block(start, min(start + blk, n)) +- start += blk ++ j0 = max(j0, 0) ++ j1 = min(j1, n) ++ while j0 < j1: ++ j0 = self._ensure_text_from(j0, j1, n) ++ ++ def _ensure_text_from(self, j0: int, j1: int, n: int) -> int: ++ """Freshen from head ``j0`` and return where to carry on. ++ ++ Tries the page ``j0`` falls in first: the worker can say whether that ++ page still renders exactly as it did, for the cost of the render alone ++ -- no rows on the wire, none unpickled, no Heads rebuilt. After a rename ++ nearly every page comes back identical, and that is 40% of what asking ++ for it again would have cost. Falls back to the block refetch when the ++ page has genuinely changed (or predates the digest). ++ """ ++ with self._lock: ++ p = bisect.bisect_right(self._page_head, j0) - 1 ++ usable = (0 <= p < len(self._page_head) ++ and self._page_digest[p] is not None) ++ if usable: ++ p_lo = self._page_head[p] ++ p_hi = (self._page_head[p + 1] if p + 1 < len(self._page_head) ++ else len(self._heads)) ++ gen = self._text_gen ++ fresh = all(self._head_gen[k] == gen for k in range(p_lo, p_hi)) ++ addr, want_dig = self._page_addr[p], self._page_digest[p] ++ want_rows = self._page_rows[p] ++ if usable and fresh: ++ return p_hi ++ if usable and self._verify_page(p, p_lo, p_hi, addr, want_dig, ++ want_rows, gen): ++ return p_hi ++ blk = self.TEXT_BLOCK ++ end = min(j0 + blk, j1 if usable else n) ++ self._ensure_text_block(j0, max(end, j0 + 1)) ++ return max(end, j0 + 1) ++ ++ def _verify_page(self, p: int, p_lo: int, p_hi: int, addr: int, ++ want_dig: object, want_rows: int, gen: int) -> bool: ++ """Ask whether page ``p`` still renders as it did; mark it fresh if so.""" ++ try: ++ payload = self._prog.client.call( ++ "heads", addr=hex(addr), count=self.PAGE, annotate=True, ++ digest=True) ++ except Exception: # noqa: BLE001 -- an older worker has no digest mode ++ return False ++ if not isinstance(payload, dict): ++ return False ++ got = payload.get("digest") ++ if got is None or got != want_dig or payload.get("count") != want_rows: ++ return False ++ with self._lock: ++ if self._text_gen != gen or len(self._heads) < p_hi: ++ return False ++ for k in range(p_lo, p_hi): ++ self._head_gen[k] = gen ++ return True + + def _ensure_text_block(self, j0: int, j1: int) -> None: + """Re-render one block, snapped out to whole ADDRESS groups. +diff --git a/server/patch_server.py b/server/patch_server.py +index f3cbe35..a34a4a9 100644 +--- a/server/patch_server.py ++++ b/server/patch_server.py +@@ -576,6 +576,28 @@ def _idatui_spans(line): + return [[k, t] for k, t, _o in out], trimmed + + ++def _idatui_rows_digest(rows): ++ """A value that changes whenever any of ``rows`` would render differently. ++ ++ Covers everything a client keeps off a row: address, kind, size, the plain ++ text, the symbol name and the colour spans (which is what makes it exact ++ rather than a heuristic -- two lines can collapse to the same text and still ++ be coloured differently). ++ ++ Uses the interpreter's own ``hash``, deliberately. It never has to mean ++ anything outside this process: the client stores what a page hashed to when ++ it loaded it and hands the same number back to ask whether the page still ++ hashes to that. One worker, one process, one hash seed. ++ """ ++ acc = 0 ++ for r in rows: ++ sp = r.get("spans") ++ acc = hash((acc, r.get("ea"), r.get("kind"), r.get("size"), ++ r.get("text"), r.get("name"), ++ tuple(map(tuple, sp)) if sp else None)) ++ return acc ++ ++ + def _idatui_unknown_row(ea, size): + """One collapsed row for a run of ``size`` undefined bytes starting at + ``ea``. A single byte is rendered normally (shows its value); a longer run +@@ -661,6 +683,7 @@ def heads( + end: Annotated[str, "Optional exclusive end address; default = segment end"] = "", + back: Annotated[bool, "Walk backwards: return the count heads ENDING just before addr, in forward order"] = False, + annotate: Annotated[bool, "Emit IDA-style function boundary banner rows (kind sep/funchdr)"] = False, ++ digest: Annotated[bool, "Return only a digest+count of the rows, not the rows themselves"] = False, + ) -> dict: + """Walk item heads from ``addr`` as a flat listing: every head is rendered + (code OR data OR undefined) via generate_disasm_line and stepped with +@@ -767,7 +790,17 @@ def heads( + rows.extend(_rows_for(ea)) # a struct head expands into member rows + ea = _advance(ea) + cursor = {"next": hex(ea)} if more else {"done": True} +- return {"addr": str(addr), "heads": rows, "cursor": cursor} ++ out = {"addr": str(addr), "cursor": cursor, ++ "digest": _idatui_rows_digest(rows), "count": len(rows)} ++ # ``digest`` mode answers "is this page still exactly what you have?" without ++ # shipping it. The rows are built either way -- generate_disasm_line is the ++ # floor and there is no way to know a line is unchanged without rendering it ++ # -- but pickling several hundred rows with their colour spans, unpickling ++ # them and rebuilding Heads is about 40% of what a page costs, and after a ++ # rename almost every page comes back identical. ++ if not digest: ++ out["heads"] = rows ++ return out + + + @tool |
