aboutsummaryrefslogtreecommitdiffstats
path: root/idatui
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-25 16:26:40 +0200
committerblasty <blasty@local>2026-07-25 16:26:40 +0200
commit391422bc805fb8620a77cc3f82787da989f1b2cf (patch)
treef9f585a86488cacaa43fb44fc97eaa90433038d9 /idatui
parenttheme: rebuild the code palette on measured contrast, not taste (diff)
downloadida-tui-391422bc805fb8620a77cc3f82787da989f1b2cf.tar.gz
ida-tui-391422bc805fb8620a77cc3f82787da989f1b2cf.tar.xz
ida-tui-391422bc805fb8620a77cc3f82787da989f1b2cf.zip
projects: don't duplicate a binary that's already in the project
`--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.
Diffstat (limited to 'idatui')
-rw-r--r--idatui/launch.py16
-rw-r--r--idatui/project.py26
2 files changed, 36 insertions, 6 deletions
diff --git a/idatui/launch.py b/idatui/launch.py
index 0ae63a1..9618668 100644
--- a/idatui/launch.py
+++ b/idatui/launch.py
@@ -67,10 +67,18 @@ def main(argv: list[str] | None = None) -> int:
try:
if os.path.isfile(ppath):
project = Project.load(ppath)
- for b in args.binary: # extend an existing project
- project.add(b)
- if args.binary:
- project.save()
+ if args.binary: # extend an existing project, skipping repeats
+ before = len(project.refs)
+ for b in args.binary:
+ project.add(b)
+ added = len(project.refs) - before
+ dupes = len(args.binary) - added
+ if added:
+ project.save()
+ _log(f"added {added} binary(ies) to {ppath}")
+ if dupes:
+ _log(f"{dupes} already in the project (matched by path) "
+ f"— left alone")
elif args.binary:
project = Project.create(ppath, args.binary)
_log(f"created project {ppath} with {len(project.refs)} binaries")
diff --git a/idatui/project.py b/idatui/project.py
index 2d417df..ac4f033 100644
--- a/idatui/project.py
+++ b/idatui/project.py
@@ -123,8 +123,14 @@ class Project:
"""Write a new project file listing ``binaries`` (an ad-hoc project)."""
if not binaries:
raise ProjectError("a project needs at least one binary")
- entries = [{"path": os.path.abspath(os.path.expanduser(b))}
- for b in binaries]
+ entries, seen = [], set()
+ for b in binaries: # the same file twice on one command line is a typo
+ p = os.path.abspath(os.path.expanduser(b))
+ key = os.path.realpath(p)
+ if key in seen:
+ continue
+ seen.add(key)
+ entries.append({"path": p})
path = os.path.abspath(os.path.expanduser(path))
proj = cls(path, name or os.path.splitext(os.path.basename(path))[0],
entries, memory_pct)
@@ -188,7 +194,23 @@ class Project:
def by_label(self, label: str) -> BinaryRef | None:
return next((r for r in self._refs if r.label == label), None)
+ def by_source(self, binary: str) -> BinaryRef | None:
+ """The entry for ``binary``, matched by resolved path.
+
+ Identity is the real path, not the file name: a project can legitimately
+ hold two different ``foo.elf`` from different directories (the labels
+ disambiguate them), but the same file must not be listed twice — and
+ ``./a.elf``, ``/abs/a.elf`` and a symlink to it are all the same file.
+ """
+ key = os.path.realpath(os.path.abspath(os.path.expanduser(binary)))
+ return next((r for r in self._refs
+ if os.path.realpath(r.source) == key), None)
+
def add(self, binary: str, label: str | None = None) -> BinaryRef:
+ """Add a binary, or return the existing entry if it's already here."""
+ existing = self.by_source(binary)
+ if existing is not None:
+ return existing
entry = {"path": os.path.abspath(os.path.expanduser(binary))}
if label:
entry["label"] = label