<feed xmlns='http://www.w3.org/2005/Atom'>
<title>ida-tui.git/tests/test_blob_ui.py, branch main</title>
<subtitle>tui for headless ida</subtitle>
<id>https://git.sl0p.foo/ida-tui.git/atom/tests/test_blob_ui.py?h=main</id>
<link rel='self' href='https://git.sl0p.foo/ida-tui.git/atom/tests/test_blob_ui.py?h=main'/>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/'/>
<updated>2026-07-26T13:22:11Z</updated>
<entry>
<title>app: rebuild the function index after an edit changes it</title>
<updated>2026-07-26T13:22:11Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-07-26T13:22:11Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=cc7ef9bb2c3fdb23cde0162158184d15709af43c'/>
<id>urn:sha1:cc7ef9bb2c3fdb23cde0162158184d15709af43c</id>
<content type='text'>
"even when I define functions with `p` I still get 'no functions: wrong
processor/base?'". Two bugs, and the second is the worse one.

The hint was LATCHED at load and only cleared on a reload, so it went on telling
you the image was described wrongly long after you'd proved otherwise. It is now
derived: the moment the index has a function, it stops being true.

But the index never had one. Nothing rebuilt _func_index after an edit, so `p`
gave you a function the rest of the app could not see — the names pane didn't
list it and Ctrl+N couldn't find it. The hint was just the visible symptom of
that.

_edit_done now reindexes when the edit changed which functions exist (`p` and
`u`; carving code doesn't, and a full walk after every `c` would be waste). It
uses its own worker rather than _load_functions(), which is the BOOT path — that
one clears the table, streams progress and auto-lands, which would yank the view
off the function you just made.

Verified on a blob with no functions: carve, `p`, and the status reads "created
function 0x4040–0x404c", the index reports 1, and the hint is gone.

Also: the reload confirmation said "1 functions". It counts now.

tests: +4 blob UI (30) — no functions and the hint says so, `p` creates one the
index can see, the stale hint is gone, the status names it. The Ctrl+L check
became wrong in the good way and now asserts the confirmation counts what would
be lost, since by then there IS something to lose. 209/0 scenarios, 13/0 thumb,
30/0 project UI.
</content>
</entry>
<entry>
<title>app: one way to preserve view state across a model rebuild (ViewAnchor)</title>
<updated>2026-07-26T08:03:05Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-07-26T08:03:05Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=eea10d6230b1d6c460767b701fcd58d2f965c7b4'/>
<id>urn:sha1:eea10d6230b1d6c460767b701fcd58d2f965c7b4</id>
<content type='text'>
Two bugs in two days had the same shape — an edit rebuilds the model and
whatever ran last wins. The status message got clobbered by the reload's own
status write; the scroll position got recomputed from a row index that no longer
meant the same thing. Both were patched by hand. A third was coming.

ViewAnchor makes it one thing: where you are looking, in ADDRESSES, plus the
message the rebuild must not eat. _anchor() captures it on the UI thread before
the edit; _anchor_rows() resolves it against the rebuilt model; _edit_done()
settles the aftermath. The edit paths (_do_edit_item, _do_make_data) now hand one
object through instead of threading positions and messages separately.

Addresses, not indices, because an edit can change how many rows an item takes:
four undefined byte rows collapse into one instruction row, undefining does the
reverse. An index means a different place afterwards.

_reload_active_code deliberately does NOT use it. Renames and comments don't
change row structure, and the model that path rebuilds is constructed empty —
index_of_ea returns -1 until pages load, so an anchor would resolve to nothing
while costing an extra model build on the UI thread. Wrote that out and reverted
it rather than leave an abstraction applied where it does nothing.

Also fixed in passing: _do_make_data had the same latent bug (no scroll
preservation at all) and now goes through the same path.

The bigger find is in TODO. The scenario suite mutates targets/echo.i64 and
SAVES it, so a scenario that undefines an instruction breaks later runs
permanently — decomp_follow_self had been failing on a polluted database, not on
any code change. It also made an edit-position check look flaky one run in
three, which I nearly wrote up as a race. Coverage for this lives in
test_blob_ui.py instead, which builds a throwaway binary and can mutate freely.

tests: +1 blob UI (commenting leaves the view where it was), alongside the
carve checks. 26/0 blob, 202/0 scenarios (on a fresh .i64), 30/0 project UI.
</content>
</entry>
<entry>
<title>listing: an edit must not move the view</title>
<updated>2026-07-26T07:37:47Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-07-26T07:37:47Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=8add668ad1f10f77132de36b062d16a9e4103989'/>
<id>urn:sha1:8add668ad1f10f77132de36b062d16a9e4103989</id>
<content type='text'>
Pressing `c` jumped the scroll position. Defining an item reloads the view, and
the reload only carried the cursor's row index — the viewport was recomputed
from scratch, so you landed somewhere else and lost your place mid-carve.

Row indices are the wrong thing to remember across this reload anyway: carving
COLLAPSES rows (four undefined byte rows become one instruction row), so the row
that was at the top is a different address afterwards. The anchor has to be the
top visible ADDRESS, resolved back to a row after the model is rebuilt.

on_edit_item_requested captures it before the edit, _do_edit_item resolves it
against the new model, and _open_at grew a scroll_y so the entry can carry it.

Verified: cursor at 0x4800 with the top of the screen at 0x47da, press `c`, and
both are unchanged afterwards.

tests: +3 blob UI (scrolled far enough to have something to lose, top address
unchanged, cursor address unchanged). 25/0 blob, 202/0 scenarios.
</content>
</entry>
<entry>
<title>listing: `c` disassembles until something stops it</title>
<updated>2026-07-26T07:30:12Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-07-26T07:30:12Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=4e65c3ba16b13c3bb042830b020788a745cee9a9'/>
<id>urn:sha1:4e65c3ba16b13c3bb042830b020788a745cee9a9</id>
<content type='text'>
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 -&gt; "defined 4 instructions (0x4040–0x4050) —
control flow ends here", with 0x4050 left as an undefined byte. Starting on
existing code -&gt; no-op. Random bytes -&gt; 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.
</content>
</entry>
<entry>
<title>listing: make every undefined byte its own row, so you can carve anywhere</title>
<updated>2026-07-26T07:21:52Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-07-26T07:21:52Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=3ba7518a600fceeffaa9c077a8f2855490f92ac7'/>
<id>urn:sha1:3ba7518a600fceeffaa9c077a8f2855490f92ac7</id>
<content type='text'>
Loading a blob and pressing `c` at 0 disassembles one instruction; everything
after it collapsed into a single row — "db 2044 dup(?)" — with no cursor
position anywhere inside it. There was no way to start a second instruction
stream at an arbitrary offset, which is most of what carving a firmware image
IS. In IDA every undefined byte is its own line and you just put the cursor on
one.

The collapse existed for a real reason (see the comment in the heads tool): a
.bss or a fresh blob would otherwise be millions of one-byte rows, and this
model materialises what it walks. Expanding physically would also make
`g &lt;far address&gt;` walk every byte in between.

So the run stays ONE physical head and PRESENTS as N logical rows. _row_at is a
prefix sum over heads, _phys() maps a row back to (head, byte offset), and the
text for an interior row is synthesised on demand — "db 4Ah", the actual value,
because the byte values are the whole point when you're looking for a stream.
Memory is unchanged (libcrypto: 1 head for its 80-byte .bss, 61MB RSS), and
index_of_ea into the middle of a run is 0.01ms via bisect.

Now: cursor on any byte, `c`, and you get an instruction; the bytes before it
stay individually addressable.

Two bugs found on the way:

* IDAToolError takes (tool, message) and five call sites in domain.py passed one
  string. Every one of those error paths raised TypeError INSTEAD of the real
  error — "define code @ 0x4020: Failed to create instruction" reached the user
  as "IDAToolError.__init__() missing 1 required positional argument". Fixed all
  five; the message that finally came through is what identified the next issue.
* Searching now walks one row per undefined byte, so _index_for_search is capped
  at 400k lines and says when it truncated, rather than grinding through a
  multi-megabyte blob nobody wants to text-search.

tests: test_blob_ui.py +8 — a run presents one row per byte, each is a single
addressable byte showing its value, an interior address resolves to its own row,
`c` on a chosen byte carves there, the carved row spans the instruction, and
neighbouring bytes stay addressable. Uses PLANTED A64 instructions, because
whether random bytes decode is chance and a test that depends on chance is
worthless on the run where it fails. 19/0 blob, 202/0 scenarios, 30/0 project UI.
</content>
</entry>
<entry>
<title>loading: a blob that analyses to nothing is still openable</title>
<updated>2026-07-26T06:58:31Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-07-26T06:58:31Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=a0cb1a1a6f8aca7a64c102dba388e899267d5bc1'/>
<id>urn:sha1:a0cb1a1a6f8aca7a64c102dba388e899267d5bc1</id>
<content type='text'>
Pick a random .bin, say ARM at 0x4000, and you got two empty panes and
"functions still loading…" — which was a lie; loading had finished. _auto_land
falls back to the symbol picker when there's no entry function, and the picker
answers an empty index with that message. Nothing ever opened.

Zero functions is not a corner case. It's exactly what a real firmware image
looks like when it's described wrongly, and IDA has no complaint of its own to
make about it, so this was the last silent-wrong-answer in the blob path.

Now:

* Land in the listing at the start of the image. The bytes exist even when no
  code was recognised, so there is always something to show.
* Say so, in the status bar, for as long as it stays true — an image with no
  functions is a property of the database, not an event, and writing it once
  meant the next status write erased it (the same clobber that bit the split
  view's "decompiling…").
* Ctrl+L re-asks. The .i64 has the old processor and base baked in and takes
  precedence over any switches, so reloading means deleting it; the confirmation
  says what that costs, and when there are no functions it says nothing is lost.

Verified with real keys on a random 64K blob: ARM @ 0x4000 lands showing
"db 65536 dup(?)" with the hint in the status; Ctrl+L -&gt; confirm -&gt; dialog -&gt;
metapc reloads at 0. The hint survives scrolling.

tests: new tests/test_blob_ui.py (11) driving a real random blob end to end —
lands, has rows, right base, honest status, hint survives navigation, Ctrl+L
offers the reload and declining leaves the binary open. 202/0 scenarios, 30/0
project UI.

(Harness note for future me: tmux send-keys reads "0x4000" as a hex KEY CODE and
sends U+4000. Use send-keys -l for literal text.)
</content>
</entry>
</feed>
