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