summaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-08-03 02:23:35 +0200
committersalmonik <salmonik@tuta.io>2026-08-03 02:23:35 +0200
commit9fde94d068f87b34e0bc6bfa48388d89bb54f1e4 (patch)
treea5a4ce95363e7a316a23371489f2b0b85c1a8735 /tools
parentv0.5.7: fix vt verify content check — expect BLANK (0x70020), not zero (diff)
downloadrs-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 '')
-rw-r--r--tools/qrcheck.py208
-rw-r--r--tools/qrcheck/.gitignore1
-rw-r--r--tools/qrcheck/Cargo.lock7
-rw-r--r--tools/qrcheck/Cargo.toml13
-rw-r--r--tools/qrcheck/src/main.rs58
5 files changed, 287 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
4Strategy: 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),
6so instead of comparing against `segno.make()` directly we feed the kernel's
7own interleaved codeword stream through segno's placement primitives
8(make_matrix / add_finder_patterns / add_alignment_patterns / add_codewords /
9apply_mask / add_format_info / add_version_info) and compare matrices
10bit-for-bit. For auto-mask cases the chosen mask is validated against
11segno's `mask_scores` penalty evaluation on the same (kernel-stream) matrix.
12"""
13import importlib
14import subprocess
15import sys
16
17sys.path.insert(0, "/tmp/segno_pkg")
18segno = importlib.import_module("segno") # vendored reference at /tmp/segno_pkg
19E = importlib.import_module("segno.encoder")
20C = importlib.import_module("segno.consts")
21
22BIN = "tools/qrcheck/target/debug/qrcheck"
23ROOT = "/home/salmon/Documents/rust/rsdos"
24
25
26def 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
34def 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
44def 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
50def _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
58def 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
83def render(rows):
84 return ["".join("#" if c else "." for c in row) for row in rows]
85
86
87def 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
107def 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
127def _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
137def 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
169def 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
207if __name__ == "__main__":
208 main()
diff --git a/tools/qrcheck/.gitignore b/tools/qrcheck/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/tools/qrcheck/.gitignore
@@ -0,0 +1 @@
/target
diff --git a/tools/qrcheck/Cargo.lock b/tools/qrcheck/Cargo.lock
new file mode 100644
index 0000000..141dd97
--- /dev/null
+++ b/tools/qrcheck/Cargo.lock
@@ -0,0 +1,7 @@
1# This file is automatically @generated by Cargo.
2# It is not intended for manual editing.
3version = 4
4
5[[package]]
6name = "qrcheck"
7version = "0.1.0"
diff --git a/tools/qrcheck/Cargo.toml b/tools/qrcheck/Cargo.toml
new file mode 100644
index 0000000..4d4487c
--- /dev/null
+++ b/tools/qrcheck/Cargo.toml
@@ -0,0 +1,13 @@
1[package]
2name = "qrcheck"
3version = "0.1.0"
4edition = "2024"
5
6[[bin]]
7name = "qrcheck"
8path = "src/main.rs"
9
10[profile.release]
11panic = "abort"
12
13[workspace]
diff --git a/tools/qrcheck/src/main.rs b/tools/qrcheck/src/main.rs
new file mode 100644
index 0000000..d294485
--- /dev/null
+++ b/tools/qrcheck/src/main.rs
@@ -0,0 +1,58 @@
1//! Host-side cross-check tool for the kernel QR encoder.
2//!
3//! Usage: `qrcheck TEXT [VERSION] [MASK]`
4//! - with VERSION+MASK: force both and dump the matrix (compared against
5//! segno by `tools/qrcheck.py`);
6//! - with neither: auto-pick version and best mask, print `v<ver> m<mask>`.
7//!
8//! Output: first line `v<N> m<N|999>`, then one line per matrix row of
9//! `#` (dark) / `.` (light).
10#![allow(static_mut_refs)]
11
12#[path = "../../../rsnix/src/qr.rs"]
13mod qr;
14
15fn main() {
16 let args: Vec<String> = std::env::args().collect();
17 if args.get(1).map(String::as_str) == Some("--raw") {
18 let text = args.get(2).map(String::as_str).unwrap_or("hello");
19 let version: usize = args.get(3).and_then(|s| s.parse().ok()).unwrap_or(0);
20 let v = if version > 0 {
21 qr::encode_at(text.as_bytes(), version, Some(0))
22 } else {
23 qr::encode(text.as_bytes())
24 };
25 if v == 0 {
26 eprintln!("FAIL");
27 std::process::exit(1);
28 }
29 for b in qr::codeword_stream() {
30 print!("{:02x}", b);
31 }
32 println!();
33 return;
34 }
35 let text = args.get(1).map(String::as_str).unwrap_or("hello world");
36 let version: usize = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
37 let mask: Option<usize> = args.get(3).and_then(|s| s.parse().ok());
38
39 let v = if version > 0 {
40 qr::encode_at(text.as_bytes(), version, mask)
41 } else {
42 qr::encode(text.as_bytes())
43 };
44 if v == 0 {
45 eprintln!("FAIL: cannot encode {} bytes at v{}", text.len(), version);
46 std::process::exit(1);
47 }
48 let size = qr::size();
49 let m = if let Some(m) = mask { m } else { qr::last_mask() };
50 println!("v{} m{}", v, m);
51 for y in 0..size {
52 let mut row = String::with_capacity(size);
53 for x in 0..size {
54 row.push(if qr::module(x, y) { '#' } else { '.' });
55 }
56 println!("{}", row);
57 }
58}