aboutsummaryrefslogtreecommitdiffstats
path: root/tests/test_project.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test_project.py')
-rw-r--r--tests/test_project.py229
1 files changed, 149 insertions, 80 deletions
diff --git a/tests/test_project.py b/tests/test_project.py
index bae0802..63d029e 100644
--- a/tests/test_project.py
+++ b/tests/test_project.py
@@ -15,7 +15,7 @@ import sys
import tempfile
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-from idatui.project import Project, ProjectError, SIDECAR_SUFFIX # noqa: E402
+from idatui.project import SIDECAR_SUFFIX, Project, ProjectError # noqa: E402
PASS = FAIL = 0
@@ -48,36 +48,48 @@ def main() -> int:
proj = Project.create(pfile, [httpd, libauth], name="router-fw")
check("create() writes the project file", os.path.isfile(pfile))
proj = Project.load(pfile)
- check("load() round-trips name + binaries",
- proj.name == "router-fw" and len(proj.refs) == 2,
- f"name={proj.name} n={len(proj.refs)}")
- check("labels default to the basename",
- [r.label for r in proj.refs] == ["httpd", "libauth.so"],
- f"{[r.label for r in proj.refs]}")
+ check(
+ "load() round-trips name + binaries",
+ proj.name == "router-fw" and len(proj.refs) == 2,
+ f"name={proj.name} n={len(proj.refs)}",
+ )
+ check(
+ "labels default to the basename",
+ [r.label for r in proj.refs] == ["httpd", "libauth.so"],
+ f"{[r.label for r in proj.refs]}",
+ )
# -- layout -------------------------------------------------------- #
- check("sidecar sits beside the project file",
- proj.sidecar == os.path.join(tmp, "router-fw" + SIDECAR_SUFFIX),
- proj.sidecar)
+ check(
+ "sidecar sits beside the project file",
+ proj.sidecar == os.path.join(tmp, "router-fw" + SIDECAR_SUFFIX),
+ proj.sidecar,
+ )
ref = proj.by_label("httpd")
- check("staged path lives in the sidecar, not the source tree",
- ref.staged.startswith(proj.bin_dir) and src not in ref.staged,
- ref.staged)
+ check(
+ "staged path lives in the sidecar, not the source tree",
+ ref.staged.startswith(proj.bin_dir) and src not in ref.staged,
+ ref.staged,
+ )
check("db path hangs off the staged file", ref.db == ref.staged + ".i64")
# -- staging: hardlink, freshness ---------------------------------- #
check("a binary starts out stale (not yet staged)", proj.is_stale(ref))
proj.stage(ref)
- check("stage() materialises the binary in the sidecar",
- os.path.isfile(ref.staged))
- check("stage() copies (distinct inode) so the source can't be mutated "
- "through it",
- os.stat(ref.staged).st_ino != os.stat(ref.source).st_ino
- and open(ref.staged, "rb").read() == open(ref.source, "rb").read())
+ check(
+ "stage() materialises the binary in the sidecar", os.path.isfile(ref.staged)
+ )
+ check(
+ "stage() copies (distinct inode) so the source can't be mutated through it",
+ os.stat(ref.staged).st_ino != os.stat(ref.source).st_ino
+ and open(ref.staged, "rb").read() == open(ref.source, "rb").read(),
+ )
check("a staged binary is no longer stale", not proj.is_stale(ref))
- check("source tree stays clean (no IDA artifacts beside it)",
- sorted(os.listdir(src)) == ["httpd", "libauth.so"],
- f"{sorted(os.listdir(src))}")
+ check(
+ "source tree stays clean (no IDA artifacts beside it)",
+ sorted(os.listdir(src)) == ["httpd", "libauth.so"],
+ f"{sorted(os.listdir(src))}",
+ )
# -- a changed source re-stages and drops the stale DB ------------- #
open(ref.db, "wb").write(b"fake i64")
@@ -88,11 +100,15 @@ def main() -> int:
os.utime(ref.source, (1, 1))
check("a source rebuilt in place goes stale", proj.is_stale(ref))
proj.stage(ref)
- check("re-staging refreshes the staged bytes",
- open(ref.staged, "rb").read().endswith(b"v2 (longer)"))
+ check(
+ "re-staging refreshes the staged bytes",
+ open(ref.staged, "rb").read().endswith(b"v2 (longer)"),
+ )
check("re-staging drops the now-stale database", not proj.has_db(ref))
- check("re-staging drops the stale scratch too",
- not os.path.exists(ref.staged + ".id0"))
+ check(
+ "re-staging drops the stale scratch too",
+ not os.path.exists(ref.staged + ".id0"),
+ )
# -- scratch sweep keeps the DB ------------------------------------ #
open(ref.db, "wb").write(b"fake i64")
@@ -107,22 +123,35 @@ def main() -> int:
os.makedirs(sub)
dup = _bin(os.path.join(sub, "httpd"), b"\x7fELF other httpd")
with open(pfile, "w") as f:
- json.dump({"name": "p", "binaries": [
- {"path": "src/httpd"}, # relative to the project file
- {"path": dup}, # same basename -> collision
- {"path": libauth, "label": "auth"},
- ]}, f)
+ json.dump(
+ {
+ "name": "p",
+ "binaries": [
+ {"path": "src/httpd"}, # relative to the project file
+ {"path": dup}, # same basename -> collision
+ {"path": libauth, "label": "auth"},
+ ],
+ },
+ f,
+ )
proj2 = Project.load(pfile)
- check("relative paths resolve against the project file",
- proj2.refs[0].source == httpd, proj2.refs[0].source)
- check("colliding labels are disambiguated",
- [r.label for r in proj2.refs] == ["httpd", "httpd_2", "auth"],
- f"{[r.label for r in proj2.refs]}")
+ check(
+ "relative paths resolve against the project file",
+ proj2.refs[0].source == httpd,
+ proj2.refs[0].source,
+ )
+ check(
+ "colliding labels are disambiguated",
+ [r.label for r in proj2.refs] == ["httpd", "httpd_2", "auth"],
+ f"{[r.label for r in proj2.refs]}",
+ )
check("explicit labels are honoured", proj2.by_label("auth") is not None)
proj2.stage_all()
- check("stage_all() stages every binary to a distinct file",
- len({r.staged for r in proj2.refs}) == 3
- and all(os.path.isfile(r.staged) for r in proj2.refs))
+ check(
+ "stage_all() stages every binary to a distinct file",
+ len({r.staged for r in proj2.refs}) == 3
+ and all(os.path.isfile(r.staged) for r in proj2.refs),
+ )
# -- add / remove ---------------------------------------------------- #
extra = _bin(os.path.join(src, "extra"))
@@ -132,39 +161,60 @@ def main() -> int:
# -- 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]}")
+ 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]}")
+ 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]}")
+ 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)
+ 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)
+ check(
+ "remove() drops one",
+ proj2.remove("extra") and proj2.by_label("extra") is None,
+ )
# -- bad input ------------------------------------------------------- #
bad = os.path.join(tmp, "bad.json")
@@ -191,43 +241,62 @@ def main() -> int:
# -- load options for headerless blobs --------------------------------- #
with tempfile.TemporaryDirectory() as tmp:
- src = os.path.join(tmp, "src"); os.makedirs(src)
+ src = os.path.join(tmp, "src")
+ os.makedirs(src)
blob = os.path.join(src, "fw.bin")
with open(blob, "wb") as f:
f.write(b"\x00" * 64)
- proj = Project.create(os.path.join(tmp, "p.json"), [blob], name="p",
- load={"processor": "arm", "base": 0x8000000})
+ proj = Project.create(
+ os.path.join(tmp, "p.json"),
+ [blob],
+ name="p",
+ load={"processor": "arm", "base": 0x8000000},
+ )
r = proj.refs[0]
- check("create() records load options per binary",
- r.processor == "arm" and r.base == 0x8000000,
- f"proc={r.processor!r} base={r.base:#x}")
+ check(
+ "create() records load options per binary",
+ r.processor == "arm" and r.base == 0x8000000,
+ f"proc={r.processor!r} base={r.base:#x}",
+ )
# -b is PARAGRAPHS: 0x8000000 >> 4 == 0x800000. Getting this wrong loads
# the image 16x too high and every address in the database is wrong.
- check("base is converted to IDA's paragraph units",
- r.load_args == "-parm -b800000", r.load_args)
+ check(
+ "base is converted to IDA's paragraph units",
+ r.load_args == "-parm -b800000",
+ r.load_args,
+ )
proj2 = Project.load(proj.path)
- check("load options survive a round-trip through the file",
- proj2.refs[0].load_args == "-parm -b800000",
- proj2.refs[0].load_args)
+ check(
+ "load options survive a round-trip through the file",
+ proj2.refs[0].load_args == "-parm -b800000",
+ proj2.refs[0].load_args,
+ )
blob2 = os.path.join(src, "other.bin")
with open(blob2, "wb") as f:
f.write(b"\x00" * 64)
r2 = proj2.add(blob2, load={"processor": "mipsb"})
- check("add() takes load options too",
- r2.load_args == "-pmipsb", r2.load_args)
+ check("add() takes load options too", r2.load_args == "-pmipsb", r2.load_args)
# a normal ELF needs none of this and must pass nothing
- check("a binary with no load options passes no switches",
- Project.create(os.path.join(tmp, "q.json"), [blob],
- name="q").refs[0].load_args == "")
+ check(
+ "a binary with no load options passes no switches",
+ Project.create(os.path.join(tmp, "q.json"), [blob], name="q")
+ .refs[0]
+ .load_args
+ == "",
+ )
# addresses get written by hand, so accept how people write them
- proj3 = Project.create(os.path.join(tmp, "r.json"), [blob], name="r",
- load={"base": "0x1000"})
- check("a base given as a hex STRING is parsed",
- proj3.refs[0].base == 0x1000, f"{proj3.refs[0].base}")
+ proj3 = Project.create(
+ os.path.join(tmp, "r.json"), [blob], name="r", load={"base": "0x1000"}
+ )
+ check(
+ "a base given as a hex STRING is parsed",
+ proj3.refs[0].base == 0x1000,
+ f"{proj3.refs[0].base}",
+ )
print(f"\n{PASS} passed, {FAIL} failed")
return 1 if FAIL else 0