diff options
| -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) |
