aboutsummaryrefslogtreecommitdiff
path: root/modules/test/test-cryptogzip-guard.js
blob: 14f50d34c0aad59b8f3da5f68ef8c8b5f7357fdf (plain)
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
// node modules/test/test-cryptogzip-guard.js
// test-cryptogzip-guard — CryptoGzipModule._base64ToBytes must reject non-base64 input with a
// plain, actionable Error, not a raw DOMException out of atob() (RPT-20261102-203616-e2641bff:
// "Failed to execute 'atob' on 'Window': The string to be decoded contains characters outside of
// the Latin1 range"), which escaped as an unhandledrejection when called from the node console.
//
// It also pins the happy path byte-for-byte (including unpadded base64 and newline-wrapped
// base64, both of which atob already accepted) and the two gunzip callers.
const fs = require('fs');
const path = require('path');
const zlib = require('zlib');

global.window = {};   // the file's auto-registration tail is window-guarded

const SRC = path.join(__dirname, '..', 'ext', 'cryptogzip.js');
const CryptoGzipModule = new Function(fs.readFileSync(SRC, 'utf8') + '\nreturn CryptoGzipModule;')();

let passes = 0, fails = 0;
function ok(name, cond, detail) {
    if (cond) { passes++; return; }
    fails++;
    console.log('FAIL ' + name + (detail ? ' — ' + detail : ''));
}

const m = new CryptoGzipModule();
const isDomException = (e) => typeof DOMException === 'function' && e instanceof DOMException;

function mkTerm(files) {
    const store = Object.assign({}, files || {});
    const printed = [];
    const term = {
        print: (msg, kind) => printed.push({ msg: String(msg), kind: kind || '' }),
        fs: {
            readFile: (p) => (p in store ? store[p] : null),
            writeFile: (p, c) => { store[p] = c; return true; },
            deleteFile: (p) => { delete store[p]; },
            readDir: () => []
        },
        inputCallback: null,
        _onCommand: null
    };
    const mod = new CryptoGzipModule();
    mod.terminal = term;
    mod.registerCommands();
    return { mod, term, printed, store };
}

(async () => {
    // C1-C2: happy path
    const hello = m._base64ToBytes('aGVsbG8=');
    ok('C1 decodes base64 to the right bytes', Buffer.from(hello).toString() === 'hello',
        Buffer.from(hello).toString());
    ok('C2 returns a 5-byte Uint8Array', hello instanceof Uint8Array && hello.length === 5,
        hello.constructor && hello.constructor.name + ' len ' + hello.length);

    // C3: every input that used to reach atob() must now be a plain Error mentioning base64
    const bad = [
        ['hello world!', 'non-base64 text'],
        ['☃', 'non-Latin1 character'],
        [null, 'null'],
        [undefined, 'undefined'],
        [123, 'number'],
        [{}, 'object'],
        ['abcde=', 'bad padding'],
        ['ab=', 'incomplete padding']
    ];
    for (const [value, label] of bad) {
        let err = null;
        try { m._base64ToBytes(value); } catch (e) { err = e; }
        const plainError = err instanceof Error && !isDomException(err) && err.name !== 'InvalidCharacterError';
        ok('C3 ' + label + ' throws a plain Error that mentions base64',
            plainError && /base64/.test(err.message),
            err ? err.name + ': ' + err.message : 'did not throw');
    }

    // C4: long valid base64 with CRLF/LF wrapping still decodes (atob ignores ASCII whitespace)
    const long = 'The quick brown fox jumps over the lazy dog. '.repeat(40);
    const b64 = Buffer.from(long, 'utf8').toString('base64');
    const wrapped = b64.replace(/(.{60})/g, '$1\r\n') + '\n';
    const out = Buffer.from(m._base64ToBytes(wrapped)).toString('utf8');
    ok('C4 newline-wrapped base64 decodes', out === long, out.length + ' vs ' + long.length + ' chars');

    // C5: byte-identical to atob() for every form atob accepted before (incl. unpadded)
    const corpus = ['aGVsbG8=', 'aGVsbG8', 'ab==', 'abc=', 'abcd', '', '  aGVs bG8=\n', 'aGVsbG8g\td29ybGQ='];
    let same = true, why = '';
    for (const c of corpus) {
        let want, got;
        try { want = Buffer.from(atob(c), 'binary').toString('hex'); } catch (e) { want = 'THROW'; }
        try { got = Buffer.from(m._base64ToBytes(c)).toString('hex'); } catch (e) { got = 'THROW'; }
        if (want !== got) { same = false; why = JSON.stringify(c) + ' atob=' + want + ' guard=' + got; break; }
    }
    ok('C5 happy path is byte-identical to atob()', same, why);

    // C6: _gunzipString round-trip (base64 gzip payload)
    const gz = zlib.gzipSync(Buffer.from('hi hi hi', 'utf8'));
    const text = await m._gunzipString(gz.toString('base64'));
    ok('C6 _gunzipString round-trips a real gzip payload', text === 'hi hi hi', JSON.stringify(text));

    // C7: _gunzipString on garbage rejects with the guarded Error, not a DOMException
    let gzErr = null;
    try { await m._gunzipString('hello world!'); } catch (e) { gzErr = e; }
    ok('C7 _gunzipString on non-base64 throws a guarded Error',
        !!gzErr && gzErr instanceof Error && !isDomException(gzErr) && /base64/.test(gzErr.message),
        gzErr ? gzErr.name + ': ' + gzErr.message : 'did not throw');

    // C8: `gunzip` command path prints its existing "gunzip error:" line instead of crashing
    const t8 = mkTerm({ 'notbase64.gz': 'hello world!' });
    let cmdThrew = null;
    try { await t8.mod.commands['gunzip'](['notbase64.gz']); } catch (e) { cmdThrew = e; }
    const line = t8.printed.find(p => /^gunzip error: /.test(p.msg));
    ok('C8 gunzip prints "gunzip error: … not valid base64 …" and does not throw',
        !cmdThrew && !!line && /base64/.test(line.msg) && t8.store['notbase64.gz'] === 'hello world!',
        cmdThrew ? 'threw ' + cmdThrew.message : JSON.stringify(t8.printed));

    // C9: legacy path — a base64 .gz written by `gzip` still decompresses
    const t9 = mkTerm({ 'note.txt.gz': zlib.gzipSync(Buffer.from('gunzip me', 'utf8')).toString('base64') });
    await t9.mod.commands['gunzip'](['note.txt.gz']);
    ok('C9 gunzip still decompresses a base64 .gz', t9.store['note.txt'] === 'gunzip me'
        && !('note.txt.gz' in t9.store), JSON.stringify(Object.keys(t9.store)));

    // C10-C11: the VFS helpers that call _base64ToBytes keep their behaviour
    const gzRaw = zlib.gzipSync(Buffer.from('latin1 payload', 'utf8'));
    const latin1 = Buffer.from(gzRaw).toString('latin1');   // what _vfsWriteBytes stores
    const t10 = mkTerm({ 'blob.gz': latin1 });
    const raw = t10.mod._vfsReadBytes('blob.gz');           // guard throws -> Latin-1 fallback
    ok('C10 _vfsReadBytes falls back to raw Latin-1 bytes (guard rejection is swallowed)',
        t10.mod._isGzip(raw) && Buffer.from(raw).equals(gzRaw), raw.length + ' bytes');

    const t11 = mkTerm({ 'b64.gz': gzRaw.toString('base64') });
    const dec = t11.mod._vfsReadBytes('b64.gz');            // heuristic -> base64 decode
    ok('C11 _vfsReadBytes still base64-decodes a stored payload',
        t11.mod._isGzip(dec) && Buffer.from(dec).equals(gzRaw), dec.length + ' bytes');

    console.log('test-cryptogzip-guard: ' + passes + ' passed, ' + fails + ' failed');
    process.exit(fails ? 1 : 0);
})();