aboutsummaryrefslogtreecommitdiffstats
path: root/tests/test_blob_ui.py
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-26 09:30:12 +0200
committerblasty <blasty@local>2026-07-26 09:30:12 +0200
commit4e65c3ba16b13c3bb042830b020788a745cee9a9 (patch)
tree791b3a94aec4e57801377943088a1dc0870704b4 /tests/test_blob_ui.py
parentlisting: make every undefined byte its own row, so you can carve anywhere (diff)
downloadida-tui-4e65c3ba16b13c3bb042830b020788a745cee9a9.tar.gz
ida-tui-4e65c3ba16b13c3bb042830b020788a745cee9a9.tar.xz
ida-tui-4e65c3ba16b13c3bb042830b020788a745cee9a9.zip
listing: `c` disassembles until something stops it
One instruction per keypress means pressing `c` once per opcode for the length of a routine, which on a raw image is the whole job. IDA's `c` runs; ours now does too. New define_code_run tool: create instructions consecutively and report why it stopped — 'undecodable' (bytes aren't an instruction), 'flow' (control flow ends here), 'defined' (ran into existing code/data), 'segment' or 'limit'. It loops inside the worker; from the client this would be one round trip per instruction, minutes on a real image. Stops AT a ret rather than past it: beyond the end of a routine the bytes are usually padding or data, and running on turns a clean carve into something you have to undo by hand. Stopping at already-defined items is the same principle — undefining someone's existing work to keep a speculative run going isn't a trade the user asked for. The ret test is ida_idp.is_ret_insn, NOT canonical features: on AArch64 insn.get_canon_feature() returns 0 for RET, so a CF_STOP check silently never fires and the run walks straight through the end of the function. Verified against a live IDA before relying on it. `c` on something already defined now says "already defined @ addr" instead of claiming the instruction failed to be created — count==0 from a run means two very different things. Also: the result message survives the reload. Defining an item rebuilds the view, and the reload's own cursor handler had the last word, so every edit reported itself as "ROM @ 0x4040 [listing]". A one-shot _flash is handed to whichever status write lands first after the edit. (Third time this clobber pattern has turned up: split view, the no-functions hint, now this.) Verified: nop/nop/nop/ret at 0x4040 -> "defined 4 instructions (0x4040–0x4050) — control flow ends here", with 0x4050 left as an undefined byte. Starting on existing code -> no-op. Random bytes -> stops at the first that won't decode. tests: +4 blob UI (runs to the end of flow, stops at the ret, doesn't touch the junk after it, and the status reports it). 22/0 blob, 202/0 scenarios, 30/0 project UI.
Diffstat (limited to 'tests/test_blob_ui.py')
-rw-r--r--tests/test_blob_ui.py18
1 files changed, 17 insertions, 1 deletions
diff --git a/tests/test_blob_ui.py b/tests/test_blob_ui.py
index 400c9ab..9161f44 100644
--- a/tests/test_blob_ui.py
+++ b/tests/test_blob_ui.py
@@ -55,7 +55,7 @@ async def run() -> int:
planted = 0x40 # file offset -> ea 0x4040
for k, insn in enumerate((0xD503201F, # nop
0xD503201F, # nop
- 0xD65F03C0)): # ret
+ 0xD65F03C0)): # ret <- the run must stop here
data[planted + k * 4:planted + k * 4 + 4] = insn.to_bytes(4, "little")
blob = os.path.join(tmp, "rnd.bin")
with open(blob, "wb") as f:
@@ -141,6 +141,22 @@ async def run() -> int:
f"kind={h.kind if h else None} text={h.text if h else None!r}")
if h is not None and h.kind == "code":
print(f" carved {target:#x}: {h.text}")
+ # `c` runs until something stops it, like IDA — one instruction
+ # at a time means pressing it once per opcode for the length of
+ # a routine. We planted nop/nop/nop/ret, so it must take all
+ # four and stop AT the ret, not run on into the random bytes
+ # after it.
+ run = [m.get(m.index_of_ea(target + k * 4)) for k in range(3)]
+ check("`c` keeps going until control flow ends",
+ all(x is not None and x.kind == "code" for x in run),
+ f"{[(hex(x.ea), x.kind) for x in run if x]}")
+ check("and stops at the ret instead of running into junk",
+ m.get(m.index_of_ea(target + 12)).kind == "unknown",
+ f"{m.get(m.index_of_ea(target + 12)).text!r}")
+ status = str(app.query_one("#status", Static).render())
+ check("the status reports what the run did",
+ "3 instructions" in status and "control flow" in status,
+ status[:80])
check("the carved row spans the instruction, not one byte",
h.size == 4, f"size={h.size}")
check("bytes before it stay individually addressable",