diff options
Diffstat (limited to 'tests/test_formats.py')
| -rw-r--r-- | tests/test_formats.py | 122 |
1 files changed, 122 insertions, 0 deletions
diff --git a/tests/test_formats.py b/tests/test_formats.py new file mode 100644 index 0000000..ab72e8d --- /dev/null +++ b/tests/test_formats.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +"""Format sniffing + load-switch construction (pure stdlib, no IDA).""" +import os +import sys +import tempfile + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from idatui.formats import (PROCESSORS, load_args, # noqa: E402 + needs_load_options, sniff) + +PASS = FAIL = 0 + + +def check(name, ok, detail=""): + global PASS, FAIL + if ok: + PASS += 1 + print(f" ok {name}") + else: + FAIL += 1 + print(f" FAIL {name} {detail}") + + +def main() -> int: + with tempfile.TemporaryDirectory() as tmp: + def w(name, data): + p = os.path.join(tmp, name) + with open(p, "wb") as f: + f.write(data) + return p + + # -- formats IDA can load on its own: never interrupt the user -------- # + for name, head in (("elf", b"\x7fELF\x02\x01\x01"), ("pe", b"MZ\x90\x00"), + ("macho", b"\xcf\xfa\xed\xfe"), ("dex", b"dex\n035\x00"), + ("wasm", b"\x00asm\x01\x00")): + p = w(name, head + bytes(60)) + check(f"{name} is recognised (no dialog)", + sniff(p) is not None and not needs_load_options(p), f"{sniff(p)}") + + # -- the case this exists for ----------------------------------------- # + blob = w("fw.bin", bytes(range(256)) * 4) + check("a headerless blob is not recognised (ask)", + sniff(blob) is None and needs_load_options(blob)) + + # Intel HEX / S-records are text containers IDA does load. + hexf = w("f.hex", b":10010000214601360121470136007EFE09D2190140\n") + check("Intel HEX is recognised", sniff(hexf) == "Intel HEX", f"{sniff(hexf)}") + srec = w("f.s19", b"S00600004844521B\nS1130000285F245F2212226A000424290008237C\n") + check("Motorola S-records are recognised", + sniff(srec) == "Motorola S-record", f"{sniff(srec)}") + + # A binary that merely STARTS with ':' is not Intel HEX. Text formats are + # only accepted when the whole head is printable, or half the firmware in + # the world gets mis-detected on one byte. + colon = w("colon.bin", b":\x00\xff\xfe\x01\x02" + bytes(58)) + check("a blob starting with ':' is not mistaken for Intel HEX", + sniff(colon) is None, f"{sniff(colon)}") + + check("an empty file is not recognised", sniff(w("empty", b"")) is None) + check("a missing file never asks (nothing to load)", + not needs_load_options(os.path.join(tmp, "nope"))) + check("a directory never asks", not needs_load_options(tmp)) + + # -- switch construction ------------------------------------------------- # + # -b is in PARAGRAPHS: 0x8000000 >> 4 == 0x800000. Getting this wrong loads + # the image 16x off and every address in the database is wrong. + check("base is converted to paragraphs", + load_args("arm", 0x8000000) == "-parm -b800000", + load_args("arm", 0x8000000)) + check("processor alone", load_args("mipsb") == "-pmipsb", load_args("mipsb")) + check("base alone", load_args("", 0x10000) == "-b1000", load_args("", 0x10000)) + check("base 0 emits no switch (it's the default)", + load_args("arm", 0) == "-parm", load_args("arm", 0)) + check("nothing in, nothing out", load_args() == "") + check("extra switches pass through", + load_args("arm", 0, "-T binary") == "-parm -T binary") + + check("the processor list leads with the common targets", + [n for n, _ in PROCESSORS[:3]] == ["arm", "armb", "metapc"], + f"{[n for n, _ in PROCESSORS[:3]]}") + + # Every offered name must have been checked against a real IDA, because a + # wrong one is REJECTED (rc=4) with nothing useful said — handing the user a + # dead end from inside the dialog meant to rescue them. This list is the + # output of tools/verify_procs.py; adding a processor without re-running it + # fails here on purpose. + VERIFIED = { + "arm", "armb", "metapc", "mipsl", "mipsb", "ppc", "ppcl", "sh4", "68k", + "riscv", "tricore", "xtensa", "avr", "z80", "tms320c6", "m32r", "arc", + "h8300", "sparcb", "sparcl", "s390", + } + offered = {n for n, _ in PROCESSORS} + check("every offered processor name is IDA-verified", + offered <= VERIFIED, f"unverified: {sorted(offered - VERIFIED)}") + + # These are module FILENAMES or common aliases, not -p names. IDA refuses + # them; they were in the list until a real run said otherwise. + for wrong in ("h8", "sparc", "arm64", "aarch64", "mips", "m68k"): + check(f"{wrong!r} is not offered (IDA rejects it)", wrong not in offered) + + # ...but someone WILL type them, so the labels have to carry the alias. + def finds(q): + ql = q.lower() + return [n for n, d in PROCESSORS if ql in n.lower() or ql in d.lower()] + check("typing 'arm64' still finds ARM", "arm" in finds("arm64"), f"{finds('arm64')}") + check("typing 'aarch64' still finds ARM", "arm" in finds("aarch64")) + check("typing 'm68k' still finds 68k", "68k" in finds("m68k"), f"{finds('m68k')}") + check("typing 'mips' finds both endiannesses", + set(finds("mips")) == {"mipsl", "mipsb"}, f"{finds('mips')}") + check("every processor entry has a human label", + all(n and d for n, d in PROCESSORS)) + check("endianness is spelled out where it matters", + all(any(w in d.lower() for w in ("endian",)) + for n, d in PROCESSORS if n in ("arm", "armb", "mipsb", "mipsl"))) + + print(f"\n{PASS} passed, {FAIL} failed") + return 1 if FAIL else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) |
