tools/windows/ .ps1 (pwsh, so usable from Linux and macOS too)
tools/posix/ .sh
tools/python/ .py
tools/ .c and .pl
The .c and .pl files stay at the top level because they are not
host-specific: the build compiles or invokes them on every platform.
Call sites updated in the Makefile, ports/mk/port.mk, docs and the
project memory. The one thing that does not survive a move on its own is
a script deriving the repo root from its own location -- all nine
PowerShell tools used Split-Path $PSScriptRoot -Parent, and memory-sync.sh
used dirname/.., both of which quietly become tools/ one level deeper.
Two levels now.
Two pre-existing Windows failures surfaced while verifying, both real and
neither caused by the move:
- schfs_fsck.c did not compile under mingw at all: --extract uses
POSIX mkdir(path,mode), symlink() and link(), none of which mingw
has. That took the whole tool with it, and `make schfs-check` with
it. mkdir maps to _mkdir; symlink and link fail loudly rather than
writing a regular file in their place, because an extracted tree
that differs from the image while reporting success defeats the
point of --extract.
- the round-trip test then failed at `ln -s`, since Git Bash cannot
create symlinks without developer mode. It skips with a message now.
Not a fudge: mkschfs's mingw build cannot see a symlink either, so
the test would have compared a tree with none against an image with
none and called it a pass -- coverage reporting itself as present
while being absent.
Verified on Windows: build clean, all seven bootcheck suites green,
make schfs-check passes.
185 lines
6.9 KiB
Python
185 lines
6.9 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 capacity per version: total - ecc*blocks - 2. Mirrors the table in qr.c;
|
|
# the exact-fill cases below are built from it, and a disagreement would show up
|
|
# as a version mismatch rather than passing quietly.
|
|
CAPACITY = {1: 17, 2: 32, 3: 53, 4: 78, 5: 106, 6: 134}
|
|
|
|
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 -- including 6, which
|
|
# is the only one with more than one Reed-Solomon block.
|
|
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)
|