summaryrefslogtreecommitdiffstats
path: root/tests/test_launch.py
blob: c6ed8f1d5885e523606f1e4ed678175c0e92cb7a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
#!/usr/bin/env python3
"""The launcher's file handling -- the part that deletes things.

`_sweep_locks` runs automatically when a database fails to open, and it removes
files next to the user's binary. That is exactly the kind of code that must not
be tested by trying it, so it is tested here: which files it takes, which it
must never take, and what it reports.

Pure: no IDA, no worker, no Textual.
"""
from __future__ import annotations

import os
import sys
import tempfile

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

#: pure: file handling only, nothing is opened.
#: Read by tests/run.py (--fast skips every NEEDS_IDA file).
NEEDS_IDA = False

from idatui.launch import _LOCK_SUFFIXES, _load_args, _sweep_locks  # noqa: E402

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 touch(*paths):
    for p in paths:
        with open(p, "wb") as fh:
            fh.write(b"x")


def t_sweeps_the_scratch_files():
    """IDA unpacks a .i64 into .id0/.id1/.id2/.nam/.til while it is open; a
    hard-killed worker leaves them and the .i64 then refuses to reopen."""
    with tempfile.TemporaryDirectory() as d:
        binary = os.path.join(d, "echo")
        touch(binary, *[binary + s for s in _LOCK_SUFFIXES])
        n = _sweep_locks(binary)
        check("every unpacked scratch file is swept", n == len(_LOCK_SUFFIXES),
              f"swept {n} of {len(_LOCK_SUFFIXES)}")
        check("none of them survive",
              not any(os.path.exists(binary + s) for s in _LOCK_SUFFIXES))
        check("the binary itself is untouched", os.path.exists(binary))


def t_sweeps_by_stem_too():
    """IDA keys the scratch on the full name or the stem depending on how the
    database was created, so both are swept."""
    with tempfile.TemporaryDirectory() as d:
        binary = os.path.join(d, "prog.elf")
        stem = os.path.join(d, "prog")
        touch(binary, stem + ".id0", stem + ".nam", binary + ".id1")
        n = _sweep_locks(binary)
        check("scratch named after the stem is swept too", n == 3, f"n={n}")
        check("stem-keyed files are gone",
              not os.path.exists(stem + ".id0")
              and not os.path.exists(stem + ".nam"))
        check("full-name-keyed files are gone", not os.path.exists(binary + ".id1"))
        check("the binary itself is untouched", os.path.exists(binary))


def t_never_the_database():
    """The .i64 IS the database. Nothing is saved unless idb_save was called, so
    deleting it throws away every rename and comment in the session."""
    with tempfile.TemporaryDirectory() as d:
        binary = os.path.join(d, "echo")
        db = binary + ".i64"
        stem_db = os.path.join(d, "echo.i64")
        touch(binary, db, binary + ".id0")
        _sweep_locks(binary)
        check("the .i64 is never swept", os.path.exists(db))
        check("nor the stem-keyed .i64", os.path.exists(stem_db))
        check(".i64 is not in the suffix list", ".i64" not in _LOCK_SUFFIXES,
              str(_LOCK_SUFFIXES))


def t_never_the_input_itself():
    """`.til` is both an unpacked-DB suffix and the extension of an IDA type
    library, so `ida-tui mylib.til` used to sweep its own argument out of
    existence -- irreversibly, on a path that runs automatically when an open
    fails. Same for anything named *.id0/*.id1/*.id2/*.nam.
    """
    for suf in _LOCK_SUFFIXES:
        with tempfile.TemporaryDirectory() as d:
            binary = os.path.join(d, "mylib" + suf)
            touch(binary)
            _sweep_locks(binary)
            check(f"a binary named *{suf} is not deleted by its own sweep",
                  os.path.exists(binary), f"{binary} was removed")


def t_relative_path_is_still_the_input():
    """The guard compares absolute paths -- a relative argument names the same
    file and must be protected the same way."""
    with tempfile.TemporaryDirectory() as d:
        cwd = os.getcwd()
        try:
            os.chdir(d)
            touch("mylib.til")
            _sweep_locks("mylib.til")
            check("a relative path to the input is protected too",
                  os.path.exists("mylib.til"))
        finally:
            os.chdir(cwd)


def t_missing_files_are_fine():
    with tempfile.TemporaryDirectory() as d:
        binary = os.path.join(d, "nothing-here")
        touch(binary)
        n = _sweep_locks(binary)
        check("sweeping with nothing to sweep reports 0", n == 0, f"n={n}")
        check("and does not raise", True)


def t_leaves_the_neighbours_alone():
    with tempfile.TemporaryDirectory() as d:
        binary = os.path.join(d, "echo")
        other = os.path.join(d, "other.id0")       # another binary's scratch
        src = os.path.join(d, "echo.c")
        touch(binary, other, src, binary + ".id0")
        _sweep_locks(binary)
        check("another binary's scratch is left alone", os.path.exists(other))
        check("unrelated neighbours are left alone", os.path.exists(src))
        check("our own scratch is still swept", not os.path.exists(binary + ".id0"))


def t_load_args():
    """The single-binary path turns the load options into IDA switches."""
    check("no options means no switches", _load_args({}) == "", repr(_load_args({})))
    a = _load_args({"processor": "arm", "base": 0x8000})
    check("a processor reaches the switches", "-parm" in a, a)
    # -b is in PARAGRAPHS, not bytes: 0x8000 >> 4 == 0x800.
    check("a base is converted to paragraphs", "-b800" in a, a)
    b = _load_args({"base": "0x1000"})
    check("a base given as a hex STRING is accepted (project files write those)",
          "-b100" in b, b)
    check("no base means no -b switch", "-b" not in _load_args({"processor": "arm"}),
          _load_args({"processor": "arm"}))
    c = _load_args({"ida_args": "-p1"})
    check("extra ida_args are passed through", "-p1" in c, c)


def main() -> int:
    for fn in (t_sweeps_the_scratch_files, t_sweeps_by_stem_too,
               t_never_the_database, t_never_the_input_itself,
               t_relative_path_is_still_the_input, t_missing_files_are_fine,
               t_leaves_the_neighbours_alone, t_load_args):
        print(f"\n{fn.__name__}")
        try:
            fn()
        except Exception as e:  # noqa: BLE001
            import traceback
            check(f"{fn.__name__} did not crash", False, f"{type(e).__name__}: {e}")
            traceback.print_exc()
    print(f"\n{PASS} passed, {FAIL} failed")
    return 1 if FAIL else 0


if __name__ == "__main__":
    raise SystemExit(main())