diff options
| author | blasty <blasty@local> | 2026-07-25 16:26:40 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-07-25 16:26:40 +0200 |
| commit | 391422bc805fb8620a77cc3f82787da989f1b2cf (patch) | |
| tree | f9f585a86488cacaa43fb44fc97eaa90433038d9 | |
| parent | theme: rebuild the code palette on measured contrast, not taste (diff) | |
| download | ida-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.
| -rw-r--r-- | idatui/launch.py | 16 | ||||
| -rw-r--r-- | idatui/project.py | 26 | ||||
| -rw-r--r-- | tests/test_project.py | 35 |
3 files changed, 71 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 diff --git a/tests/test_project.py b/tests/test_project.py index 1f93d4a..be46875 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -124,6 +124,41 @@ def main() -> int: extra = _bin(os.path.join(src, "extra")) proj2.add(extra, label="extra") check("add() appends a binary", proj2.by_label("extra") is not None) + + # -- re-adding must not duplicate (matched by resolved path) --------- # + n = len(proj2.refs) + proj2.add(extra) + check("re-adding the same path is a no-op", len(proj2.refs) == n, + f"{[r.label for r in proj2.refs]}") + os.chdir(src) + proj2.add("./extra") # same file, relative + proj2.add(os.path.join(src, "..", "src", "extra")) # same file, messy + check("a different spelling of the same path is a no-op", + len(proj2.refs) == n, f"{[r.label for r in proj2.refs]}") + link = os.path.join(src, "extra_link") + os.symlink(extra, link) + proj2.add(link) + check("a symlink to an existing binary is a no-op", + len(proj2.refs) == n, f"{[r.label for r in proj2.refs]}") + + # ...but a DIFFERENT file with the same basename must still be added + other_dir = os.path.join(tmp, "other2") + os.makedirs(other_dir) + twin = _bin(os.path.join(other_dir, "extra"), b"\x7fELF a different extra") + proj2.add(twin) + check("a same-named file from another directory IS added", + len(proj2.refs) == n + 1 + and proj2.by_source(twin) is not None + and proj2.by_source(extra) is not proj2.by_source(twin), + f"{[(r.label, r.source) for r in proj2.refs[-2:]]}") + check("the twins get distinct labels", + len({r.label for r in proj2.refs}) == len(proj2.refs), + f"{[r.label for r in proj2.refs]}") + check("create() also drops repeats on the command line", + len(Project.create(os.path.join(tmp, "dup.json"), + [extra, "./extra", extra]).refs) == 1) + os.chdir(tmp) + proj2.remove(proj2.by_source(twin).label) check("remove() drops one", proj2.remove("extra") and proj2.by_label("extra") is None) |
