diff --git a/packages/marshal/src/encodePassable.js b/packages/marshal/src/encodePassable.js index 2ed63f389a..3746972e00 100644 --- a/packages/marshal/src/encodePassable.js +++ b/packages/marshal/src/encodePassable.js @@ -267,7 +267,7 @@ const encodeCompactStringSuffix = str => * @type {(encoded: string) => string} */ const decodeCompactStringSuffix = encoded => { - return encoded.replace(/([!_])(.|\n)?/g, (esc, prefix, suffix) => { + return encoded.replace(/([\0-!_])(.|\n)?/g, (esc, prefix, suffix) => { switch (esc) { case '!_': return ' '; @@ -279,7 +279,7 @@ const decodeCompactStringSuffix = encoded => { return '_'; default: { const ch = /** @type {string} */ (suffix); - // The range of valid escapes is [(0x00+0x21)..(0x1F+0x21)], i.e. + // The range of valid `!`-escape suffixes is [(0x00+0x21)..(0x1F+0x21)], i.e. // [0x21..0x40] (U+0021 EXCLAMATION MARK to U+0040 COMMERCIAL AT). (prefix === '!' && suffix !== undefined && ch >= '!' && ch <= '@') || Fail`invalid string escape: ${q(esc)}`; diff --git a/packages/marshal/test/test-encodePassable.js b/packages/marshal/test/test-encodePassable.js index f36665a522..fae89f4070 100644 --- a/packages/marshal/test/test-encodePassable.js +++ b/packages/marshal/test/test-encodePassable.js @@ -261,6 +261,41 @@ test('capability encoding', t => { t.is(encodePassableCompact(decodeAsciiPassable(dataCompact)), dataCompact); }); +test('compact string validity', t => { + t.notThrows(() => decodePassableInternal('~sa"z')); + t.notThrows(() => decodePassableInternal('~sa!!z')); + const specialEscapes = ['!_', '!|', '_@', '__']; + for (const prefix of ['!', '_']) { + for (let cp = 0; cp <= 0x7f; cp += 1) { + const esc = `${prefix}${String.fromCodePoint(cp)}`; + const tryDecode = () => decodePassableInternal(`~sa${esc}z`); + if (esc.match(/![!-@]/) || specialEscapes.includes(esc)) { + t.notThrows(tryDecode, `valid string escape: ${JSON.stringify(esc)}`); + } else { + t.throws( + tryDecode, + { message: /invalid string escape/ }, + `invalid string escape: ${JSON.stringify(esc)}`, + ); + } + } + t.throws( + () => decodePassableInternal(`~sa${prefix}`), + { message: /invalid string escape/ }, + `unterminated ${JSON.stringify(prefix)} escape`, + ); + } + for (let cp = 0; cp < 0x20; cp += 1) { + const ch = String.fromCodePoint(cp); + const uCode = cp.toString(16).padStart(4, '0'); + t.throws( + () => decodePassableInternal(`~sa${ch}z`), + { message: /invalid string escape/ }, + `disallowed string control character: U+${uCode} ${JSON.stringify(ch)}`, + ); + } +}); + test('capability encoding validity constraints', t => { const r = Remotable(); let encoding;