summaryrefslogtreecommitdiff
path: root/modules/test/test-cryptogzip-guard.js
diff options
context:
space:
mode:
authorHermes SysOp <sysop@webx11.duckdns.org>2026-11-09 23:58:35 +0100
committerHermes SysOp <sysop@webx11.duckdns.org>2026-11-09 23:58:35 +0100
commit9f6744dd5cb99b904f9a23fce6f2d98e8332bbf8 (patch)
treed3f3a525e8594db010bbf0e2aa6e4c2590445339 /modules/test/test-cryptogzip-guard.js
parent8e3a6155f6a5a1266ff1fa801c8864484bcdbda9 (diff)
fix(newos4): six crash classes from the feedback queue (v4.6.158)
Each item is symptom -> cause -> fix, re-verified against the served bytes. 1. debug-ai panel threw "Cannot read properties of undefined (reading 'substring')" (RPT-20261108-231622). _updateDebugPanel and two sibling previews called JSON.stringify(entry.args).substring(); JSON.stringify(undefined) returns undefined, and _toolCallLog entries carry args: undefined for any tool call with no arguments. All three previews now go through _formatToolArgs(), which renders "null" and never throws. 2. test_module threw "Cannot convert undefined or null to object" (RPT-20261108-232127). Object.keys(instance.commands) assumed every module initialises a command map. The throw landed after loadModuleFromCode had already succeeded, so the rolledBack:true branches were skipped and an unverified module stayed loaded. Both sites now require instance.commands. 3. list_files threw "originalPath.replace is not a function" (RPT-20261109-172742, 3 occurrences in errors.json). A model can send a non-string path despite the schema. The path family (list_files/read_file/write_file/make_directory/ delete_file) now coerces through _pathArg() and answers the tool's own honest error instead of a TypeError. 4. wpm search failed for EVERY keyword (RPT-20261109-123907): packages.json entry `kanban` used `description` while the other 31 use `desc`, and search() called pkg.desc.toLowerCase() mid-loop. Catalog reads now go through _pkg() / _packageMap() (desc || description, values coerced to strings), and the kanban entry carries name/version/desc/commands. Re-running the original loop for pdf/git/zzz is clean. 5. wx/webxai/webx turned a chat-lock timeout into an unhandled rejection (RPT-20261107-150726): forwardToAI() called chat(args) in a synchronous function without returning or catching the promise, so the 10 s busy throw never reached dispatchCommand's catch. It now returns the promise with a .catch that prints "AI error: ..." through terminal.print — console.error is hooked by the debug supervisor and would re-file the same report. 6. CryptoGzip._base64ToBytes threw a raw DOMException on non-base64 input (RPT-20261102-203616). It now validates first and throws Error("not valid base64 input (<reason>)"). The duplicate shadowing definitions at the bottom of the class were deleted — the later copy is the one that carried the unguarded atob(). Also regenerated modules/core/core-bundle.js (generated, never hand-edited) and bumped the build v4.6.157 -> v4.6.158 (main.html x3 + module-loader.js). Verified: 35/35 node tests, including 6 new regression tests each proven red against the pre-fix source; an independent orchestrator harness 36/36; and newos4-maintain.py --fix all PASS after regenerating the stale changelog page. Served bytes are byte-identical for the bundle, main.html and both ext modules, and a live headless-Chromium run against v4.6.158 shows WpmModule.search('pdf') returning 1 result, the debug panel rendering a no-arg entry as "null", _base64ToBytes('grüße') raising a plain Error that names base64, and 0 JS errors on load. m.pull (RPT-20261102-211438) is closed as a usage error: no module in the tree defines pull(); the real interfaces are the webdav-pull and model-pull commands.
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);
+})();