aboutsummaryrefslogtreecommitdiffstats
path: root/tests/test_search.py
blob: 6fd1a25beb0b823212e4f0c2c98887dcb4a1276e (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
#!/usr/bin/env python3
"""Query classification for Ctrl+F: is that text, or is it bytes?

The whole risk of a mode-guessing search is guessing wrong in the direction
that loses work: deciding a word like `dead` or `add` is a byte pattern, and
silently searching the image instead of the disassembly. These checks pin that
asymmetry down. Pure -- no IDA, no worker.
"""

#: pure: stdlib only.
#: Read by tests/run.py (--fast skips every NEEDS_IDA file).
NEEDS_IDA = False
import os
import sys

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

from idatui.search import (  # noqa: E402
    BYTES, TEXT, classify, looks_like_bytes, normalise_pattern,
    pattern_problem, probably_meant_bytes,
)

PASS = FAIL = 0


def check(name, cond, detail=""):
    global PASS, FAIL
    if cond:
        PASS += 1
        print(f"  ok   {name}")
    else:
        FAIL += 1
        print(f"  FAIL {name}   {detail}")


def main() -> int:
    # -- the asymmetry: hex-looking WORDS must stay text --------------------- #
    for word in ("add", "dead", "beef", "cafe", "ff", "0", "abcdef",
                 "decode", "face"):
        check(f"{word!r} searches text, not bytes",
              classify(word)[0] == TEXT, classify(word))

    # -- unambiguous byte patterns ------------------------------------------ #
    for pat in ("48 8b ?? c3", "B8 ? ? ? ? 90", "48,8b,05", "de ad be ef",
                "48 8? ?? 24", "??"):
        check(f"{pat!r} searches bytes", classify(pat)[0] == BYTES,
              classify(pat))

    check("a quoted literal is a byte pattern",
          classify('"Hello", 0')[0] == BYTES)

    # A TYPO in a byte pattern must stay a byte pattern, so it can be refused
    # with a reason. Falling back to text answers "no match", which is
    # indistinguishable from "those bytes are not in this binary".
    check("a typo'd byte pattern is still a byte pattern",
          classify("48 zz c3")[0] == BYTES, classify("48 zz c3"))
    check("and it is refused by name",
          "'zz'" in (pattern_problem("48 zz c3") or ""))
    check("but a word among bytes is prose",
          classify("add ff")[0] == TEXT and classify("mov rdi, rax")[0] == TEXT,
          classify("add ff"))
    check("prose stays text", classify("mov rdi, rax")[0] == TEXT)
    check("a call target stays text", classify("call cs:__isoc99_scanf")[0] == TEXT)
    check("an empty query is text (nothing to search yet)",
          classify("")[0] == TEXT and not looks_like_bytes(""))

    # -- explicit wins over any guess --------------------------------------- #
    check("hex: forces bytes", classify("hex: dead") == (BYTES, "dead"))
    check("bytes: forces bytes too", classify("bytes:dead") == (BYTES, "dead"))
    check("text: forces text", classify("text: 48 8b c3") == (TEXT, "48 8b c3"))
    check("F2's forced mode beats the shape",
          classify("dead", forced=BYTES) == (BYTES, "dead")
          and classify("48 8b c3", forced=TEXT) == (TEXT, "48 8b c3"))
    check("a prefix beats even the forced mode",
          classify("text:48 8b c3", forced=BYTES)[0] == TEXT)

    # -- the shapes people paste -------------------------------------------- #
    check("commas become spaces", normalise_pattern("48,8b,05") == "48 8b 05")
    check("a run with no separators is split into bytes",
          normalise_pattern("488B05C3") == "48 8B 05 C3")
    check("whitespace is squeezed", normalise_pattern(" 48   8b\t05 ") == "48 8b 05")
    check("a quoted literal keeps its own spacing",
          normalise_pattern('"Hello, world", 0') == '"Hello, world", 0')

    # -- refusing a bad pattern with a reason -------------------------------- #
    check("an empty pattern says what to type",
          "48 8b" in (pattern_problem("") or ""))
    check("a non-hex token is named",
          "'zz'" in (pattern_problem("48 zz c3") or ""), pattern_problem("48 zz c3"))
    check("a good pattern has no complaint",
          pattern_problem("48 8b ?? c3") is None
          and pattern_problem('"Hi", 0') is None)
    check("an odd-length run is refused rather than silently split",
          pattern_problem("488B0") is not None, pattern_problem("488B0"))

    print(f"\n{PASS} passed, {FAIL} failed")
    return 1 if FAIL else 0


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