diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-03 02:23:35 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-03 02:23:35 +0200 |
| commit | 9fde94d068f87b34e0bc6bfa48388d89bb54f1e4 (patch) | |
| tree | a5a4ce95363e7a316a23371489f2b0b85c1a8735 /tools/qrcheck.py | |
| parent | v0.5.7: fix vt verify content check — expect BLANK (0x70020), not zero (diff) | |
| download | rs-dos-9fde94d068f87b34e0bc6bfa48388d89bb54f1e4.tar.gz rs-dos-9fde94d068f87b34e0bc6bfa48388d89bb54f1e4.zip | |
v0.5.8: heap-free QR encoder + crash-dump QR on framebuffer
Real hardware has no serial console, so a kernel crash only leaves a
frozen screen. Every dump_lines() report now also renders the crash
text as a scannable QR below the white-on-red band (photograph the
screen instead of relaying text).
- rsnix/src/qr.rs: from-scratch no_std/no_alloc byte-mode QR encoder
(EC-L, v1..v40, .bss statics only — safe on the fault path):
RS over GF(256), codeword interleave, zigzag placement, 8 masks with
N1-N4 penalty auto-selection, format/version info.
- gop.rs: dump_qr() joins dump lines into a static buffer, truncates
with '...' at the v40-L capacity, encodes, and renders centered
black-on-white with a 4-module quiet zone.
- tools/qrcheck: host harness (embedds qr.rs) + qrcheck.py cross-check
against segno's placement primitives. Verified bit-exact: 12
versions x 8 masks forced, plus auto-mask choice matches segno's
penalty evaluation. Caught 3 encoder bugs (multi-block interleave
offsets, transposed version info, mask eval ordering).
- QEMU e2e: page-fault dump screendump decodes via zbarimg, content
matches the serial log line-for-line.
- test-msg.sh and test-fault.sh both PASS; debug+release 0 warnings.
Diffstat (limited to 'tools/qrcheck.py')
| -rw-r--r-- | tools/qrcheck.py | 208 |
1 files changed, 208 insertions, 0 deletions
diff --git a/tools/qrcheck.py b/tools/qrcheck.py new file mode 100644 index 0000000..305594c --- /dev/null +++ b/tools/qrcheck.py | |||
| @@ -0,0 +1,208 @@ | |||
| 1 | #!/usr/bin/env python3 | ||
| 2 | """Cross-check the kernel QR encoder (tools/qrcheck) against segno. | ||
| 3 | |||
| 4 | Strategy: segno's own bitstream differs from the kernel's in the pad bits | ||
| 5 | (segno emits a full extra zero byte when the stream is already byte-aligned), | ||
| 6 | so instead of comparing against `segno.make()` directly we feed the kernel's | ||
| 7 | own interleaved codeword stream through segno's placement primitives | ||
| 8 | (make_matrix / add_finder_patterns / add_alignment_patterns / add_codewords / | ||
| 9 | apply_mask / add_format_info / add_version_info) and compare matrices | ||
| 10 | bit-for-bit. For auto-mask cases the chosen mask is validated against | ||
| 11 | segno's `mask_scores` penalty evaluation on the same (kernel-stream) matrix. | ||
| 12 | """ | ||
| 13 | import importlib | ||
| 14 | import subprocess | ||
| 15 | import sys | ||
| 16 | |||
| 17 | sys.path.insert(0, "/tmp/segno_pkg") | ||
| 18 | segno = importlib.import_module("segno") # vendored reference at /tmp/segno_pkg | ||
| 19 | E = importlib.import_module("segno.encoder") | ||
| 20 | C = importlib.import_module("segno.consts") | ||
| 21 | |||
| 22 | BIN = "tools/qrcheck/target/debug/qrcheck" | ||
| 23 | ROOT = "/home/salmon/Documents/rust/rsdos" | ||
| 24 | |||
| 25 | |||
| 26 | def run(args): | ||
| 27 | out = subprocess.run(args, capture_output=True, text=True, cwd=ROOT) | ||
| 28 | if out.returncode != 0: | ||
| 29 | print("RUST FAIL:", out.stderr.strip()) | ||
| 30 | sys.exit(1) | ||
| 31 | return out.stdout | ||
| 32 | |||
| 33 | |||
| 34 | def rust_matrix(text, version, mask): | ||
| 35 | args = [BIN, text] | ||
| 36 | if version: | ||
| 37 | args.append(str(version)) | ||
| 38 | if mask is not None: | ||
| 39 | args.append(str(mask)) | ||
| 40 | lines = run(args).splitlines() | ||
| 41 | return lines[0], lines[1:] | ||
| 42 | |||
| 43 | |||
| 44 | def rust_raw(text, version): | ||
| 45 | """Kernel interleaved codeword stream (data+EC) as bytes.""" | ||
| 46 | hexs = run([BIN, "--raw", text, str(version)]).strip() | ||
| 47 | return bytes.fromhex(hexs) | ||
| 48 | |||
| 49 | |||
| 50 | def _to_bits(my_cw): | ||
| 51 | try: | ||
| 52 | return [int(b) for byte in my_cw for b in f"{byte:08b}"] | ||
| 53 | except (ValueError, TypeError) as exc: | ||
| 54 | print(f"bad codeword stream {my_cw!r}: {exc}") | ||
| 55 | sys.exit(1) | ||
| 56 | |||
| 57 | |||
| 58 | def build_matrix(version, my_cw, mask, with_info): | ||
| 59 | """Expected matrix from the kernel's codeword stream via segno primitives. | ||
| 60 | |||
| 61 | `with_info=True` also writes format + version info (final symbol); | ||
| 62 | `with_info=False` leaves them light (as during mask evaluation). | ||
| 63 | """ | ||
| 64 | n = E.calc_matrix_size(version) | ||
| 65 | bits = _to_bits(my_cw) | ||
| 66 | m = E.make_matrix(n, n) | ||
| 67 | E.add_finder_patterns(m, n, n) | ||
| 68 | E.add_alignment_patterns(m, n, n) | ||
| 69 | E.add_codewords(m, bits, version) | ||
| 70 | fm = E.make_matrix(n, n) | ||
| 71 | E.add_finder_patterns(fm, n, n) | ||
| 72 | E.add_alignment_patterns(fm, n, n) | ||
| 73 | fm[-8][8] = 0x1 | ||
| 74 | E.apply_mask(m, E.get_data_mask_functions(False)[mask], n, n, | ||
| 75 | lambda i, j: fm[i][j] > 0x1) | ||
| 76 | if with_info: | ||
| 77 | E.add_format_info(m, version, C.ERROR_LEVEL_L, mask) | ||
| 78 | E.add_version_info(m, version) | ||
| 79 | # Normalize leftover (remainder) cells: 0x2/0x3 -> 0/1. | ||
| 80 | return [[v & 0x1 if v in (2, 3) else v for v in row] for row in m] | ||
| 81 | |||
| 82 | |||
| 83 | def render(rows): | ||
| 84 | return ["".join("#" if c else "." for c in row) for row in rows] | ||
| 85 | |||
| 86 | |||
| 87 | def compare_forced(text, version, mask): | ||
| 88 | hdr, rows = rust_matrix(text, version, mask) | ||
| 89 | my_cw = rust_raw(text, version) | ||
| 90 | exp = build_matrix(version, my_cw, mask, with_info=True) | ||
| 91 | srows = render(exp) | ||
| 92 | tag = f"v{version} m{mask} {text[:28]!r}" | ||
| 93 | if len(rows) != len(srows): | ||
| 94 | print(f"FAIL {tag}: rust {len(rows)}x vs expected {len(srows)}x") | ||
| 95 | return False | ||
| 96 | for y, (r, s) in enumerate(zip(rows, srows)): | ||
| 97 | if r != s: | ||
| 98 | print(f"FAIL {tag}: row {y} differs (rust v{mask})") | ||
| 99 | for dy in range(max(0, y - 2), min(len(rows), y + 3)): | ||
| 100 | print(f" rust row{dy}: {rows[dy]}") | ||
| 101 | print(f" exp row{dy}: {srows[dy]}") | ||
| 102 | return False | ||
| 103 | print(f"OK {tag} ({len(rows)}x{len(rows)}, {hdr})") | ||
| 104 | return True | ||
| 105 | |||
| 106 | |||
| 107 | def segno_best_mask(version, my_cw): | ||
| 108 | """Mask segno's penalty scoring would choose for the kernel stream.""" | ||
| 109 | n = E.calc_matrix_size(version) | ||
| 110 | bits = _to_bits(my_cw) | ||
| 111 | scores = [] | ||
| 112 | for mask in range(8): | ||
| 113 | m = E.make_matrix(n, n) | ||
| 114 | E.add_finder_patterns(m, n, n) | ||
| 115 | E.add_alignment_patterns(m, n, n) | ||
| 116 | E.add_codewords(m, bits, version) | ||
| 117 | fm = E.make_matrix(n, n) | ||
| 118 | E.add_finder_patterns(fm, n, n) | ||
| 119 | E.add_alignment_patterns(fm, n, n) | ||
| 120 | fm[-8][8] = 0x1 | ||
| 121 | E.apply_mask(m, E.get_data_mask_functions(False)[mask], n, n, | ||
| 122 | lambda i, j: fm[i][j] > 0x1) | ||
| 123 | scores.append((sum(E.mask_scores(m, n, n)), mask)) | ||
| 124 | return min(scores) # (score, mask); first-lowest wins like segno | ||
| 125 | |||
| 126 | |||
| 127 | def _parse_hdr(hdr): | ||
| 128 | try: | ||
| 129 | v = int(hdr.split()[0][1:]) | ||
| 130 | m = int(hdr.split()[1][1:]) | ||
| 131 | except (ValueError, IndexError) as exc: | ||
| 132 | print(f"bad header {hdr!r}: {exc}") | ||
| 133 | sys.exit(1) | ||
| 134 | return v, m | ||
| 135 | |||
| 136 | |||
| 137 | def compare_auto(text, version=None): | ||
| 138 | args = [BIN, text] | ||
| 139 | if version: | ||
| 140 | args.append(str(version)) | ||
| 141 | lines = run(args).splitlines() | ||
| 142 | hdr = lines[0] # "v<N> m<M>" | ||
| 143 | rows = lines[1:] | ||
| 144 | v, m = _parse_hdr(hdr) | ||
| 145 | my_cw = rust_raw(text, v) | ||
| 146 | score, s_mask = segno_best_mask(v, my_cw) | ||
| 147 | tag = f"v{v} auto-mask {text[:28]!r}" | ||
| 148 | if m != s_mask: | ||
| 149 | print(f"FAIL {tag}: rust chose m{m}, segno penalty chose m{s_mask} " | ||
| 150 | f"(score {score})") | ||
| 151 | return False | ||
| 152 | # Full final-matrix check with the chosen mask. | ||
| 153 | exp = build_matrix(v, my_cw, m, with_info=True) | ||
| 154 | srows = render(exp) | ||
| 155 | if len(rows) != len(srows): | ||
| 156 | print(f"FAIL {tag}: size mismatch") | ||
| 157 | return False | ||
| 158 | for y, (r, s) in enumerate(zip(rows, srows)): | ||
| 159 | if r != s: | ||
| 160 | print(f"FAIL {tag}: row {y} differs") | ||
| 161 | for dy in range(max(0, y - 2), min(len(rows), y + 3)): | ||
| 162 | print(f" rust row{dy}: {rows[dy]}") | ||
| 163 | print(f" exp row{dy}: {srows[dy]}") | ||
| 164 | return False | ||
| 165 | print(f"OK {tag} (m{m} matches segno, {len(rows)}x{len(rows)})") | ||
| 166 | return True | ||
| 167 | |||
| 168 | |||
| 169 | def main(): | ||
| 170 | ok = True | ||
| 171 | texts = { | ||
| 172 | 1: "A", | ||
| 173 | 2: "hello", | ||
| 174 | 3: "the quick brown fox", | ||
| 175 | 4: "0123456789ABCDEF", | ||
| 176 | 5: "byte mode QR encoder cross-check", | ||
| 177 | 6: "x" * 100, | ||
| 178 | 7: "hello world, this is a slightly longer message for v7", | ||
| 179 | 10: "x" * 250, | ||
| 180 | 14: "x" * 400, | ||
| 181 | 20: "x" * 800, | ||
| 182 | 25: "x" * 1200, | ||
| 183 | 40: "x" * 2900, | ||
| 184 | } | ||
| 185 | for v in (1, 2, 3, 4, 5, 6, 7, 10, 14, 20, 25, 40): | ||
| 186 | for m in range(8): | ||
| 187 | ok &= compare_forced(texts[v], v, m) | ||
| 188 | crash = ( | ||
| 189 | "!!! heap corruption in dealloc\n" | ||
| 190 | "ptr=0x404443e0 block=0x1980000 heap=0x1980000 rsp=0x1fffe0\n" | ||
| 191 | "mem: x=0x41414141 x=0x41414141 x=0x41414141 x=0x41414141\n" | ||
| 192 | "mem: x=0x41414141 x=0x41414141 x=0x41414141 x=0x41414141\n" | ||
| 193 | "stk: x=0x0 x=0x0 x=0x0 x=0x0 x=0x0 x=0x0 x=0x0 x=0x0\n" | ||
| 194 | "hdr: b=0x1980000 s=0x20 f=0 b16=0x1980000 s16=0x20 f16=0\n" | ||
| 195 | "RSDOS fault: #GP at rip=0x12345678 cr2=0x0 task=main\n" | ||
| 196 | ) | ||
| 197 | ok &= compare_auto(crash) | ||
| 198 | ok &= compare_auto("hello world", 1) | ||
| 199 | ok &= compare_auto("byte mode QR encoder cross-check", 5) | ||
| 200 | ok &= compare_auto("hello world, this is a slightly longer message for v7", 7) | ||
| 201 | ok &= compare_auto("x" * 400, 14) | ||
| 202 | ok &= compare_auto("x" * 2900, 40) | ||
| 203 | print("ALL OK" if ok else "FAILURES") | ||
| 204 | sys.exit(0 if ok else 1) | ||
| 205 | |||
| 206 | |||
| 207 | if __name__ == "__main__": | ||
| 208 | main() | ||
