<feed xmlns='http://www.w3.org/2005/Atom'>
<title>ida-tui.git/tests/test_project.py, branch main</title>
<subtitle>tui for headless ida</subtitle>
<id>https://git.sl0p.foo/ida-tui.git/atom/tests/test_project.py?h=main</id>
<link rel='self' href='https://git.sl0p.foo/ida-tui.git/atom/tests/test_project.py?h=main'/>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/'/>
<updated>2026-07-25T21:49:14Z</updated>
<entry>
<title>loading: say how to read a headerless blob (processor, base)</title>
<updated>2026-07-25T21:49:14Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-07-25T21:49:14Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=57b09047881d4bb42b2b98e4bb0739d5bc5e5af8'/>
<id>urn:sha1:57b09047881d4bb42b2b98e4bb0739d5bc5e5af8</id>
<content type='text'>
A raw firmware dump has no format to detect, so IDA fell back to x86 at address
0. It doesn't fail — it opens, analyses, and finds nothing. An AArch64 image
loaded this way gave 0 functions; told the truth it gives 35.

    ida-tui fw.bin --processor arm --base 0x8000000

and per binary in a project, which is what a multi-image firmware actually
needs:

    {"path": "app.bin", "processor": "arm", "base": "0x8000000"}

idapro.open_database() already accepted IDA command-line switches; nothing was
passing any. Plumbed BinaryRef -&gt; WorkerPool -&gt; WorkerClient -&gt; worker argv, plus
a load_args for the single-binary path that has no project ref.

base is written the way people say it (0x8000000, int or string, any base).
IDA's -b is in PARAGRAPHS — -b1000 loads at 0x10000 — so BinaryRef.load_args
converts, and a base that isn't 16-byte aligned is refused rather than silently
landing 16x off. ida_args passes anything else through.

Two bugs found by testing the whole path rather than the happy one:

* Project.load() whitelisted path/label when normalising entries, so the load
  options were dropped the first time a project was reopened — set a processor,
  come back tomorrow, it's gone.
* Re-passing the switches to an EXISTING database makes IDA refuse the open
  (rc != 0, no functions). The .i64 already records how the image was loaded, so
  the worker skips them once a database exists. My first guard checked
  splitext(path) + ".i64" and never fired, because IDA names it "&lt;file&gt;.i64" —
  keeping the extension. It checks both spellings now.

Verified on a real AArch64 blob: fresh load 35 functions based at 0x8002440,
reopen 35 again, and the CLI rejects an unaligned or non-numeric --base.

tests: +6 project (options recorded, paragraph conversion, file round-trip,
add() takes them, an ELF passes nothing, hex-string base). 39/0 project, 195/0
scenarios, 30/0 project UI, 36/0 index, 27/0 pool.
</content>
</entry>
<entry>
<title>projects: don't duplicate a binary that's already in the project</title>
<updated>2026-07-25T14:26:40Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-07-25T14:26:40Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=391422bc805fb8620a77cc3f82787da989f1b2cf'/>
<id>urn:sha1:391422bc805fb8620a77cc3f82787da989f1b2cf</id>
<content type='text'>
`--project fw.json a.elf b.elf` where those are already listed appended them
again, so the project grew a second copy on every launch — each duplicate then
staged its own file, opened its own worker and got its own index entries.

Dedupe on the RESOLVED SOURCE PATH, which is the only identity that's actually
correct here: two different foo.elf from different directories are different
binaries and must both be accepted (the label disambiguator already gives them
foo.elf and foo.elf_2), while ./a.elf, /abs/a.elf and a symlink to it are all the
same file and must collapse to one entry.

* Project.by_source(path) — lookup by realpath.
* Project.add() returns the existing entry instead of appending a duplicate.
* Project.create() drops repeats on one command line too.
* launch.py reports what it did: "added N binary(ies)" / "N already in the
  project (matched by path) — left alone", and only rewrites the file when
  something actually changed.

Not deduped in _build_refs on load: remove() maps refs to entries by index, so
collapsing there would desync them, and a hand-edited duplicate still works
(labels disambiguate).

tests/test_project.py +8 checks: re-add is a no-op, so are a relative spelling,
a messy ../ path and a symlink; a same-named file from another directory IS
added and gets a distinct label; create() drops repeats. 33/33. Verified on the
real CLI: re-running the reported command leaves the project at 3 entries.
</content>
</entry>
<entry>
<title>projects: project model + binary staging (phase 1a)</title>
<updated>2026-07-25T09:26:11Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-07-25T09:26:11Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=54e5aab945d6c27edcaf7900c5d328384143fab2'/>
<id>urn:sha1:54e5aab945d6c27edcaf7900c5d328384143fab2</id>
<content type='text'>
First slice of multi-binary projects (docs/PROJECTS.md): the on-disk model, with
no runtime wiring yet.

idatui/project.py (stdlib-only, like domain/worker):
* Project.load/create/save — an explicit JSON project file listing binaries;
  paths resolve relative to it, labels default to the basename and are
  disambiguated on collision (they name files).
* A sidecar dir beside the project file (&lt;stem&gt;.idatui.d/) holds bin/ (staged
  binaries), their .i64 + scratch, and idx/ for phase 2. IDA opens the STAGED
  file, so nothing lands in the source tree — today targets/ carries ~244MB of
  IDA litter around ~13MB of binaries, much of it stale wedge files.
* stage() copies rather than hardlinks. A hardlink is free but makes source and
  staged one inode, so an in-place rebuild (cp over the path truncates instead of
  replacing) would silently swap the bytes under an analysed DB with nothing to
  detect it. The unit test caught exactly that. A copy also leaves the sidecar
  self-contained once the sources are gone.
* Re-staging a changed source drops its now-stale DB; sweep_scratch() clears the
  unpacked working files a hard-killed worker leaves behind (never the .i64).

tests/test_project.py: 27 checks, pure stdlib (no IDA/textual/worker), &lt;1s.

docs/PROJECTS.md: the full design — the one-worker-per-DB constraint with
measured costs (bash worker = 126MB RSS/117MB PSS; libcrypto's DB is 72MB, so
residency is budgeted by MEMORY, not a worker count), the switch between
"switching needs a live worker" and "searching doesn't (cached index)", and
phases 1-4.
</content>
</entry>
</feed>
