carlos / contra Public

Clone
git clone https://amadan.net/carlos/contra

Plain git — no account needed to clone.

Download

Download this file

1// The QR encoder, against two implementations that are not ours.
2//
3// This is the artifact that has to be right years after it was printed, on a
4// phone we have never seen, when the person holding it has already lost
5// everything else. So it is checked twice on every run:
6//
7// 1. ZXing decodes each symbol back to its payload — "will it scan?"
8// 2. Python's `qrcode` produces the same matrix, bit-for-bit, at the same
9// version, EC level and mask — "is it the RIGHT symbol?"
10//
11// The second question is the one decoding cannot answer. Error correction
12// exists to make a damaged symbol readable, which means a symbol with a real
13// mistake in it can still scan — and would, right up until the day somebody's
14// print was slightly faded too. Reed–Solomon would hide our bug behind its
15// own redundancy.
16//
17// Needs python3 with zxing-cpp, qrcode, pillow and numpy:
18//
19// pip install --user zxing-cpp qrcode pillow numpy
20//
21// A HARD gate. If the oracle is missing this fails rather than skipping: a
22// skip on this test is indistinguishable from a pass, on the one artifact
23// where being wrong is unrecoverable.
24
25import { test } from "node:test";
26import assert from "node:assert/strict";
27import { fileURLToPath } from "node:url";
28import { dirname, join } from "node:path";
29import { run as runOracle } from "./oracle.mjs";
30
31const here = dirname(fileURLToPath(import.meta.url));
32const { encode, smallestVersion } = await import(
33 join(here, "..", "internal", "contra", "static", "crypto", "qr.js"));
34
35// Real printed forms, the extremes of the alphabet, and the boundaries where
36// a version rolls over — which is where a capacity table is wrong if it is
37// wrong anywhere.
38const PAYLOADS = [
39 "C1-E3D5Z-Z4R6M-8BQ93-740R9-8TWYX-MHGE0",
40 "A1-YR7V5-0F8RW-QPE9D-DTK5X-83KV1-0GJC8",
41 "A1-00000-00000-00000-00000-00000-00000",
42 "C1-ZZZZZ-ZZZZZ-ZZZZZ-ZZZZZ-ZZZZZ-ZZZZZ",
43 "0",
44 "HELLO WORLD",
45 "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:",
46];
47
48function ask(cases) {
49 return JSON.parse(runOracle("qroracle.py", cases));
50}
51
52test("every symbol scans, and is the symbol another encoder would have made", () => {
53 const cases = [];
54 for (const text of PAYLOADS) {
55 for (const ec of ["L", "M", "Q", "H"]) {
56 for (let mask = 0; mask < 8; mask++) {
57 const r = encode(text, { ec, mask });
58 cases.push({ text, ec, mask, version: r.version, modules: r.modules });
59 }
60 }
61 }
62
63 const verdicts = ask(cases);
64 assert.equal(verdicts.length, cases.length);
65
66 const bad = verdicts.filter((v) => !v.ok);
67 const describe = (v) =>
68 `${v.ec} mask=${v.mask} v${v.version} ${JSON.stringify(v.text).slice(0, 30)}: ` +
69 `scans=${v.scans} identical=${v.identical}` +
70 (v.differing_modules ? ` differing=${v.differing_modules}` : "") +
71 (v.reference_error ? ` refErr=${v.reference_error}` : "");
72 assert.deepEqual(bad.map(describe), [], `${bad.length} of ${cases.length} symbols wrong`);
73});
74
75// The mask the encoder picks on its own must also be right — the sweep above
76// forces every mask, which would not catch a broken penalty score.
77test("the automatically chosen mask still scans", () => {
78 const cases = PAYLOADS.map((text) => {
79 const r = encode(text, { ec: "Q" });
80 return { text, ec: "Q", mask: r.mask, version: r.version, modules: r.modules };
81 });
82 for (const v of ask(cases)) {
83 assert.ok(v.scans, `${JSON.stringify(v.text)} did not scan at its chosen mask`);
84 assert.ok(v.identical, `${JSON.stringify(v.text)} differs from the reference`);
85 }
86});
87
88// A CONTRA code at the EC level the kit prints at. Pinned so a change to
89// either is a deliberate one.
90test("a printed code is a version 3 symbol at EC-Q", () => {
91 const code = "C1-E3D5Z-Z4R6M-8BQ93-740R9-8TWYX-MHGE0";
92 assert.equal(code.length, 38);
93 assert.equal(smallestVersion(code, "Q"), 3);
94 const r = encode(code, { ec: "Q" });
95 assert.equal(r.version, 3);
96 assert.equal(r.modules.length, 29, "a version 3 symbol is 4*3+17 = 29 modules across");
97});
98
99// The encoder refuses rather than guessing, on both edges.
100test("it refuses what it cannot encode", () => {
101 assert.throws(() => encode("lower case"), /not alphanumeric/);
102 assert.throws(() => encode("héllo"), /not alphanumeric/);
103 assert.throws(() => encode("Z".repeat(200), { ec: "H" }), /version above 6/);
104 assert.throws(() => encode("SHORT", { ec: "X" }), /unknown EC level/);
105});
106