aboutsummaryrefslogtreecommitdiffstats
path: root/server/patch_server.py
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-22 23:50:40 +0200
committerblasty <blasty@local>2026-07-22 23:50:40 +0200
commitd4bd0f246237cd218a75e3014f80d7379e25194b (patch)
treebd7737b215c9e7c1a8b3aeea1d56e76bc27ce7ea /server/patch_server.py
parentapp: ListingView — virtualized flat code+data listing for regions (M2) (diff)
downloadida-tui-d4bd0f246237cd218a75e3014f80d7379e25194b.tar.gz
ida-tui-d4bd0f246237cd218a75e3014f80d7379e25194b.tar.xz
ida-tui-d4bd0f246237cd218a75e3014f80d7379e25194b.zip
server: coalesce undefined byte runs in the heads walker
The M2 fix (step by get_item_end so undefined bytes render and arbitrary addresses land exactly) emits one row per undefined byte, which would explode a large .bss/undefined region into millions of rows and make load_all crawl. Collapse a run of consecutive undefined bytes into a single `db N dup(?)` row (its end found in O(1) via next_head, which skips undefined). A single stray undefined byte still renders normally (shows its value), so exact-address navigation and the c/p/u workflow are unchanged: a 42-byte gap is one row at the run start, and 'p' there auto-analyzes the function. Verified: undefining a function yields one `db 42 dup(?)` row (not 42); echo .text stays 5036 heads; data segments unaffected. region_define + listing_view + test_domain[listing] all green. Needs a supervisor restart.
Diffstat (limited to 'server/patch_server.py')
-rw-r--r--server/patch_server.py47
1 files changed, 41 insertions, 6 deletions
diff --git a/server/patch_server.py b/server/patch_server.py
index 3e5193b..52b81a6 100644
--- a/server/patch_server.py
+++ b/server/patch_server.py
@@ -209,6 +209,23 @@ def _idatui_head_row(ea):
return row
+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
+ collapses to ``db N dup(?)`` so a big .bss/gap doesn't explode into millions
+ of one-byte rows."""
+ import ida_name
+
+ if size <= 1:
+ return _idatui_head_row(ea)
+ row = {"ea": hex(ea), "kind": "unknown", "size": int(size),
+ "text": f"db {size} dup(?)"}
+ nm = ida_name.get_ea_name(ea)
+ if nm:
+ row["name"] = nm
+ return row
+
+
@tool
@idasync
def heads(
@@ -264,24 +281,42 @@ def heads(
# Walk by item END (not next_head): next_head SKIPS undefined bytes, but a
# flat listing must show them (IDA renders undefined as `db ?` lines, and
- # navigating to an unmarked address must land ON it). get_item_end steps by
- # the item's size for code/data and by 1 through undefined bytes.
- def _step(e):
+ # navigating to an unmarked address must land ON it). Defined items advance
+ # by get_item_end; a run of undefined bytes is COLLAPSED into one row (its
+ # end found in O(1) via next_head, which skips undefined) so a large .bss or
+ # gap doesn't explode into millions of one-byte rows.
+ def _is_unknown(e):
+ f = ida_bytes.get_flags(e)
+ return not (ida_bytes.is_code(f) or ida_bytes.is_data(f))
+
+ def _run_end(e):
+ """End (exclusive) of the undefined run starting at ``e``."""
+ nh = ida_bytes.next_head(e, hi)
+ return nh if (nh != idaapi.BADADDR and e < nh <= hi) else hi
+
+ def _advance(e):
+ if _is_unknown(e):
+ return _run_end(e)
nxt = ida_bytes.get_item_end(e)
return nxt if nxt > e else e + 1
+ def _row(e):
+ if _is_unknown(e):
+ return _idatui_unknown_row(e, _run_end(e) - e)
+ return _idatui_head_row(e)
+
ea = ida_bytes.get_item_head(start)
for _ in range(offset):
if ea >= hi or ea == idaapi.BADADDR:
break
- ea = _step(ea)
+ ea = _advance(ea)
more = False
while ea != idaapi.BADADDR and ea < hi:
if len(rows) >= count:
more = True
break
- rows.append(_idatui_head_row(ea))
- ea = _step(ea)
+ rows.append(_row(ea))
+ ea = _advance(ea)
cursor = {"next": hex(ea)} if more else {"done": True}
return {"addr": str(addr), "heads": rows, "cursor": cursor}
'''