SchOS/tools/python/qrcheck.py
Nexoniarz d8e8ae604f qr: every version, 1 to 40
PSOGM mode will put the kernel log on screen as QR codes, a page per
symbol, and version 6 held 134 bytes. Version 40 at level L holds 2,953.

What grew: blocks of two lengths and the interleave that steps around
them, the alignment-pattern grid, the version-information blocks from
version 7, the 16-bit character count from version 10. The per-version
table is down to the two columns that are genuinely transcription (EC
codewords per block, block count); total codewords come from the
geometry.

Checked against segno module for module at every version (exact-fill
payloads), and 120 symbols across all 40 versions decode with zbar.
OpenCV's detector cannot find symbols above about version 24 at all,
segno's included, so qrcheck reports those as the detector's limit.

The crash card stays a version-6 symbol (QR_CARD_BYTES), so a phone still
reads it from across the desk.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-25 00:43:36 +02:00

191 lines
7.3 KiB
Python

#!/usr/bin/env python3
"""Cross-check kernel/core/qr.c against independent implementations.
The self-checks inside tools/qrtest.c verify what has a local notion of
correctness -- table arithmetic, the Reed-Solomon generator polynomials, whether
a finder pattern is where it should be. They cannot verify the parts that only
mean something to a decoder: zigzag placement order, mask selection, and block
interleaving. All three have the same failure mode, which is a symbol that looks
exactly like a QR code and is noise.
Two oracles, in order of authority:
1. **A real decoder** (OpenCV). Does the symbol read back as the exact bytes
that went in? This is the only test that matches what the feature is for --
somebody pointing a phone at a crashed machine -- and it is the one that
decides pass or fail.
2. **A second encoder** (segno), matrix compared module for module. Stricter
than decoding when it agrees, because it pins down the exact mask and
placement rather than merely something readable. Applied only to payloads
that exactly fill their version, for the reason below.
segno adds a spurious zero pad codeword to every byte-mode symbol. Its
write_padding_bits() does `buff.extend([0] * (8 - (length % 8)))`, which
emits a whole byte when the stream is already on a codeword boundary -- and
in byte mode the stream is always 8L+16 bits after the terminator, so it
always is. ISO/IEC 18004 makes that conditional ("if the bit stream length is
such that it does not end at a codeword boundary"), and segno quotes exactly
that sentence above the line. It is harmless -- decoders read `count` bytes
and ignore padding -- but it means the two encoders cannot agree bit-for-bit
on any payload that needs padding, so those are compared by decoding only.
pip install --target <dir> opencv-python-headless numpy segno
PYTHONPATH=<dir> python tools/qrcheck.py
Requires build/tools/qrtest (make qrtest). Both libraries are optional; whatever
is installed gets used.
"""
import os
import subprocess
import sys
# Normalised and absolute: Windows CreateProcess rejects a relative path written
# with forward slashes, even when the file is plainly there.
QRTEST = os.path.abspath(os.path.join("build", "tools",
"qrtest.exe" if os.name == "nt" else "qrtest"))
try:
import cv2
import numpy as np
except ImportError:
cv2 = None
try:
import segno
except ImportError:
segno = None
if cv2 is None and segno is None:
sys.exit("qrcheck: needs opencv-python-headless and/or segno")
if not os.path.exists(QRTEST):
sys.exit(f"qrcheck: {QRTEST} not found -- run `make qrtest`")
def ours(text):
"""Our encoder's matrix, as a list of strings of '0'/'1'."""
out = subprocess.run([QRTEST, "-m", text], capture_output=True, text=True)
if out.returncode != 0:
raise RuntimeError(f"qrtest failed on {text!r}")
return out.stdout.split()
def our_mask(m):
"""The mask our encoder chose, read back out of the format bits.
Read from the symbol rather than reported by it, because the point is to
check what was actually written.
"""
bits = 0
for i in range(15):
if i <= 5: b = int(m[i][8])
elif i == 6: b = int(m[7][8])
elif i == 7: b = int(m[8][8])
elif i == 8: b = int(m[8][7])
else: b = int(m[8][14 - i])
bits |= b << i
return ((bits ^ 0x5412) >> 10) & 7
def render(m, scale, border):
n = len(m)
side = (n + 2 * border) * scale
img = np.full((side, side), 255, np.uint8)
for y in range(n):
for x in range(n):
if m[y][x] == "1":
img[(y + border) * scale:(y + border + 1) * scale,
(x + border) * scale:(x + border + 1) * scale] = 0
return img
# OpenCV's detector is not a reference implementation and does fail on symbols
# that are perfectly valid -- sensitive to scale and quiet-zone width in ways
# that have nothing to do with the encoding. Several renderings are tried before
# concluding anything, and a payload that still will not decode is re-tested
# against segno's symbol for the same input: if that fails too, the detector is
# what is at fault, not the encoder. Without that second step this check reports
# false failures, which is worse than not running it.
RENDERINGS = [(8, 4), (6, 6), (8, 6), (4, 4), (12, 6), (16, 4)]
def decode(m):
for scale, border in RENDERINGS:
text, _, _ = cv2.QRCodeDetector().detectAndDecode(render(m, scale, border))
if text:
return text
return ""
# Byte-mode capacity per version at level L, from ISO/IEC 18004 table 7 --
# written out rather than computed, so it checks qr.c instead of agreeing with
# it. The exact-fill cases below are built from it, and a disagreement shows up
# as a version mismatch rather than passing quietly.
CAPACITY = dict(enumerate([
17, 32, 53, 78, 106, 134, 154, 192, 230, 271, 321, 367, 425, 458, 520,
586, 644, 718, 792, 858, 929, 1003, 1091, 1171, 1273, 1367, 1465, 1528,
1628, 1732, 1840, 1952, 2068, 2188, 2303, 2431, 2563, 2699, 2809, 2953,
], start=1))
alpha = lambda n: "".join(chr(65 + i % 26) for i in range(n))
CASES = [
"HELLO",
"SCHOS",
"a",
"0123456789",
"SCHOS PANIC\ntrap 14 err 4 cpu 0\nrip ffffffff80104e2a",
bytes(range(1, 40)).decode("latin-1"), # non-printable payload
]
# One payload that exactly fills each version, and one just under, so both the
# padded and unpadded paths are covered at every version: several Reed-Solomon
# blocks from 6, blocks of two lengths from 6 too, the version information
# from 7, the 16-bit character count from 10.
for v, cap in CAPACITY.items():
CASES.append(alpha(cap))
CASES.append(alpha(cap - 1))
fails = 0
print("decode (OpenCV)" if cv2 else "decode -- skipped, no opencv")
for text in CASES:
if not cv2:
break
m = ours(text)
v = (len(m) - 17) // 4
got = decode(m)
if got == text:
print(f" [ok] v{v} {len(text):3d}B")
continue
# Before blaming the encoder, check whether an independently generated
# symbol for the same payload decodes either.
if segno and not decode([
"".join(str(x) for x in row)
for row in segno.make(text.encode("latin-1"), error="l", version=v,
boost_error=False, mode="byte").matrix]):
print(f" [detector] v{v} {len(text):3d}B -- segno's symbol does not "
f"decode either; OpenCV's limitation, not ours")
continue
fails += 1
print(f" [FAIL] v{v} {len(text):3d}B decoded {got[:40]!r}")
print("\nmatrix vs segno (exact-fill payloads only)" if segno else "\nsegno -- skipped")
for v, cap in CAPACITY.items():
if not segno:
break
text = alpha(cap)
m = ours(text)
mask = our_mask(m)
q = segno.make(text.encode("latin-1"), error="l", version=v,
mask=mask, boost_error=False, mode="byte")
ref = ["".join(str(x) for x in row) for row in q.matrix]
ok = m == ref
fails += not ok
n = len(m)
diff = "" if ok else f" {sum(1 for a, b in zip(''.join(m), ''.join(ref)) if a != b)}/{n*n} differ"
print(f" [{'ok' if ok else 'FAIL'}] v{v} mask {mask} {cap:3d}B{diff}")
print()
print("qrcheck: FAILED" if fails else "qrcheck: passed")
sys.exit(1 if fails else 0)