aboutsummaryrefslogtreecommitdiffstats
path: root/idatui/remote_tools.py
diff options
context:
space:
mode:
Diffstat (limited to 'idatui/remote_tools.py')
-rw-r--r--idatui/remote_tools.py89
1 files changed, 89 insertions, 0 deletions
diff --git a/idatui/remote_tools.py b/idatui/remote_tools.py
index d2cb02e..614fba4 100644
--- a/idatui/remote_tools.py
+++ b/idatui/remote_tools.py
@@ -510,6 +510,95 @@ def _idatui_func_footer_rows(ea, func):
]
+def segment_index(
+ addr: Annotated[str, "Any address in the segment to index"],
+ end: Annotated[str, "Optional exclusive end address; default = segment end"] = "",
+ page_rows: Annotated[int, "Rows between anchors (default 500)"] = 500,
+) -> dict:
+ """How many listing rows a segment has, and where to seek into it.
+
+ The listing needs a total row count to size its scrollbar, and the only way
+ to get one used to be to fetch every row: 455 round trips and 227k rows for
+ a 1.2MB bash, none of which is looked at. This walks the same items and
+ counts what ``heads`` WOULD emit, without building or rendering any of them.
+
+ Returns ``{rows, heads, anchors}`` where anchors is ``[[row, ea], ...]``
+ every ``page_rows`` logical rows -- enough to turn "show me row N" into a
+ ``heads(addr=anchor)`` call, so pages can be fetched on demand instead of
+ streamed in order.
+
+ **The count must match what heads() actually emits, exactly**, or the
+ scrollbar lies and a jump lands on the wrong row. It therefore mirrors
+ _rows_for's arithmetic rather than approximating it: 3 banner rows at a
+ function start, a label row for a named code head that is not one, the head
+ row itself, struct member rows for data, 2 footer rows at a function end,
+ and an undefined run counted as its byte length (the client presents one
+ collapsed row as that many logical rows). Verified equal to summing the real
+ pages, head for head, over a whole segment.
+ """
+
+ count = max(int(page_rows), 1)
+ try:
+ start = parse_address(addr)
+ except Exception as e:
+ return {"addr": str(addr), "error": str(e), "rows": 0, "anchors": []}
+ import ida_segment
+ seg = ida_segment.getseg(start)
+ if not seg:
+ return {"addr": str(addr), "error": "no segment", "rows": 0, "anchors": []}
+ lo, hi = seg.start_ea, seg.end_ea
+ if end:
+ try:
+ hi = min(hi, parse_address(end))
+ except Exception:
+ pass
+
+ get_flags = ida_bytes.get_flags
+ get_item_end = ida_bytes.get_item_end
+ next_head = ida_bytes.next_head
+ get_ea_name = ida_name.get_ea_name
+ get_func = idaapi.get_func
+ BAD = idaapi.BADADDR
+
+ rows = 0
+ n_heads = 0
+ anchors = []
+ fn = None
+ ea = ida_bytes.get_item_head(lo)
+ while ea != BAD and ea < hi:
+ if rows // count >= len(anchors):
+ anchors.append([rows, hex(ea)])
+ f = get_flags(ea)
+ cls = f & _MS_CLS
+ if cls != _FF_CODE and cls != _FF_DATA:
+ # An undefined run is ONE emitted row that PRESENTS as one logical
+ # row per byte (see _idatui_unknown_row and the client's _span).
+ nh = next_head(ea, hi)
+ stop = nh if (nh != BAD and ea < nh <= hi) else hi
+ run = stop - ea
+ rows += run if run > 1 else 1
+ n_heads += 1
+ ea = stop
+ continue
+ if fn is None or not (fn.start_ea <= ea < fn.end_ea):
+ fn = get_func(ea)
+ n = 1
+ if fn is not None and fn.start_ea == ea:
+ n += 3 # blank, banner, `proc`
+ elif cls == _FF_CODE and get_ea_name(ea):
+ n += 1 # loc_XXX label on its own row
+ if cls == _FF_DATA:
+ n += len(_idatui_struct_member_rows(ea))
+ item_end = get_item_end(ea)
+ if fn is not None and item_end >= fn.end_ea:
+ n += 2 # `endp` + separator
+ rows += n
+ n_heads += 1
+ ea = item_end if item_end > ea else ea + 1
+ return {"addr": hex(lo), "end": hex(hi), "rows": rows,
+ "heads": n_heads, "anchors": anchors}
+
+
def heads(
addr: Annotated[str, "Start address or name to walk from"],
count: Annotated[int, "Max heads to return (default 200, max 2000)"] = 200,