diff options
Diffstat (limited to 'idatui/project.py')
| -rw-r--r-- | idatui/project.py | 111 |
1 files changed, 103 insertions, 8 deletions
diff --git a/idatui/project.py b/idatui/project.py index 2d417df..e2fc542 100644 --- a/idatui/project.py +++ b/idatui/project.py @@ -52,12 +52,44 @@ class BinaryRef: label: str # unique within the project; names the staged file source: str # absolute path to the original binary staged: str # absolute path IDA actually opens (inside the sidecar) + #: How to LOAD it. Only meaningful for a headerless blob: an ELF/PE says what + #: it is, a raw firmware image doesn't, and IDA defaults to metapc at 0. + processor: str = "" # IDA processor name: arm, armb, mipsb, metapc, … + base: int = 0 # load address (natural, e.g. 0x8000000) + ida_args: str = "" # escape hatch: extra IDA command-line switches @property def db(self) -> str: """The database IDA creates for the staged file.""" return self.staged + ".i64" + @property + def load_args(self) -> str: + """``processor``/``base`` as IDA command-line switches. + + ``-b`` is in PARAGRAPHS, not bytes — ``-b1000`` loads at 0x10000. That + trap is worth hiding: projects say ``"base": "0x8000000"`` and the one + conversion lives in ``formats.load_args``. + """ + from .formats import load_args + return load_args(self.processor, self.base, self.ida_args) + + +def _as_addr(v) -> int: + """A load address from JSON: int, or a string in any base ("0x8000000"). + + Addresses are written by hand in a project file, so accept how people write + them rather than demanding decimal. + """ + if v is None or v == "": + return 0 + if isinstance(v, int): + return v + try: + return int(str(v), 0) + except ValueError: + return 0 + def _stat_key(path: str) -> tuple[int, int] | None: """(size, mtime) identity used to spot a source that changed under us.""" @@ -109,7 +141,12 @@ class Project: e = {"path": e} if not isinstance(e, dict) or not e.get("path"): raise ProjectError(f"project {path}: bad binary entry {e!r}") - norm.append({k: e[k] for k in ("path", "label") if e.get(k)}) + # Keep every recognised key: a whitelist of path/label silently + # dropped the load options on the first save, so a blob's processor + # and base vanished the moment the project was reopened. + norm.append({k: e[k] for k in + ("path", "label", "processor", "base", "ida_args") + if e.get(k) not in (None, "")}) name = raw.get("name") or os.path.splitext(os.path.basename(path))[0] try: pct = int(raw.get("memory_pct", DEFAULT_MEMORY_PCT)) @@ -119,12 +156,25 @@ class Project: @classmethod def create(cls, path: str, binaries: list[str], name: str | None = None, - memory_pct: int = DEFAULT_MEMORY_PCT) -> "Project": - """Write a new project file listing ``binaries`` (an ad-hoc project).""" + memory_pct: int = DEFAULT_MEMORY_PCT, load: dict | None = None) -> "Project": + """Write a new project file listing ``binaries`` (an ad-hoc project). + + ``load`` carries per-binary load options (processor/base/ida_args) that + apply to every binary given here — a headerless blob needs them, and one + command line normally adds blobs of the same kind. + """ 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) + e = {"path": p} + e.update({k: v for k, v in (load or {}).items() if v}) + entries.append(e) path = os.path.abspath(os.path.expanduser(path)) proj = cls(path, name or os.path.splitext(os.path.basename(path))[0], entries, memory_pct) @@ -177,21 +227,66 @@ class Project: n += 1 label = f"{label}_{n}" used.add(label) - refs.append(BinaryRef(label=label, source=src, - staged=os.path.join(self.bin_dir, label))) + refs.append(BinaryRef( + label=label, source=src, + staged=os.path.join(self.bin_dir, label), + processor=str(e.get("processor") or ""), + base=_as_addr(e.get("base")), + ida_args=str(e.get("ida_args") or ""))) return tuple(refs) @property def refs(self) -> tuple[BinaryRef, ...]: return self._refs + def set_load(self, label: str, processor: str = "", base: int = 0, + ida_args: str = "") -> BinaryRef | None: + """Record how ``label`` should be loaded, and persist it. + + Answered once: the dialog that asks writes the answer here, so reopening + the project doesn't ask again — and neither does adding the same blob to + another project, since it travels with the entry. + """ + ref = self.by_label(label) + if ref is None: + return None + i = self._refs.index(ref) + e = self._entries[i] + if processor: + e["processor"] = processor + if base: + e["base"] = int(base) + if ida_args: + e["ida_args"] = ida_args + self._refs = self._build_refs() + self.save() + return self._refs[i] + def by_label(self, label: str) -> BinaryRef | None: return next((r for r in self._refs if r.label == label), None) - def add(self, binary: str, label: str | None = None) -> BinaryRef: + 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, + load: dict | 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 + entry.update({k: v for k, v in (load or {}).items() if v}) self._entries.append(entry) self._refs = self._build_refs() return self._refs[-1] |
