summaryrefslogtreecommitdiff
path: root/modules/test/test-cryptogzip-guard.js
diff options
context:
space:
mode:
Diffstat (limited to 'modules/test/test-cryptogzip-guard.js')
-rw-r--r--modules/test/test-cryptogzip-guard.js136
1 files changed, 136 insertions, 0 deletions
diff --git a/modules/test/test-cryptogzip-guard.js b/modules/test/test-cryptogzip-guard.js
new file mode 100644
index 0000000..14f50d3
--- /dev/null
+++ b/modules/test/test-cryptogzip-guard.js
@@ -0,0 +1,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);
+})();