aboutsummaryrefslogtreecommitdiffstats
path: root/idatui/project.py
diff options
context:
space:
mode:
Diffstat (limited to 'idatui/project.py')
-rw-r--r--idatui/project.py111
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]