aboutsummaryrefslogtreecommitdiff
path: root/modules/test/test-cryptogzip-gunzip.js
diff options
context:
space:
mode:
Diffstat (limited to 'modules/test/test-cryptogzip-gunzip.js')
-rw-r--r--modules/test/test-cryptogzip-gunzip.js130
1 files changed, 130 insertions, 0 deletions
diff --git a/modules/test/test-cryptogzip-gunzip.js b/modules/test/test-cryptogzip-gunzip.js
new file mode 100644
index 0000000..3a9411f
--- /dev/null
+++ b/modules/test/test-cryptogzip-gunzip.js
@@ -0,0 +1,130 @@
+// node modules/test/test-cryptogzip-gunzip.js
+// test-cryptogzip-gunzip — plan 2026-11-03_061900-newos4-cryptogzip-base64-guard.md, T3.
+// `_cmdGunzip` must go through the byte path so that:
+// (1) a base64 .gz written by the module's own `gzip` still decompresses,
+// (2) a real gzip file (raw Latin-1 bytes in the VFS, e.g. from `wget`) also decompresses
+// — before T3 this reported "not valid base64 input (…)" and could not be unpacked at all,
+// (3) a plain text file named *.gz reports exactly "not a gzip file" and changes nothing,
+// (4) the "output already exists" confirm guard still refuses and changes nothing.
+const fs = require('fs');
+const path = require('path');
+
+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 : ''));
+}
+
+// VFS stub mirroring the real semantics: the exact string handed to writeFile is what readFile
+// returns (base64 text, or Latin-1 one-char-per-byte for _vfsWriteBytes).
+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, answer: (line) => term.inputCallback(line) };
+}
+
+const SUCCESS = /^gunzipped: (\S+) → (\S+) \((\d+) → (\d+) chars\)$/;
+
+(async () => {
+ // ── T1: module-produced (base64) .gz still decompresses (legacy path) ────────────────
+ const t1 = mkTerm({});
+ t1.store['note.txt.gz'] = await t1.mod._gzipString('gunzip me');
+ await t1.mod.commands['gunzip'](['note.txt.gz']);
+ const s1 = t1.printed.find(p => p.kind === 'success');
+ ok('T1 base64 .gz decompresses', t1.store['note.txt'] === 'gunzip me',
+ JSON.stringify(Object.keys(t1.store)));
+ ok('T1 .gz removed after success', !('note.txt.gz' in t1.store),
+ JSON.stringify(Object.keys(t1.store)));
+ ok('T1 success line keeps its format', !!s1 && SUCCESS.test(s1.msg), s1 ? s1.msg : 'no success line');
+
+ // ── T2: real gzip bytes in the VFS (what `wget` stores) decompress ───────────────────
+ const t2 = mkTerm({});
+ const payload = 'binary payload — ümlauts ✓\nline two\n';
+ const gzBytes = await t2.mod._gzipBytes(new TextEncoder().encode(payload));
+ t2.mod._vfsWriteBytes('dl.txt.gz', gzBytes); // raw Latin-1, like wget
+ const asRead = t2.mod._vfsReadBytes('dl.txt.gz');
+ ok('T2 fixture is real gzip magic and read back byte-identical',
+ t2.mod._isGzip(asRead) && Buffer.from(asRead).equals(Buffer.from(gzBytes)),
+ asRead.length + ' bytes, magic ' + asRead[0].toString(16) + ' ' + asRead[1].toString(16));
+ await t2.mod.commands['gunzip'](['dl.txt.gz']);
+ ok('T2 raw gzip bytes decompress', t2.store['dl.txt'] === payload,
+ JSON.stringify(t2.store['dl.txt']));
+ ok('T2 no "not valid base64" on a real gzip file',
+ !t2.printed.some(p => /base64/.test(p.msg)), JSON.stringify(t2.printed));
+
+ // ── T3: plain text named .gz -> exactly "not a gzip file", nothing touched ───────────
+ const t3 = mkTerm({ 'fake.gz': 'this is not compressed at all' });
+ await t3.mod.commands['gunzip'](['fake.gz']);
+ const err3 = t3.printed.find(p => /^gunzip error: /.test(p.msg));
+ ok('T3 reports "not a gzip file"', !!err3 && err3.msg === 'gunzip error: not a gzip file',
+ err3 ? err3.msg : 'no gunzip error line');
+ ok('T3 error is not a base64 message', !!err3 && !/base64/.test(err3.msg),
+ err3 ? err3.msg : 'no gunzip error line');
+ ok('T3 input file untouched', t3.store['fake.gz'] === 'this is not compressed at all',
+ JSON.stringify(t3.store));
+ ok('T3 no output file created', !('fake' in t3.store), JSON.stringify(Object.keys(t3.store)));
+ ok('T3 nothing deleted (exactly one key left)', Object.keys(t3.store).length === 1,
+ JSON.stringify(Object.keys(t3.store)));
+ ok('T3 no success line', !t3.printed.some(p => p.kind === 'success'), JSON.stringify(t3.printed));
+
+ // ── T4: output-exists guard still refuses (answer "no") and changes nothing ──────────
+ const t4 = mkTerm({});
+ t4.store['dup.txt.gz'] = await t4.mod._gzipString('compressed contents');
+ t4.store['dup.txt'] = 'ORIGINAL — must survive';
+ const p4 = t4.mod.commands['gunzip'](['dup.txt.gz']);
+ t4.answer('no');
+ await p4;
+ ok('T4 refuses to overwrite without confirmation', t4.store['dup.txt'] === 'ORIGINAL — must survive',
+ JSON.stringify(t4.store['dup.txt']));
+ ok('T4 leaves the .gz in place', t4.store['dup.txt.gz'] !== undefined,
+ JSON.stringify(Object.keys(t4.store)));
+ ok('T4 prints Cancelled.', t4.printed.some(p => /^Cancelled\.$/.test(p.msg)),
+ JSON.stringify(t4.printed));
+
+ // ── T5: same guard, answer "yes" -> overwrite proceeds ───────────────────────────────
+ const t5 = mkTerm({});
+ t5.store['dup2.txt.gz'] = await t5.mod._gzipString('new contents');
+ t5.store['dup2.txt'] = 'stale';
+ const p5 = t5.mod.commands['gunzip'](['dup2.txt.gz']);
+ t5.answer('yes');
+ await p5;
+ ok('T5 overwrite proceeds on "yes"', t5.store['dup2.txt'] === 'new contents'
+ && !('dup2.txt.gz' in t5.store), JSON.stringify(t5.store));
+
+ // ── T6: non-.gz path is still refused with the Usage line ────────────────────────────
+ const t6 = mkTerm({ 'plain.txt.gz': 'x', 'other.txt': 'y' });
+ await t6.mod.commands['gunzip'](['other.txt']);
+ ok('T6 refuses a non-.gz path', t6.printed.some(p => /^Usage: gunzip /.test(p.msg))
+ && t6.store['other.txt'] === 'y', JSON.stringify(t6.printed));
+
+ // ── T7: missing file is still a "File not found" error (no throw) ────────────────────
+ const t7 = mkTerm({});
+ let threw7 = null;
+ try { await t7.mod.commands['gunzip'](['nope.gz']); } catch (e) { threw7 = e; }
+ ok('T7 missing file reports File not found', !threw7
+ && t7.printed.some(p => /^File not found: nope\.gz$/.test(p.msg)),
+ threw7 ? 'threw ' + threw7.message : JSON.stringify(t7.printed));
+
+ console.log('test-cryptogzip-gunzip: ' + passes + ' passed, ' + fails + ' failed');
+ process.exit(fails ? 1 : 0);
+})().catch(e => { console.error('FATAL ' + (e && e.stack || e)); process.exit(1); });