summaryrefslogtreecommitdiff
path: root/test/codes
diff options
context:
space:
mode:
authorcirakg <cirakg@cirakg.xyz>2026-10-20 06:56:58 +0200
committercirakg <cirakg@cirakg.xyz>2026-10-20 06:56:58 +0200
commitf455246fde086dda0cb61b92c84a5146a831a2bf (patch)
tree09b5e97ee5f84e495bbd0646e1ad0e3e2fbfb9af /test/codes
parent2df98d1ba129f95a10c6b81665671d3b12560c79 (diff)
test(codes): compare QR with segno, decode QR dumps and the QR screenshot
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'test/codes')
-rw-r--r--test/codes/CodeCacheTest.mc54
-rw-r--r--test/codes/QrTest.mc120
2 files changed, 174 insertions, 0 deletions
diff --git a/test/codes/CodeCacheTest.mc b/test/codes/CodeCacheTest.mc
new file mode 100644
index 0000000..e09d902
--- /dev/null
+++ b/test/codes/CodeCacheTest.mc
@@ -0,0 +1,54 @@
+// Author: cirakg & claude ai
+// Summary: Tests for the encrypted barcode matrix cache: round trip, keys of locked entries, invalidation and tampering.
+
+import Toybox.Lang;
+import Toybox.Test;
+
+(:test)
+function codeCacheRoundTripsAndInvalidates(logger as Test.Logger) as Boolean {
+ var store = new MemoryBackend();
+ var vault = new Vault(store, new FakeTime());
+ var e = StorageFixtures.code("QR card");
+ e.type = Codes.QR;
+ var id = vault.add(e);
+ var qr = Qr.encode("cache me", 1, 0, -1) as Qr.MatrixCode;
+ var matrix = new Codes.Matrix(qr.size, qr.size, 4, qr.modules);
+ vault.saveCodeCache(id, matrix.toBytes());
+ var back = Codes.Matrix.fromBytes(vault.loadCodeCache(id) as ByteArray);
+ var same = back != null && back.width == qr.size && back.quiet == 4 && Bytes.equal(back.modules, qr.modules);
+ // The cache is sealed: tampering gives no cache rather than a wrong code.
+ var stored = store.raw("x." + id) as ByteArray;
+ stored[stored.size() - 1] = stored[stored.size() - 1] ^ 1;
+ var tampered = vault.loadCodeCache(id) == null;
+ vault.saveCodeCache(id, matrix.toBytes());
+ var opened = vault.open(id);
+ vault.update(opened);
+ opened.wipeSecrets();
+ var clearedByUpdate = store.getValue("x." + id) == null;
+ vault.saveCodeCache(id, matrix.toBytes());
+ vault.remove(id);
+ return same && tampered && clearedByUpdate && store.getValue("x." + id) == null;
+}
+
+// A locked entry's cache uses the master key: unreadable while locked.
+(:test)
+function codeCacheOfLockedEntriesNeedsTheSession(logger as Test.Logger) as Boolean {
+ var vault = new Vault(new MemoryBackend(), new FakeTime());
+ StorageFixtures.setPin(vault, StorageFixtures.PIN);
+ var e = StorageFixtures.code("Locked QR");
+ e.type = Codes.QR;
+ e.locked = true;
+ var id = vault.add(e);
+ vault.saveCodeCache(id, [3, 1, 4, 0xE0]b);
+ vault.lock();
+ var code = 0;
+ try {
+ vault.loadCodeCache(id);
+ } catch (err instanceof VaultError) {
+ code = err.code;
+ }
+ StorageFixtures.unlock(vault, StorageFixtures.PIN);
+ var back = vault.loadCodeCache(id);
+ return code == VaultError.LOCKED && back != null && Bytes.equal(back, [3, 1, 4, 0xE0]b)
+ && Codes.Matrix.fromBytes([3, 1, 4, 0xE0]b) != null && Codes.Matrix.fromBytes([3, 1, 4]b) == null;
+}
diff --git a/test/codes/QrTest.mc b/test/codes/QrTest.mc
new file mode 100644
index 0000000..50cb35a
--- /dev/null
+++ b/test/codes/QrTest.mc
@@ -0,0 +1,120 @@
+// Author: cirakg & claude ai
+// Summary: Tests for the QR encoder: segno reference matrices (forced and automatic version and mask), decoding dumps, capacity, timing.
+
+import Toybox.Lang;
+import Toybox.System;
+import Toybox.Test;
+
+(:test)
+module QrFixtures {
+
+ // Row-major modules packed 8 per byte, as hex (the CodeVectors format).
+ function packedHex(code as Qr.MatrixCode) as String {
+ var m = code.modules;
+ var out = new [(m.size() + 7) / 8]b;
+ for (var i = 0; i < m.size(); i++) {
+ if (m[i] == 1) {
+ out[i / 8] = out[i / 8] | (0x80 >> (i % 8));
+ }
+ }
+ return Bytes.toHex(out);
+ }
+}
+
+// README 12.4 check 1 for QR: identical to segno with forced version and
+// mask, and the same automatic version and mask choice.
+(:test)
+function qrMatchesSegno(logger as Test.Logger) as Boolean {
+ var rows = CodeVectors.QR;
+ for (var i = 0; i < rows.size(); i++) {
+ var row = rows[i] as Array;
+ var payload = row[0] as String;
+ var result = Qr.encode(payload, row[1] as Number, row[2] as Number, row[3] as Number);
+ if (result instanceof Codes.CodeError) {
+ logger.error("row " + i + ": " + result.message);
+ return false;
+ }
+ var expectedVersion = row[4] as Number;
+ var expectedMask = row[5] as Number;
+ if (result.version != expectedVersion || result.mask != expectedMask) {
+ logger.error("row " + i + " '" + Text.clip(payload, 20) + "': version " + result.version + " mask " + result.mask
+ + ", segno " + expectedVersion + " / " + expectedMask);
+ return false;
+ }
+ var got = QrFixtures.packedHex(result);
+ var expected = row[6] as String;
+ if (!got.equals(expected)) {
+ logger.error("row " + i + " '" + Text.clip(payload, 20) + "' (version " + result.version + ") matrix differs");
+ logger.debug("QRGOT " + got);
+ return false;
+ }
+ }
+ logger.debug("CODES qr reference rows=" + rows.size());
+ return true;
+}
+
+// README 12.4 check 2: mixed-mode (segmented), Unicode and long payloads,
+// decoded by tools/check_codes.py with zxing-cpp.
+(:test)
+function qrDumpForDecoding(logger as Test.Logger) as Boolean {
+ var payloads = [
+ "ABC12345678901234xyz", "Membership 0042-7731-55 · Zürich", "12345678901234567890HELLO WORLD hello world",
+ "WIFI:T:WPA;S:FakeNet;P:not-a-real-password;;", "😀 emoji and Ünïcödé", "a"
+ ];
+ var seed = 777;
+ for (var i = 0; i < 24; i++) {
+ var text = "";
+ var length = 1 + i * 13 % 90;
+ for (var k = 0; k < length; k++) {
+ seed = (seed * 1103515245 + 12345) & 0x7FFFFFFF;
+ var pool = (seed >> 16) % 3 == 0 ? "0123456789" : ((seed >> 16) % 3 == 1 ? "ABCXYZ $%*+-./:" : "abcxyz,!?é");
+ var at = (seed >> 4) % pool.length();
+ text += pool.substring(at, at + 1);
+ }
+ payloads.add(text);
+ }
+ for (var i = 0; i < payloads.size(); i++) {
+ var text = payloads[i] as String;
+ var result = Qr.encode(text, i % 4, 0, -1);
+ if (result instanceof Codes.CodeError) {
+ logger.error("'" + text + "': " + result.message);
+ return false;
+ }
+ logger.debug("QRDUMP " + result.size + " " + Bytes.toHex(Bytes.fromUtf8(text)) + " " + QrFixtures.packedHex(result));
+ }
+ return true;
+}
+
+(:test)
+function qrReportsCapacity(logger as Test.Logger) as Boolean {
+ var big = "";
+ for (var i = 0; i < 300; i++) {
+ big += "abcdefghij";
+ }
+ var tooLong = Qr.encode(big, 3, 0, -1);
+ var forced = Qr.encode("hello world, too long for version 1", 3, 1, -1);
+ var empty = Qr.encode("", 1, 0, -1);
+ return tooLong instanceof Codes.CodeError && (tooLong.message.find("too long") != null)
+ && forced instanceof Codes.CodeError && empty instanceof Codes.CodeError;
+}
+
+// Timing and step counts for PERFORMANCE.md (simulator).
+(:test)
+function qrBenchmark(logger as Test.Logger) as Boolean {
+ var text = "";
+ for (var i = 0; i < 20; i++) {
+ text += "abcdefghij";
+ }
+ var inputs = ["https://example.com/member/12345", text, text + text + text + text + text];
+ for (var i = 0; i < inputs.size(); i++) {
+ var job = new Qr.QrJob(inputs[i] as String, 1, 0, -1);
+ var steps = 0;
+ var start = System.getTimer();
+ while (!job.step(1)) {
+ steps++;
+ }
+ var code = job.result as Qr.MatrixCode;
+ logger.debug("BENCH qr_v" + code.version + " ms=" + (System.getTimer() - start) + " steps=" + steps);
+ }
+ return true;
+}