summaryrefslogtreecommitdiff
path: root/test/codes/CodeCacheTest.mc
blob: 3b2a02e397ba4533d8a8613b073dd666eac29ccb (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
// Author:  cirakg & claude ai
// Summary: Tests for the encrypted barcode matrix cache: round trip, packed format, runs, 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, 1, 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);
    var pdf = new Codes.Matrix(3, 2, 2, [1, 0, 1, 0, 1, 1]b);
    pdf.rowHeight = 3;
    var round = Codes.Matrix.fromBytes(pdf.toBytes());
    return code == VaultError.LOCKED && back != null && Bytes.equal(back, [3, 1, 4, 1, 0xE0]b)
        && Codes.Matrix.fromBytes([2, 0, 3, 0, 1, 4, 1, 0xE0]b) != null && Codes.Matrix.fromBytes([2, 0, 3, 0, 1, 4]b) == null
        && round != null && round.rowHeight == 3 && Bytes.equal(round.modules, [1, 0, 1, 0, 1, 1]b);
}

// Format 2 holds widths over 255 (wide PDF417); a 0.7 cache (one width
// byte) is refused, so the code is encoded again.
(:test)
function codeCachePackedFormat(logger as Test.Logger) as Boolean {
    var bits = new [300 * 2]b;
    for (var i = 0; i < bits.size(); i++) {
        bits[i] = (i * 7 / 3) % 2;
    }
    var wide = new Codes.Matrix(300, 2, 2, bits);
    var bytes = wide.toBytes();
    var back = Codes.Matrix.fromBytes(bytes);
    var oldFormat = Codes.Packed.parse([3, 1, 4, 1, 0xE0]b);
    var wrongSize = Codes.Packed.parse(bytes.slice(0, bytes.size() - 1));
    return bytes[0] == 2 && back != null && back.width == 300 && back.height == 2 && Bytes.equal(back.modules, bits)
        && oldFormat == null && wrongSize == null && Codes.Packed.parse(null) == null;
}

// The runs drawn on screen are exactly the dark modules, row by row,
// including runs across byte boundaries and whole dark or light bytes.
(:test)
function codeRunsCoverExactlyTheDarkModules(logger as Test.Logger) as Boolean {
    var codes = [Qr.encode("runs, runs, runs", 1, 0, -1) as Qr.MatrixCode];
    var matrices = [new Codes.Matrix(codes[0].size, codes[0].size, 4, codes[0].modules)] as Array<Codes.Matrix>;
    var stripes = new [37 * 5]b;
    for (var i = 0; i < stripes.size(); i++) {
        stripes[i] = (i % 37) < 20 || (i % 37) > 33 ? 1 : 0;
    }
    matrices.add(new Codes.Matrix(37, 5, 1, stripes));
    for (var c = 0; c < matrices.size(); c++) {
        var m = matrices[c];
        var finder = new Codes.RunFinder(Codes.Packed.parse(m.toBytes()) as Codes.Packed);
        Jobs.runToCompletion(finder, 1);
        var drawn = new [m.width * m.height]b;
        var runs = finder.result.runs;
        for (var i = 0; i < runs.size(); i++) {
            var r = runs[i];
            var y = r >> 20;
            var x = (r >> 10) & 0x3FF;
            for (var k = 0; k < (r & 0x3FF); k++) {
                if (drawn[y * m.width + x + k] == 1) {
                    logger.error("matrix " + c + ": run " + i + " overlaps");
                    return false;
                }
                drawn[y * m.width + x + k] = 1;
            }
        }
        if (!Bytes.equal(drawn, m.modules)) {
            logger.error("matrix " + c + ": runs differ from the modules");
            return false;
        }
    }
    return true;
}

// Steps a display job to the end and checks its percentage follows the
// work: never down, never above 99, no big jumps, and a real spread (the
// "Encoding" screen once showed 90 % at the first frame and sat there).
function progressIsSteady(job as Codes.DisplayJob) as Boolean {
    var last = job.percent();
    var biggest = last;
    var steps = 0;
    var steady = last < 35;
    while (!job.step(1)) {
        var now = job.percent();
        steady = steady && now >= last && now <= 99;
        biggest = now - last > biggest ? now - last : biggest;
        last = now;
        steps++;
    }
    return steady && biggest <= 35 && steps >= 3 && last >= 60 && job.error == null;
}

(:test)
function displayProgressFollowsTheWork(logger as Test.Logger) as Boolean {
    var member = "https://example.com/member/fake-0042?name=Alice%20Example&card=0000111122223333&valid=2027-12-31&level=gold";
    var long = "";
    for (var i = 0; i < 40; i++) {
        long += "abcdefghij";
    }
    var qr = progressIsSteady(new Codes.DisplayJob(new Qr.QrJob(member, 1, 0, -1), null));
    var qrBig = progressIsSteady(new Codes.DisplayJob(new Qr.QrJob(long, 0, 0, -1), null));
    var qrForced = progressIsSteady(new Codes.DisplayJob(new Qr.QrJob(member, 1, 0, 3), null));
    var dm = progressIsSteady(new Codes.DisplayJob(new DataMatrix.DataMatrixJob(long.substring(0, 200) as String), null));
    var az = progressIsSteady(new Codes.DisplayJob(new Aztec.AztecJob(long.substring(0, 200) as String, 1), null));
    var pdf = progressIsSteady(new Codes.DisplayJob(new Pdf417.Pdf417Job(long.substring(0, 200) as String, 1, 260, 260, true),
        null));
    // A cached code only runs the finder: 0 at the start, then up to 99.
    var bits = new [60 * 60]b;
    for (var i = 0; i < bits.size(); i++) {
        bits[i] = (i * 7 / 3) % 2;
    }
    var packed = Codes.Packed.parse(new Codes.Matrix(60, 60, 4, bits).toBytes()) as Codes.Packed;
    var cached = new Codes.DisplayJob(null, packed);
    var start = cached.percent();
    var cachedSteady = progressIsSteady(cached);
    logger.debug("progress qr=" + qr + " big=" + qrBig + " forced=" + qrForced + " dm=" + dm + " az=" + az + " pdf=" + pdf
        + " cached=" + cachedSteady);
    return qr && qrBig && qrForced && dm && az && pdf && start == 0 && cachedSteady;
}