From 9fde94d068f87b34e0bc6bfa48388d89bb54f1e4 Mon Sep 17 00:00:00 2001 From: salmonik Date: Mon, 3 Aug 2026 02:23:35 +0200 Subject: v0.5.8: heap-free QR encoder + crash-dump QR on framebuffer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- tools/qrcheck.py | 208 ++++++++++++++++++++++++++++++++++++++++++++++ tools/qrcheck/.gitignore | 1 + tools/qrcheck/Cargo.lock | 7 ++ tools/qrcheck/Cargo.toml | 13 +++ tools/qrcheck/src/main.rs | 58 +++++++++++++ 5 files changed, 287 insertions(+) create mode 100644 tools/qrcheck.py create mode 100644 tools/qrcheck/.gitignore create mode 100644 tools/qrcheck/Cargo.lock create mode 100644 tools/qrcheck/Cargo.toml create mode 100644 tools/qrcheck/src/main.rs (limited to 'tools') 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 @@ +#!/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 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() 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 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "qrcheck" +version = "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 @@ +[package] +name = "qrcheck" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "qrcheck" +path = "src/main.rs" + +[profile.release] +panic = "abort" + +[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 @@ +//! Host-side cross-check tool for the kernel QR encoder. +//! +//! Usage: `qrcheck TEXT [VERSION] [MASK]` +//! - with VERSION+MASK: force both and dump the matrix (compared against +//! segno by `tools/qrcheck.py`); +//! - with neither: auto-pick version and best mask, print `v m`. +//! +//! Output: first line `v m`, then one line per matrix row of +//! `#` (dark) / `.` (light). +#![allow(static_mut_refs)] + +#[path = "../../../rsnix/src/qr.rs"] +mod qr; + +fn main() { + let args: Vec = std::env::args().collect(); + if args.get(1).map(String::as_str) == Some("--raw") { + let text = args.get(2).map(String::as_str).unwrap_or("hello"); + let version: usize = args.get(3).and_then(|s| s.parse().ok()).unwrap_or(0); + let v = if version > 0 { + qr::encode_at(text.as_bytes(), version, Some(0)) + } else { + qr::encode(text.as_bytes()) + }; + if v == 0 { + eprintln!("FAIL"); + std::process::exit(1); + } + for b in qr::codeword_stream() { + print!("{:02x}", b); + } + println!(); + return; + } + let text = args.get(1).map(String::as_str).unwrap_or("hello world"); + let version: usize = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(0); + let mask: Option = args.get(3).and_then(|s| s.parse().ok()); + + let v = if version > 0 { + qr::encode_at(text.as_bytes(), version, mask) + } else { + qr::encode(text.as_bytes()) + }; + if v == 0 { + eprintln!("FAIL: cannot encode {} bytes at v{}", text.len(), version); + std::process::exit(1); + } + let size = qr::size(); + let m = if let Some(m) = mask { m } else { qr::last_mask() }; + println!("v{} m{}", v, m); + for y in 0..size { + let mut row = String::with_capacity(size); + for x in 0..size { + row.push(if qr::module(x, y) { '#' } else { '.' }); + } + println!("{}", row); + } +} -- cgit v1.2.3