diff options
| author | Hermes SysOp <sysop@webx11.duckdns.org> | 2026-11-09 23:58:35 +0100 |
|---|---|---|
| committer | Hermes SysOp <sysop@webx11.duckdns.org> | 2026-11-09 23:58:35 +0100 |
| commit | 9f6744dd5cb99b904f9a23fce6f2d98e8332bbf8 (patch) | |
| tree | d3f3a525e8594db010bbf0e2aa6e4c2590445339 /modules/ext/cryptogzip.js | |
| parent | 8e3a6155f6a5a1266ff1fa801c8864484bcdbda9 (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/ext/cryptogzip.js')
| -rw-r--r-- | modules/ext/cryptogzip.js | 42 |
1 files changed, 28 insertions, 14 deletions
diff --git a/modules/ext/cryptogzip.js b/modules/ext/cryptogzip.js index cc275df..25b6ca4 100644 --- a/modules/ext/cryptogzip.js +++ b/modules/ext/cryptogzip.js @@ -190,8 +190,31 @@ class CryptoGzipModule { for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]); return btoa(s); } + // Validate before atob(): atob() throws a raw DOMException (InvalidCharacterError / "characters + // outside of the Latin1 range") for arbitrary input, which escaped as an unhandledrejection when + // called from the node/JS console (RPT-20261102-203616-e2641bff). Everything atob() would reject + // is rejected here first, as a plain Error the command wrappers can print. + _base64InvalidReason(s) { + if (typeof s !== 'string') return 'expected a string, got ' + (s === null ? 'null' : typeof s); + const t = s.replace(/[\t\n\f\r ]/g, ''); // atob() ignores exactly these ASCII whitespace chars + if (t.length === 0) return null; // atob('') === '' — keep the empty case working + if (!/^[A-Za-z0-9+/]*={0,2}$/.test(t)) { + const bad = t.match(/[^A-Za-z0-9+/=]/); + if (bad) return 'illegal character ' + JSON.stringify(bad[0]) + ' — expected A-Z a-z 0-9 + / ='; + return 'misplaced or excessive "=" padding'; + } + const rem = t.length % 4; + if (rem === 1) return 'length ' + t.length + ' is not a valid base64 length'; + // Padding may be omitted (atob accepts that too), but when present the length must be a + // multiple of 4 — otherwise atob() throws "not correctly encoded". + if (t.indexOf('=') !== -1 && rem !== 0) return 'length ' + t.length + ' has incomplete "=" padding'; + return null; + } + _isBase64(s) { return this._base64InvalidReason(s) === null; } _base64ToBytes(b64) { - const s = atob(b64); + const reason = this._base64InvalidReason(b64); + if (reason) throw new Error('not valid base64 input (' + reason + ')'); + const s = atob(b64.replace(/[\t\n\f\r ]/g, '')); const u8 = new Uint8Array(s.length); for (let i = 0; i < s.length; i++) u8[i] = s.charCodeAt(i); return u8; @@ -582,19 +605,10 @@ class CryptoGzipModule { } // ============================================================ - // Base64 Helpers - // ============================================================ - - _bytesToBase64(bytes) { - const bin = Array.from(bytes, (b) => String.fromCharCode(b)).join(''); - return btoa(bin); - } - - _base64ToBytes(b64) { - const bin = atob(b64); - return Uint8Array.from(bin, (c) => c.charCodeAt(0)); - } - + // Base64 Helpers — defined once, above, next to the gzip code. + // (A second, later copy of _bytesToBase64/_base64ToBytes used to + // live here and silently shadow the first; it carried the unguarded + // atob() call. Do not re-add a duplicate.) // ============================================================ registerCommands() { |