summaryrefslogtreecommitdiffstats
path: root/tools/qrcheck.py
blob: 305594c044355afc809c817e1ecd9b60d2b2c9e5 (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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
#!/usr/bin/env python3
"""Cross-check the kernel QR encoder (tools/qrcheck) against segno.

Strategy: segno's own bitstream differs from the kernel's in the pad bits
(segno emits a full extra zero byte when the stream is already byte-aligned),
so instead of comparing against `segno.make()` directly we feed the kernel's
own interleaved codeword stream through segno's placement primitives
(make_matrix / add_finder_patterns / add_alignment_patterns / add_codewords /
apply_mask / add_format_info / add_version_info) and compare matrices
bit-for-bit.  For auto-mask cases the chosen mask is validated against
segno's `mask_scores` penalty evaluation on the same (kernel-stream) matrix.
"""
import importlib
import subprocess
import sys

sys.path.insert(0, "/tmp/segno_pkg")
segno = importlib.import_module("segno")  # vendored reference at /tmp/segno_pkg
E = importlib.import_module("segno.encoder")
C = importlib.import_module("segno.consts")

BIN = "tools/qrcheck/target/debug/qrcheck"
ROOT = "/home/salmon/Documents/rust/rsdos"


def run(args):
    out = subprocess.run(args, capture_output=True, text=True, cwd=ROOT)
    if out.returncode != 0:
        print("RUST FAIL:", out.stderr.strip())
        sys.exit(1)
    return out.stdout


def rust_matrix(text, version, mask):
    args = [BIN, text]
    if version:
        args.append(str(version))
    if mask is not None:
        args.append(str(mask))
    lines = run(args).splitlines()
    return lines[0], lines[1:]


def rust_raw(text, version):
    """Kernel interleaved codeword stream (data+EC) as bytes."""
    hexs = run([BIN, "--raw", text, str(version)]).strip()
    return bytes.fromhex(hexs)


def _to_bits(my_cw):
    try:
        return [int(b) for byte in my_cw for b in f"{byte:08b}"]
    except (ValueError, TypeError) as exc:
        print(f"bad codeword stream {my_cw!r}: {exc}")
        sys.exit(1)


def build_matrix(version, my_cw, mask, with_info):
    """Expected matrix from the kernel's codeword stream via segno primitives.

    `with_info=True` also writes format + version info (final symbol);
    `with_info=False` leaves them light (as during mask evaluation).
    """
    n = E.calc_matrix_size(version)
    bits = _to_bits(my_cw)
    m = E.make_matrix(n, n)
    E.add_finder_patterns(m, n, n)
    E.add_alignment_patterns(m, n, n)
    E.add_codewords(m, bits, version)
    fm = E.make_matrix(n, n)
    E.add_finder_patterns(fm, n, n)
    E.add_alignment_patterns(fm, n, n)
    fm[-8][8] = 0x1
    E.apply_mask(m, E.get_data_mask_functions(False)[mask], n, n,
                 lambda i, j: fm[i][j] > 0x1)
    if with_info:
        E.add_format_info(m, version, C.ERROR_LEVEL_L, mask)
        E.add_version_info(m, version)
    # Normalize leftover (remainder) cells: 0x2/0x3 -> 0/1.
    return [[v & 0x1 if v in (2, 3) else v for v in row] for row in m]


def render(rows):
    return ["".join("#" if c else "." for c in row) for row in rows]


def compare_forced(text, version, mask):
    hdr, rows = rust_matrix(text, version, mask)
    my_cw = rust_raw(text, version)
    exp = build_matrix(version, my_cw, mask, with_info=True)
    srows = render(exp)
    tag = f"v{version} m{mask} {text[:28]!r}"
    if len(rows) != len(srows):
        print(f"FAIL {tag}: rust {len(rows)}x vs expected {len(srows)}x")
        return False
    for y, (r, s) in enumerate(zip(rows, srows)):
        if r != s:
            print(f"FAIL {tag}: row {y} differs (rust v{mask})")
            for dy in range(max(0, y - 2), min(len(rows), y + 3)):
                print(f"   rust row{dy}: {rows[dy]}")
                print(f"   exp  row{dy}: {srows[dy]}")
            return False
    print(f"OK   {tag} ({len(rows)}x{len(rows)}, {hdr})")
    return True


def segno_best_mask(version, my_cw):
    """Mask segno's penalty scoring would choose for the kernel stream."""
    n = E.calc_matrix_size(version)
    bits = _to_bits(my_cw)
    scores = []
    for mask in range(8):
        m = E.make_matrix(n, n)
        E.add_finder_patterns(m, n, n)
        E.add_alignment_patterns(m, n, n)
        E.add_codewords(m, bits, version)
        fm = E.make_matrix(n, n)
        E.add_finder_patterns(fm, n, n)
        E.add_alignment_patterns(fm, n, n)
        fm[-8][8] = 0x1
        E.apply_mask(m, E.get_data_mask_functions(False)[mask], n, n,
                     lambda i, j: fm[i][j] > 0x1)
        scores.append((sum(E.mask_scores(m, n, n)), mask))
    return min(scores)  # (score, mask); first-lowest wins like segno


def _parse_hdr(hdr):
    try:
        v = int(hdr.split()[0][1:])
        m = int(hdr.split()[1][1:])
    except (ValueError, IndexError) as exc:
        print(f"bad header {hdr!r}: {exc}")
        sys.exit(1)
    return v, m


def compare_auto(text, version=None):
    args = [BIN, text]
    if version:
        args.append(str(version))
    lines = run(args).splitlines()
    hdr = lines[0]  # "v<N> m<M>"
    rows = lines[1:]
    v, m = _parse_hdr(hdr)
    my_cw = rust_raw(text, v)
    score, s_mask = segno_best_mask(v, my_cw)
    tag = f"v{v} auto-mask {text[:28]!r}"
    if m != s_mask:
        print(f"FAIL {tag}: rust chose m{m}, segno penalty chose m{s_mask} "
              f"(score {score})")
        return False
    # Full final-matrix check with the chosen mask.
    exp = build_matrix(v, my_cw, m, with_info=True)
    srows = render(exp)
    if len(rows) != len(srows):
        print(f"FAIL {tag}: size mismatch")
        return False
    for y, (r, s) in enumerate(zip(rows, srows)):
        if r != s:
            print(f"FAIL {tag}: row {y} differs")
            for dy in range(max(0, y - 2), min(len(rows), y + 3)):
                print(f"   rust row{dy}: {rows[dy]}")
                print(f"   exp  row{dy}: {srows[dy]}")
            return False
    print(f"OK   {tag} (m{m} matches segno, {len(rows)}x{len(rows)})")
    return True


def main():
    ok = True
    texts = {
        1: "A",
        2: "hello",
        3: "the quick brown fox",
        4: "0123456789ABCDEF",
        5: "byte mode QR encoder cross-check",
        6: "x" * 100,
        7: "hello world, this is a slightly longer message for v7",
        10: "x" * 250,
        14: "x" * 400,
        20: "x" * 800,
        25: "x" * 1200,
        40: "x" * 2900,
    }
    for v in (1, 2, 3, 4, 5, 6, 7, 10, 14, 20, 25, 40):
        for m in range(8):
            ok &= compare_forced(texts[v], v, m)
    crash = (
        "!!! heap corruption in dealloc\n"
        "ptr=0x404443e0 block=0x1980000 heap=0x1980000 rsp=0x1fffe0\n"
        "mem: x=0x41414141 x=0x41414141 x=0x41414141 x=0x41414141\n"
        "mem: x=0x41414141 x=0x41414141 x=0x41414141 x=0x41414141\n"
        "stk: x=0x0 x=0x0 x=0x0 x=0x0 x=0x0 x=0x0 x=0x0 x=0x0\n"
        "hdr: b=0x1980000 s=0x20 f=0 b16=0x1980000 s16=0x20 f16=0\n"
        "RSDOS fault: #GP at rip=0x12345678 cr2=0x0 task=main\n"
    )
    ok &= compare_auto(crash)
    ok &= compare_auto("hello world", 1)
    ok &= compare_auto("byte mode QR encoder cross-check", 5)
    ok &= compare_auto("hello world, this is a slightly longer message for v7", 7)
    ok &= compare_auto("x" * 400, 14)
    ok &= compare_auto("x" * 2900, 40)
    print("ALL OK" if ok else "FAILURES")
    sys.exit(0 if ok else 1)


if __name__ == "__main__":
    main()