// 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); })();