From 7632803791dc6160c39c1af33963f52e0f2730ec Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 28 Mar 2025 17:36:06 -1000 Subject: [PATCH 01/31] wip(syrup): seeking and parser for lazy parsing --- packages/syrup/src/decode.js | 627 ++++++++++++++++++++++++++++-- packages/syrup/test/_zoo.bin | Bin 0 -> 290 bytes packages/syrup/test/parse.test.js | Bin 0 -> 8032 bytes 3 files changed, 605 insertions(+), 22 deletions(-) create mode 100644 packages/syrup/test/_zoo.bin create mode 100644 packages/syrup/test/parse.test.js diff --git a/packages/syrup/src/decode.js b/packages/syrup/src/decode.js index a11c659a8b..16e962b87f 100644 --- a/packages/syrup/src/decode.js +++ b/packages/syrup/src/decode.js @@ -11,13 +11,13 @@ const LIST_START = '['.charCodeAt(0); const LIST_END = ']'.charCodeAt(0); const DICT_START = '{'.charCodeAt(0); const DICT_END = '}'.charCodeAt(0); -// const SET_START = '#'.charCodeAt(0); -// const SET_END = '$'.charCodeAt(0); +const SET_START = '#'.charCodeAt(0); +const SET_END = '$'.charCodeAt(0); const BYTES_START = ':'.charCodeAt(0); const STRING_START = '"'.charCodeAt(0); -// const SYMBOL_START = "'".charCodeAt(0); -// const RECORD_START = '<'.charCodeAt(0); -// const RECORD_END = '>'.charCodeAt(0); +const SYMBOL_START = "'".charCodeAt(0); +const RECORD_START = '<'.charCodeAt(0); +const RECORD_END = '>'.charCodeAt(0); const TRUE = 't'.charCodeAt(0); const FALSE = 'f'.charCodeAt(0); // const SINGLE = 'F'.charCodeAt(0); @@ -122,15 +122,20 @@ function decodeAfterInteger(bytes, integer, start, end, name) { ); } +function seekEndOfInteger(bytes, start, end) { + let at = start + 1; + // eslint-disable-next-line no-empty + for (; at < end && bytes[at] >= ZERO && bytes[at] <= NINE; at += 1) {} + return at; +} + /** * @param {Uint8Array} bytes * @param {number} start * @param {number} end */ function decodeInteger(bytes, start, end) { - let at = start + 1; - // eslint-disable-next-line no-empty - for (; at < end && bytes[at] >= ZERO && bytes[at] <= NINE; at += 1) {} + const at = seekEndOfInteger(bytes, start, end); return { start: at, integer: BigInt(textDecoder.decode(bytes.subarray(start, at))), @@ -143,7 +148,7 @@ function decodeInteger(bytes, start, end) { * @param {number} end * @param {string} name */ -function decodeArray(bytes, start, end, name) { +function decodeList(bytes, start, end, name) { const list = []; for (;;) { if (start >= end) { @@ -165,13 +170,52 @@ function decodeArray(bytes, start, end, name) { } } +function seekList(bytes, start, end, name) { + for (;;) { + if (start >= end) { + throw Error( + `Unexpected end of Syrup, expected Syrup value or end of Syrup list marker "]" at index ${start} in ${name}`, + ); + } + const cc = bytes[start]; + if (cc === LIST_END) { + return { + start: start + 1, + }; + } + ({ start } = seekAny(bytes, start, end, name)); + } +} + +function seekSet(bytes, start, end, name) { + for (;;) { + if (start >= end) { + throw Error( + `Unexpected end of Syrup, expected Syrup value or end of Syrup set marker "$" at index ${start} in ${name}`, + ); + } + const cc = bytes[start]; + if (cc === SET_END) { + return { + start: start + 1, + }; + } + ({ start } = seekAny(bytes, start, end, name)); + } +} + +export function decodeSymbolName(bytes, start, end, name) { + const { value } = decodeStringlike(bytes, start, end, name); + return value; +} + /** * @param {Uint8Array} bytes * @param {number} start * @param {number} end * @param {string} name */ -function decodeString(bytes, start, end, name) { +function decodeStringlike(bytes, start, end, name) { if (start >= end) { throw Error( `Unexpected end of Syrup, expected Syrup string at end of ${name}`, @@ -181,7 +225,8 @@ function decodeString(bytes, start, end, name) { ({ start, integer: length } = decodeInteger(bytes, start, end)); const cc = bytes[start]; - if (cc !== STRING_START) { + if (cc !== STRING_START && cc !== SYMBOL_START) { + // TODO: error message implies this is only for dictionaries, but it's not throw Error( `Unexpected byte ${JSON.stringify( String.fromCharCode(cc), @@ -201,13 +246,90 @@ function decodeString(bytes, start, end, name) { return { start, value }; } +/** + * @param {Uint8Array} bytes + * @param {number} start + * @param {number} end + * @param {string} name + * @param {number} typeCode + * @param {string} typeName + */ +function seekStringOfType(bytes, start, end, name, typeCode, typeName) { + if (start >= end) { + throw Error( + `Unexpected end of Syrup, expected Syrup ${typeName} at end of ${name}`, + ); + } + let length; + ({ start, integer: length } = decodeInteger(bytes, start, end)); + + const cc = bytes[start]; + if (cc !== typeCode) { + throw Error( + `Unexpected character ${JSON.stringify(String.fromCharCode(cc))}, Syrup ${typeName} must start with ${JSON.stringify(String.fromCharCode(typeCode))}, got ${JSON.stringify(String.fromCharCode(cc))} at index ${start} of ${name}`, + ); + } + start += 1; + const subStart = start; + start += Number(length); + if (start > end) { + throw Error( + `Unexpected end of Syrup, expected ${length} bytes after index ${subStart} of ${name}`, + ); + } + return { start }; +} + +/** + * @param {Uint8Array} bytes + * @param {number} start + * @param {number} end + * @param {string} name + */ +function seekString(bytes, start, end, name) { + return seekStringOfType(bytes, start, end, name, STRING_START, 'string'); +} + +/** + * @param {Uint8Array} bytes + * @param {number} start + * @param {number} end + * @param {string} name + */ +function seekSymbol(bytes, start, end, name) { + return seekStringOfType(bytes, start, end, name, SYMBOL_START, 'symbol'); +} + /** * @param {Uint8Array} bytes * @param {number} start * @param {number} end * @param {string} name */ -function decodeRecord(bytes, start, end, name) { +function seekBytestring(bytes, start, end, name) { + return seekStringOfType(bytes, start, end, name, BYTES_START, 'bytestring'); +} + +function seekDictionaryKey(bytes, start, end, name) { + const typeInfo = peekType(bytes, start, end, name); + if (typeInfo.type === 'string') { + return seekString(bytes, start, end, name); + } + if (typeInfo.type === 'symbol') { + return seekSymbol(bytes, start, end, name); + } + throw Error( + `Unexpected type ${typeInfo.type}, Syrup dictionary keys must be strings or symbols at index ${start} of ${name}`, + ); +} + +/** + * @param {Uint8Array} bytes + * @param {number} start + * @param {number} end + * @param {string} name + */ +function decodeDictionary(bytes, start, end, name) { const record = {}; let priorKey = ''; let priorKeyStart = -1; @@ -227,7 +349,7 @@ function decodeRecord(bytes, start, end, name) { } const keyStart = start; let key; - ({ start, value: key } = decodeString(bytes, start, end, name)); + ({ start, value: key } = decodeStringlike(bytes, start, end, name)); const keyEnd = start; // Validate strictly non-descending keys. @@ -273,6 +395,62 @@ function decodeRecord(bytes, start, end, name) { } } +function seekDictionary(bytes, start, end, name) { + let priorKey = ''; + let priorKeyStart = -1; + let priorKeyEnd = -1; + for (;;) { + if (start >= end) { + throw Error( + `Unexpected end of Syrup, expected Syrup string or end of Syrup dictionary marker "}" at ${start} of ${name}`, + ); + } + const cc = bytes[start]; + if (cc === DICT_END) { + return { + start: start + 1, + }; + } + const keyStart = start; + let key; + console.log('seekDictionary next bytes', {start, end, name, string: textDecoder.decode(bytes.subarray(start, end))}); + ({ start, value: key } = decodeStringlike(bytes, start, end, name)); + const keyEnd = start; + + // Validate strictly non-descending keys. + if (priorKeyStart !== -1) { + const order = compareByteArrays( + bytes, + bytes, + priorKeyStart, + priorKeyEnd, + keyStart, + keyEnd, + ); + if (order === 0) { + throw Error( + `Syrup dictionary keys must be unique, got repeated ${JSON.stringify( + key, + )} at index ${start} of ${name}`, + ); + } else if (order > 0) { + throw Error( + `Syrup dictionary keys must be in bytewise sorted order, got ${JSON.stringify( + key, + )} immediately after ${JSON.stringify( + priorKey, + )} at index ${start} of ${name}`, + ); + } + } + priorKey = key; + priorKeyStart = keyStart; + priorKeyEnd = keyEnd; + + ({ start } = seekAny(bytes, start, end, name)); + } +} + /** * @param {Uint8Array} bytes * @param {number} start @@ -305,36 +483,108 @@ function decodeFloat64(bytes, start, end, name) { return { start, value }; } +function peekTypeWithNumberPrefix(bytes, start, end, name) { + const at = seekEndOfInteger(bytes, start, end); + const typePostfix = bytes[at]; + console.log('peekTypeWithNumberPrefix', {start, end, name, at, typePostfix, char: String.fromCharCode(typePostfix)}); + if (typePostfix === PLUS) { + return { type: 'integer', start: at + 1 }; + } + if (typePostfix === MINUS) { + return { type: 'integer', start: at + 1 }; + } + // TODO: these start values are not correct bc they dont include the actual string length (?) + // we need to clarify what the string parser/seeker wants + if (typePostfix === BYTES_START) { + const { start: next } = seekBytestring(bytes, start, end, name); + return { type: 'bytestring', start: next }; + } + if (typePostfix === STRING_START) { + const { start: next } = seekString(bytes, start, end, name); + return { type: 'string', start: next }; + } + if (typePostfix === SYMBOL_START) { + const { start: next } = seekSymbol(bytes, start, end, name); + return { type: 'symbol', start: next }; + } + throw Error( + `Unexpected character ${JSON.stringify( + String.fromCharCode(typePostfix), + )} at index ${start} of ${name}`, + ); +} + /** * @param {Uint8Array} bytes * @param {number} start * @param {number} end * @param {string} name - * @returns {{start: number, value: any}} + * @returns {{type: string, start: number}} */ -function decodeAny(bytes, start, end, name) { +export function peekType(bytes, start, end, name) { if (start >= end) { throw Error( `Unexpected end of Syrup, expected any value at index ${start} of ${name}`, ); } const cc = bytes[start]; + console.log('peekType', {start, end, name, cc, char: String.fromCharCode(cc)}); if (cc === DOUBLE) { - return decodeFloat64(bytes, start + 1, end, name); + return { type: 'float64', start: start + 1 }; } if (cc >= ONE && cc <= NINE) { - let integer; - ({ start, integer } = decodeInteger(bytes, start, end)); - return decodeAfterInteger(bytes, integer, start, end, name); + console.log('peekType number', {start, end, name, cc, char: String.fromCharCode(cc)}); + return peekTypeWithNumberPrefix(bytes, start, end, name); } if (cc === ZERO) { - return decodeAfterInteger(bytes, 0n, start + 1, end, name); + return { type: 'integer', start: start + 1 }; } if (cc === LIST_START) { - return decodeArray(bytes, start + 1, end, name); + return { type: 'list', start: start + 1 }; + } + if (cc === SET_START) { + return { type: 'set', start: start + 1 }; } if (cc === DICT_START) { - return decodeRecord(bytes, start + 1, end, name); + return { type: 'dictionary', start: start + 1 }; + } + if (cc === RECORD_START) { + return { type: 'record', start: start + 1 }; + } + if (cc === TRUE) { + return { type: 'boolean', start: start + 1 }; + } + if (cc === FALSE) { + return { type: 'boolean', start: start + 1 }; + } + throw Error( + `Unexpected character ${JSON.stringify( + String.fromCharCode(cc), + )} at index ${start} of ${name}`, + ); +} + +/** + * @param {Uint8Array} bytes + * @param {number} start + * @param {number} end + * @param {string} name + * @returns {{start: number, value: any}} + */ +function decodeAny(bytes, start, end, name) { + if (start >= end) { + throw Error( + `Unexpected end of Syrup, expected any value at index ${start} of ${name}`, + ); + } + const cc = bytes[start]; + if (cc >= ONE && cc <= NINE) { + let integer; + ({ start, integer } = decodeInteger(bytes, start, end)); + return decodeAfterInteger(bytes, integer, start, end, name); + } + if (cc === ZERO) { + return decodeAfterInteger(bytes, 0n, start + 1, end, name); } if (cc === TRUE) { return { start: start + 1, value: true }; @@ -342,6 +592,41 @@ function decodeAny(bytes, start, end, name) { if (cc === FALSE) { return { start: start + 1, value: false }; } + console.log('decodeAny', {start, end, name}); + const { type, start: next } = peekType(bytes, start, end, name); + console.log('decodeAny', {type, next}); + if (type === 'float64') { + return decodeFloat64(bytes, next, end, name); + } + if (type === 'string') { + return decodeStringlike(bytes, next, end, name); + } + if (type === 'bytestring') { + throw Error( + `decode Bytestrings are not yet supported.`, + ); + } + if (type === 'symbol') { + throw Error( + `decode Symbols are not yet supported.`, + ); + } + if (type === 'list') { + return decodeList(bytes, next, end, name); + } + if (type === 'set') { + throw Error( + `decode Sets are not yet supported.`, + ); + } + if (type === 'dictionary') { + return decodeDictionary(bytes, next, end, name); + } + if (type === 'record') { + throw Error( + `decode Records are not yet supported.`, + ); + } throw Error( `Unexpected character ${JSON.stringify( String.fromCharCode(cc), @@ -349,6 +634,32 @@ function decodeAny(bytes, start, end, name) { ); } +function seekAny(bytes, start, end, name) { + const { type, start: next } = peekType(bytes, start, end, name); + // String-likes operate on the start index + if (type === 'symbol') { + return seekSymbol(bytes, start, end, name); + } + if (type === 'bytestring') { + console.log('seekAny bytestring', {type, next}); + return seekBytestring(bytes, start, end, name); + } + // Non-string-likes operate on the next index + if (type === 'list') { + return seekList(bytes, next, end, name); + } + if (type === 'set') { + return seekSet(bytes, next, end, name); + } + if (type === 'dictionary') { + return seekDictionary(bytes, next, end, name); + } + console.log('seekAny fallback to decodeAny', {type, next}); + // TODO: We want to seek to the end of the value, not decode it. + // Decode any provided as a fallback. + return decodeAny(bytes, start, end, name); +} + /** * @param {Uint8Array} bytes * @param {object} options @@ -371,3 +682,275 @@ export function decodeSyrup(bytes, options = {}) { } return value; } + +class SyrupParser { + constructor(bytes, options) { + this.bytes = bytes; + this.state = { + start: options.start ?? 0, + end: options.end ?? bytes.byteLength, + name: options.name ?? '', + }; + } + next() { + const { start, end, name } = this.state; + console.log('next/decodeAny', {start, end, name}); + if (end > this.bytes.byteLength) { + throw Error( + `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, + ); + } + const { start: next, value } = decodeAny(this.bytes, start, end, name); + this.state.start = next; + return value; + } + skip() { + const { start, end, name } = this.state; + console.log('skip', {start, end, name}); + const { start: next } = seekAny(this.bytes, start, end, name); + // const { start: next2 } = decodeAny(this.bytes, next, end, name); + // if (next2 !== next) { + // throw Error( + // `Unexpected length mismatch, expected ${next2} bytes, got ${next}`, + // ); + // } + this.state.start = next; + } + peekType() { + const { start, end, name } = this.state; + // console.log('peekType', {start, end, name}); + if (end > this.bytes.byteLength) { + throw Error( + `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, + ); + } + const { type, start: next } = peekType(this.bytes, start, end, name); + return { type, start: next }; + } + nextType() { + const { start, end, name } = this.state; + const { type, start: next } = peekType(this.bytes, start, end, name); + this.state.start = next; + return { type, start: next }; + } + enterRecord() { + const { start, end, name } = this.state; + if (end > this.bytes.byteLength) { + throw Error( + `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, + ); + } + const cc = this.bytes[start]; + if (cc !== RECORD_START) { + throw Error( + `Unexpected character ${JSON.stringify( + String.fromCharCode(cc), + )} at index ${start} of ${name}`, + ); + } + this.state.start = start + 1; + } + exitRecord() { + const { start, end, name } = this.state; + if (end > this.bytes.byteLength) { + throw Error( + `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, + ); + } + const cc = this.bytes[start]; + if (cc !== RECORD_END) { + throw Error( + `Unexpected character ${JSON.stringify(String.fromCharCode(cc))}, Syrup records must end with "}", got ${JSON.stringify(String.fromCharCode(cc))} at index ${start} of ${name}`, + ); + } + this.state.start = start + 1; + } + nextRecordLabel() { + const { start, end, name } = this.state; + if (end > this.bytes.byteLength) { + throw Error( + `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, + ); + } + const { start: next, value } = decodeStringlike(this.bytes, start, end, name); + this.state.start = next; + return value; + } + enterDictionary() { + const { start, end, name } = this.state; + if (end > this.bytes.byteLength) { + throw Error( + `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, + ); + } + const cc = this.bytes[start]; + if (cc !== DICT_START) { + throw Error( + `Unexpected character ${JSON.stringify( + String.fromCharCode(cc), + )} at index ${start} of ${name}`, + ); + } + this.state.start = start + 1; + } + exitDictionary() { + const { start, end, name } = this.state; + if (end > this.bytes.byteLength) { + throw Error( + `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, + ); + } + const cc = this.bytes[start]; + if (cc !== DICT_END) { + throw Error( + `Unexpected character ${JSON.stringify( + String.fromCharCode(cc), + )} at index ${start} of ${name}`, + ); + } + this.state.start = start + 1; + } + nextDictionaryKey() { + const { start, end, name } = this.state; + if (end > this.bytes.byteLength) { + throw Error( + `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, + ); + } + const { start: next, value } = decodeStringlike(this.bytes, start, end, name); + this.state.start = next; + return value; + } + nextDictionaryValue() { + const { start, end, name } = this.state; + if (end > this.bytes.byteLength) { + throw Error( + `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, + ); + } + const { start: next, value } = decodeAny(this.bytes, start, end, name); + this.state.start = next; + return value; + } + *iterateDictionaryEntries() { + const { start, end, name } = this.state; + if (end > this.bytes.byteLength) { + throw Error( + `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, + ); + } + let next = start; + while (next < end) { + const cc = this.bytes[next]; + if (cc === DICT_END) { + break; + } + const key = this.nextDictionaryKey(); + const value = this.nextDictionaryValue(); + yield { key, value }; + } + } + *seekDictionaryEntries() { + const { start, end, name } = this.state; + if (end > this.bytes.byteLength) { + throw Error( + `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, + ); + } + let next = start; + while (next < end) { + const cc = this.bytes[next]; + if (cc === DICT_END) { + this.state.start = next + 1; + break; + } + const { start: afterKey } = seekDictionaryKey(this.bytes, next, end, name); + const { start: afterValue } = seekAny(this.bytes, afterKey, end, name); + yield { key: next, value: afterKey, start: afterValue }; + next = afterValue; + this.state.start = next; + } + } + enterList() { + const { start, end, name } = this.state; + if (end > this.bytes.byteLength) { + throw Error( + `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, + ); + } + const cc = this.bytes[start]; + if (cc !== LIST_START) { + throw Error( + `Unexpected character ${JSON.stringify(String.fromCharCode(cc))}, Syrup lists must start with "[", got ${JSON.stringify(String.fromCharCode(cc))} at index ${start} of ${name}`, + ); + } + this.state.start = start + 1; + } + exitList() { + const { start, end, name } = this.state; + if (end > this.bytes.byteLength) { + throw Error( + `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, + ); + } + const cc = this.bytes[start]; + if (cc !== LIST_END) { + throw Error( + `Unexpected character ${JSON.stringify(String.fromCharCode(cc))}, Syrup lists must end with "]", got ${JSON.stringify(String.fromCharCode(cc))} at index ${start} of ${name}`, + ); + } + this.state.start = start + 1; + } + nextListValue() { + const { start, end, name } = this.state; + const { start: next, value } = decodeAny(this.bytes, start, end, name); + this.state.start = next; + return value; + } + enterSet() { + const { start, end, name } = this.state; + if (end > this.bytes.byteLength) { + throw Error( + `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, + ); + } + const cc = this.bytes[start]; + if (cc !== SET_START) { + throw Error( + `Unexpected character ${JSON.stringify(String.fromCharCode(cc))}, Syrup sets must start with "#", got ${JSON.stringify(String.fromCharCode(cc))} at index ${start} of ${name}`, + ); + } + this.state.start = start + 1; + } + exitSet() { + const { start, end, name } = this.state; + if (end > this.bytes.byteLength) { + throw Error( + `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, + ); + } + const cc = this.bytes[start]; + if (cc !== SET_END) { + throw Error( + `Unexpected character ${JSON.stringify(String.fromCharCode(cc))}, Syrup sets must end with "$", got ${JSON.stringify(String.fromCharCode(cc))} at index ${start} of ${name}`, + ); + } + this.state.start = start + 1; + } + nextSetValue() { + const { start, end, name } = this.state; + const { start: next, value } = decodeAny(this.bytes, start, end, name); + this.state.start = next; + return value; + } +} + +export function parseSyrup(bytes, options = {}) { + const { start = 0, end = bytes.byteLength, name = '' } = options; + if (end > bytes.byteLength) { + throw Error( + `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${bytes.byteLength}`, + ); + } + return new SyrupParser(bytes, options); +} diff --git a/packages/syrup/test/_zoo.bin b/packages/syrup/test/_zoo.bin new file mode 100644 index 0000000000000000000000000000000000000000..7c17ef95b4367a9842f3abe02512e1dc5302af9f GIT binary patch literal 290 zcmZY3%?`mp6a`>wPtlZQ7GhDYY25e?iItt5SlrfaXF_MBGa-nl@gm;B>nN%g&e@#R zm(%y*thLv3mx7#mj%wp9NMTC3q}9b|#qv6>fDu~L2q2M01dz#?IBaDU?Wh?rg&BqJ z5+k$%Im4Yi(8zLjB$*038#~E6Av1f5B^e)LwAT;Lzjg=_s-h+YsSL%|lrnecr1O-% vaPot_!T$EB4=R{Lrqz~CrR{avZFAc;628y=6&SZOq*9rRULwbet!q=K{m@+Rrt$SlCX)dvNMa+ADhN4NJ^pE( z{-pa0_bdPcltNmbI@MHpkq9gn&px}@1;9>+9i_`vE)`RvP;4smB@6g94_=h*PZ%%Q z6n}NswD3JEuI9Fxe)^7EHW5ia5pOojIA8q8m!i#9To!1M>lJ~JdffuGeUEvR>i2(nWbvJj-w=BcJhR;vQ9xtJ4y=C>GisUB#uv$ zr$Rh^-W{HuO($YH9EeGr^ag_gpZ4OoJLz@E*pmrY{3yvY*!}+fB$XL}vI`(p^T5`_t>tkQ6P2;?Y_zS`kUnfYuAE0$^VP>jk)qq~Me9(gp0zl2GfDqR1E5B6JhA zS@7Q47qp;+>?KrTkYraSA^>8>_Kg;qyK{&`dsRt6U7c_btkou2`i4e!r~ zjaG@qWnwH6H>vW@BdYb`sfNGMHljg%^Zy7m*U)%4^!jG?qSDXlR@j}%hwsU3*M z`mvt!5xYJAAU56c`88kQfBf$0-@Wmj1^GHH6sCE5o+`0~2z^0@zWU>LR_G@bVmTpV z&KEoui&7u78h!mSyb`LpVEGzLo1Kr@6i!72TNJ1jOwDs=nV|W;sGaXi6tE{p=g2Yr z5y~3AyJyE&(E7dP*@XQ|WLV-d73g@2e+;$DP}8TiZ-1j^*@yU?{pON41Y??89N=NSs@a~1!J{F4UfMUxpYMC*fedjse7^d z-ygr@??r=I!_hL&5PP<}*|Ki$#L6_dt^xM996U;gZ_5=;uz~lRum3@x2Gq|+vw2>W zt}~$DL3Vufq|;pg8#h_^)nBvg)2Xv8U5_ZhQm@wgi?3PHruvx}L;BTg8@bgbw3eo( ztnt2(Ztn;7?g+d>4{B(41a41Id`MRY4`aYGO(k~JNH8eip-Ha>O-WEqAuWgRZcwx+ z_6IeY#MCQ2BNj-GBx9#sATNF<^GTIAymz*1x!w&?9xm~hi+-1eiQ6$5WNhzNjZ(=j z8x&c4wrl;T4x{r=?VgSFmuM?)!&-;a^tr7>`Y^0}(HfDi^y23EOGB#~+;9gc><;DI!32YDfEj1%tt%bcWkDcFf7X!On*)H+88(i`ET zowWKG`qWuIdw7366zyKTUb%<;tURp{GWyu<>~2(5U2U_5S9d6^r(2G{AFGO{E9J*6UUa zLJc;Al=JB)wFzW{MWZ%WnbZ?wgDH-xuhGNM=~ z8FTqw^3b9}V+eFw9EF;nlG4!i+p)UBy3yi>qC!mEV6$o0?@#c$N#C6Y#sIo@! Date: Fri, 28 Mar 2025 21:04:16 -1000 Subject: [PATCH 02/31] wip(syrup): support symbol decoding --- packages/syrup/src/decode.js | 34 ++++++++++++++++++++++++----- packages/syrup/src/symbol.js | 3 +++ packages/syrup/test/_table.js | 11 ++++++++-- packages/syrup/test/decode.test.js | 2 +- packages/syrup/test/encode.test.js | 2 +- packages/syrup/test/parse.test.js | Bin 8032 -> 8027 bytes 6 files changed, 42 insertions(+), 10 deletions(-) create mode 100644 packages/syrup/src/symbol.js diff --git a/packages/syrup/src/decode.js b/packages/syrup/src/decode.js index 16e962b87f..bcb27af8b2 100644 --- a/packages/syrup/src/decode.js +++ b/packages/syrup/src/decode.js @@ -1,6 +1,7 @@ // @ts-check import { compareByteArrays } from './compare.js'; +import { SyrupSymbolFor } from './symbol.js'; const MINUS = '-'.charCodeAt(0); const PLUS = '+'.charCodeAt(0); @@ -115,6 +116,18 @@ function decodeAfterInteger(bytes, integer, start, end, name) { const value = textDecoder.decode(bytes.subarray(subStart, start)); return { start, value }; } + if (cc === SYMBOL_START) { + start += 1; + const subStart = start; + start += Number(integer); + if (start > end) { + throw Error( + `Unexpected end of Syrup, expected ${integer} bytes after symbol starting at index ${subStart} in ${name}`, + ); + } + const value = textDecoder.decode(bytes.subarray(subStart, start)); + return { start, value: SyrupSymbolFor(value) }; + } throw Error( `Unexpected character ${JSON.stringify( String.fromCharCode(cc), @@ -242,10 +255,21 @@ function decodeStringlike(bytes, start, end, name) { `Unexpected end of Syrup, expected ${length} bytes after index ${subStart} of ${name}`, ); } - const value = textDecoder.decode(bytes.subarray(subStart, start)); + const stringValue = textDecoder.decode(bytes.subarray(subStart, start)); + let value; + if (cc === SYMBOL_START) { + value = SyrupSymbolFor(stringValue); + } else { + value = stringValue; + } + return { start, value }; } +function decodeSymbol(bytes, start, end, name) { + return decodeStringlike(bytes, start, end, name); +} + /** * @param {Uint8Array} bytes * @param {number} start @@ -331,7 +355,7 @@ function seekDictionaryKey(bytes, start, end, name) { */ function decodeDictionary(bytes, start, end, name) { const record = {}; - let priorKey = ''; + let priorKey = undefined; let priorKeyStart = -1; let priorKeyEnd = -1; for (;;) { @@ -396,7 +420,7 @@ function decodeDictionary(bytes, start, end, name) { } function seekDictionary(bytes, start, end, name) { - let priorKey = ''; + let priorKey = undefined; let priorKeyStart = -1; let priorKeyEnd = -1; for (;;) { @@ -607,9 +631,7 @@ function decodeAny(bytes, start, end, name) { ); } if (type === 'symbol') { - throw Error( - `decode Symbols are not yet supported.`, - ); + return decodeSymbol(bytes, next, end, name); } if (type === 'list') { return decodeList(bytes, next, end, name); diff --git a/packages/syrup/src/symbol.js b/packages/syrup/src/symbol.js new file mode 100644 index 0000000000..2df85d2258 --- /dev/null +++ b/packages/syrup/src/symbol.js @@ -0,0 +1,3 @@ +// To be used as keys, syrup symbols must be javascript symbols. +// To avoid an otherwise meaningful symbol name, we prefix it with 'syrup:'. +export const SyrupSymbolFor = (name) => Symbol.for(`syrup:${name}`); diff --git a/packages/syrup/test/_table.js b/packages/syrup/test/_table.js index 8dcff53450..3cf5b3b6db 100644 --- a/packages/syrup/test/_table.js +++ b/packages/syrup/test/_table.js @@ -1,3 +1,5 @@ +import { SyrupSymbolFor } from '../src/symbol.js'; + const textEncoder = new TextEncoder(); export const table = [ @@ -7,13 +9,18 @@ export const table = [ { syrup: 't', value: true }, { syrup: 'f', value: false }, { syrup: '5"hello', value: 'hello' }, + { syrup: '5\'hello', value: SyrupSymbolFor('hello') }, { syrup: '5:hello', value: textEncoder.encode('hello') }, { syrup: '[1+2+3+]', value: [1n, 2n, 3n] }, { syrup: '[3"abc3"def]', value: ['abc', 'def'] }, { syrup: '{1"a10+1"b20+}', value: { a: 10n, b: 20n } }, - { syrup: '{1"a10+1"b20+}', value: { b: 20n, a: 10n } }, // order canonicalization + { syrup: '{1"a10+1"b20+}', value: { b: 20n, a: 10n } }, + // order canonicalization { syrup: '{0"10+1"i20+}', value: { '': 10n, i: 20n } }, - { syrup: '{0"10+1"i20+}', value: { i: 20n, '': 10n } }, // order canonicalization + // order canonicalization + { syrup: '{0"10+1"i20+}', value: { i: 20n, '': 10n } }, + // dictionary with mixed string and symbol keys + { syrup: '{3"dog20+3\'cat10+}', value: { dog: 20n, [SyrupSymbolFor('cat')]: 10n } }, { syrup: 'D?\xf0\x00\x00\x00\x00\x00\x00', value: 1 }, { syrup: 'D@^\xdd/\x1a\x9f\xbew', value: 123.456 }, { syrup: '[3"foo123+t]', value: ['foo', 123n, true] }, diff --git a/packages/syrup/test/decode.test.js b/packages/syrup/test/decode.test.js index 922f349d54..4482cba898 100644 --- a/packages/syrup/test/decode.test.js +++ b/packages/syrup/test/decode.test.js @@ -13,7 +13,7 @@ test('affirmative decode cases', t => { bytes[i] = syrup.charCodeAt(i); } const actual = decodeSyrup(bytes); - t.deepEqual(actual, value, `for ${JSON.stringify(syrup)}`); + t.deepEqual(actual, value, `for ${String(syrup)}`); } }); diff --git a/packages/syrup/test/encode.test.js b/packages/syrup/test/encode.test.js index 79d550cdb5..5d28235963 100644 --- a/packages/syrup/test/encode.test.js +++ b/packages/syrup/test/encode.test.js @@ -13,7 +13,7 @@ test('affirmative encode cases', t => { for (const cc of Array.from(actual)) { string += String.fromCharCode(cc); } - t.deepEqual(syrup, string, `for ${JSON.stringify(syrup)} ${value}`); + t.deepEqual(syrup, string, `for ${JSON.stringify(syrup)} ${String(value)}`); } }); diff --git a/packages/syrup/test/parse.test.js b/packages/syrup/test/parse.test.js index 65df80f4fc8d5e301fa0ae155a8a2c16ae85831d..847f4c19594403156c9d3b489e6b639e2577abe6 100644 GIT binary patch delta 12 UcmaE0ciV2m6p76R delta 18 acmca@_rPw$6bV+n{JfmX%~K?fvjG53O9!<8 From 74c153a502a0db5696a58e56c7519a1790c5c9c4 Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 31 Mar 2025 14:42:11 -1000 Subject: [PATCH 03/31] feat: improve symbol and dictionary serde --- packages/syrup/src/decode.js | 67 +++++++++++++++++----------------- packages/syrup/src/encode.js | 69 +++++++++++++++++++++++++++--------- packages/syrup/src/symbol.js | 19 +++++++++- 3 files changed, 104 insertions(+), 51 deletions(-) diff --git a/packages/syrup/src/decode.js b/packages/syrup/src/decode.js index bcb27af8b2..146ff864ab 100644 --- a/packages/syrup/src/decode.js +++ b/packages/syrup/src/decode.js @@ -237,12 +237,12 @@ function decodeStringlike(bytes, start, end, name) { let length; ({ start, integer: length } = decodeInteger(bytes, start, end)); - const cc = bytes[start]; - if (cc !== STRING_START && cc !== SYMBOL_START) { + const typeCode = bytes[start]; + if (typeCode !== STRING_START && typeCode !== SYMBOL_START) { // TODO: error message implies this is only for dictionaries, but it's not throw Error( `Unexpected byte ${JSON.stringify( - String.fromCharCode(cc), + String.fromCharCode(typeCode), )}, Syrup dictionary keys must be strings or symbols at index ${start} of ${name}`, ); } @@ -255,19 +255,35 @@ function decodeStringlike(bytes, start, end, name) { `Unexpected end of Syrup, expected ${length} bytes after index ${subStart} of ${name}`, ); } - const stringValue = textDecoder.decode(bytes.subarray(subStart, start)); - let value; - if (cc === SYMBOL_START) { - value = SyrupSymbolFor(stringValue); - } else { - value = stringValue; + const value = textDecoder.decode(bytes.subarray(subStart, start)); + return { start, value, typeCode }; +} + +function decodeSymbol(bytes, start, end, name) { + const { start: next, value, typeCode } = decodeStringlike(bytes, start, end, name); + if (typeCode === SYMBOL_START) { + return { start: next, value: SyrupSymbolFor(value) }; } + throw Error(`Unexpected type ${typeCode}, Syrup symbols must start with ${SYMBOL_START} at index ${start} of ${name}`); +} - return { start, value }; +function decodeString(bytes, start, end, name) { + const { start: next, value, typeCode } = decodeStringlike(bytes, start, end, name); + if (typeCode === STRING_START) { + return { start: next, value }; + } + throw Error(`Unexpected type ${typeCode}, Syrup strings must start with ${STRING_START} at index ${start} of ${name}`); } -function decodeSymbol(bytes, start, end, name) { - return decodeStringlike(bytes, start, end, name); +function decodeDictionaryKey(bytes, start, end, name) { + const { start: next, value, typeCode } = decodeStringlike(bytes, start, end, name); + if (typeCode === SYMBOL_START) { + return { start: next, value: SyrupSymbolFor(value), typeCode }; + } + if (typeCode === STRING_START) { + return { start: next, value, typeCode }; + } + throw Error(`Unexpected type ${typeCode}, Syrup dictionary keys must be strings or symbols at index ${start} of ${name}`); } /** @@ -373,7 +389,7 @@ function decodeDictionary(bytes, start, end, name) { } const keyStart = start; let key; - ({ start, value: key } = decodeStringlike(bytes, start, end, name)); + ({ start, value: key } = decodeDictionaryKey(bytes, start, end, name)); const keyEnd = start; // Validate strictly non-descending keys. @@ -437,8 +453,7 @@ function seekDictionary(bytes, start, end, name) { } const keyStart = start; let key; - console.log('seekDictionary next bytes', {start, end, name, string: textDecoder.decode(bytes.subarray(start, end))}); - ({ start, value: key } = decodeStringlike(bytes, start, end, name)); + ({ start, value: key } = decodeDictionaryKey(bytes, start, end, name)); const keyEnd = start; // Validate strictly non-descending keys. @@ -510,7 +525,6 @@ function decodeFloat64(bytes, start, end, name) { function peekTypeWithNumberPrefix(bytes, start, end, name) { const at = seekEndOfInteger(bytes, start, end); const typePostfix = bytes[at]; - console.log('peekTypeWithNumberPrefix', {start, end, name, at, typePostfix, char: String.fromCharCode(typePostfix)}); if (typePostfix === PLUS) { return { type: 'integer', start: at + 1 }; } @@ -552,12 +566,10 @@ export function peekType(bytes, start, end, name) { ); } const cc = bytes[start]; - console.log('peekType', {start, end, name, cc, char: String.fromCharCode(cc)}); if (cc === DOUBLE) { return { type: 'float64', start: start + 1 }; } if (cc >= ONE && cc <= NINE) { - console.log('peekType number', {start, end, name, cc, char: String.fromCharCode(cc)}); return peekTypeWithNumberPrefix(bytes, start, end, name); } if (cc === ZERO) { @@ -616,14 +628,12 @@ function decodeAny(bytes, start, end, name) { if (cc === FALSE) { return { start: start + 1, value: false }; } - console.log('decodeAny', {start, end, name}); const { type, start: next } = peekType(bytes, start, end, name); - console.log('decodeAny', {type, next}); if (type === 'float64') { return decodeFloat64(bytes, next, end, name); } if (type === 'string') { - return decodeStringlike(bytes, next, end, name); + return decodeString(bytes, next, end, name); } if (type === 'bytestring') { throw Error( @@ -663,7 +673,6 @@ function seekAny(bytes, start, end, name) { return seekSymbol(bytes, start, end, name); } if (type === 'bytestring') { - console.log('seekAny bytestring', {type, next}); return seekBytestring(bytes, start, end, name); } // Non-string-likes operate on the next index @@ -676,7 +685,6 @@ function seekAny(bytes, start, end, name) { if (type === 'dictionary') { return seekDictionary(bytes, next, end, name); } - console.log('seekAny fallback to decodeAny', {type, next}); // TODO: We want to seek to the end of the value, not decode it. // Decode any provided as a fallback. return decodeAny(bytes, start, end, name); @@ -716,7 +724,6 @@ class SyrupParser { } next() { const { start, end, name } = this.state; - console.log('next/decodeAny', {start, end, name}); if (end > this.bytes.byteLength) { throw Error( `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, @@ -728,19 +735,11 @@ class SyrupParser { } skip() { const { start, end, name } = this.state; - console.log('skip', {start, end, name}); const { start: next } = seekAny(this.bytes, start, end, name); - // const { start: next2 } = decodeAny(this.bytes, next, end, name); - // if (next2 !== next) { - // throw Error( - // `Unexpected length mismatch, expected ${next2} bytes, got ${next}`, - // ); - // } this.state.start = next; } peekType() { const { start, end, name } = this.state; - // console.log('peekType', {start, end, name}); if (end > this.bytes.byteLength) { throw Error( `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, @@ -794,7 +793,7 @@ class SyrupParser { `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, ); } - const { start: next, value } = decodeStringlike(this.bytes, start, end, name); + const { start: next, value } = decodeSymbol(this.bytes, start, end, name); this.state.start = next; return value; } @@ -839,7 +838,7 @@ class SyrupParser { `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, ); } - const { start: next, value } = decodeStringlike(this.bytes, start, end, name); + const { start: next, value } = decodeDictionaryKey(this.bytes, start, end, name); this.state.start = next; return value; } diff --git a/packages/syrup/src/encode.js b/packages/syrup/src/encode.js index ee69cdff73..56c6ba9c28 100644 --- a/packages/syrup/src/encode.js +++ b/packages/syrup/src/encode.js @@ -1,8 +1,10 @@ // @ts-check import { compareByteArrays } from './compare.js'; +import { getSyrupSymbolName } from './symbol.js'; -const { freeze, keys } = Object; +const { freeze } = Object; +const { ownKeys } = Reflect; const defaultCapacity = 256; @@ -54,8 +56,9 @@ function grow(buffer, increaseBy) { /** * @param {Buffer} buffer * @param {string} value + * @param {string} typeChar */ -function encodeString(buffer, value) { +function encodeStringlike(buffer, value, typeChar) { const stringLength = value.length; const likelyPrefixLength = `${stringLength}`.length + 1; // buffer.length will be incorrect until we fix it before returning. @@ -70,7 +73,7 @@ function encodeString(buffer, value) { written += chunk.written || 0; read += chunk.read || 0; if (read === stringLength) { - const prefix = `${written}"`; // length prefix quote suffix + const prefix = `${written}${typeChar}`; // length prefix, typeChar suffix const prefixLength = prefix.length; buffer.length = start; grow(buffer, prefixLength + written); @@ -90,21 +93,50 @@ function encodeString(buffer, value) { /** * @param {Buffer} buffer - * @param {Record} record - * @param {Array} path + * @param {string} value + */ +function encodeString(buffer, value) { + encodeStringlike(buffer, value, '"'); +} + +/** + * @param {Buffer} buffer + * @param {string} value + */ +function encodeSymbol(buffer, value) { + encodeStringlike(buffer, value, '\''); +} + +/** + * @param {Buffer} buffer + * @param {Record} record + * @param {Array} path */ -function encodeRecord(buffer, record, path) { +function encodeDictionary(buffer, record, path) { const restart = buffer.length; const indexes = []; - const keyStrings = []; + const keys = []; const keyBytes = []; - for (const key of keys(record)) { + const encodeKey = (key) => { + if (typeof key === 'string') { + encodeString(buffer, key); + return; + } + if (typeof key === 'symbol') { + const syrupSymbol = getSyrupSymbolName(key); + encodeSymbol(buffer, syrupSymbol); + return; + } + throw TypeError(`Dictionary keys must be strings or symbols, got ${typeof key} at ${path.join('/')}`); + }; + + for (const key of ownKeys(record)) { const start = buffer.length; - encodeString(buffer, key); + encodeKey(key); const end = buffer.length; - keyStrings.push(key); + keys.push(key); keyBytes.push(buffer.bytes.subarray(start, end)); indexes.push(indexes.length); } @@ -126,10 +158,10 @@ function encodeRecord(buffer, record, path) { buffer.bytes[cursor] = DICT_START; for (const index of indexes) { - const key = keyStrings[index]; + const key = keys[index]; const value = record[key]; - encodeString(buffer, key); + encodeKey(key); // Recursion, it's a thing! // eslint-disable-next-line no-use-before-define encodeAny(buffer, value, path, key); @@ -142,7 +174,7 @@ function encodeRecord(buffer, record, path) { /** * @param {Buffer} buffer * @param {Array} array - * @param {Array} path + * @param {Array} path */ function encodeArray(buffer, array, path) { let cursor = grow(buffer, 2 + array.length); @@ -164,10 +196,15 @@ function encodeArray(buffer, array, path) { /** * @param {Buffer} buffer * @param {any} value - * @param {Array} path - * @param {string | number} pathSuffix + * @param {Array} path + * @param {string | symbol | number} pathSuffix */ function encodeAny(buffer, value, path, pathSuffix) { + if (typeof value === 'symbol') { + encodeSymbol(buffer, getSyrupSymbolName(value)); + return; + } + if (typeof value === 'string') { encodeString(buffer, value); return; @@ -211,7 +248,7 @@ function encodeAny(buffer, value, path, pathSuffix) { if (Object(value) === value) { path.push(pathSuffix); - encodeRecord(buffer, value, path); + encodeDictionary(buffer, value, path); path.pop(); return; } diff --git a/packages/syrup/src/symbol.js b/packages/syrup/src/symbol.js index 2df85d2258..8265f092db 100644 --- a/packages/syrup/src/symbol.js +++ b/packages/syrup/src/symbol.js @@ -1,3 +1,20 @@ +export const SYRUP_SYMBOL_PREFIX = 'syrup:'; + // To be used as keys, syrup symbols must be javascript symbols. // To avoid an otherwise meaningful symbol name, we prefix it with 'syrup:'. -export const SyrupSymbolFor = (name) => Symbol.for(`syrup:${name}`); +export const SyrupSymbolFor = (name) => Symbol.for(`${SYRUP_SYMBOL_PREFIX}${name}`); + +/** + * @param {symbol} symbol + * @returns {string} + */ +export const getSyrupSymbolName = (symbol) => { + const description = symbol.description; + if (!description) { + throw TypeError(`Symbol ${String(symbol)} has no description`); + } + if (!description.startsWith(SYRUP_SYMBOL_PREFIX)) { + throw TypeError(`Symbol ${String(symbol)} has a description that does not start with "${SYRUP_SYMBOL_PREFIX}", got "${description}"`); + } + return description.slice(SYRUP_SYMBOL_PREFIX.length); +} From 91a284350946b149359400d5f2ff80c3082e582e Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 31 Mar 2025 22:30:24 -1000 Subject: [PATCH 04/31] wip: improve decoding and error messages --- packages/syrup/src/decode.js | 102 ++++++++++++++++++++++------------- 1 file changed, 65 insertions(+), 37 deletions(-) diff --git a/packages/syrup/src/decode.js b/packages/syrup/src/decode.js index 146ff864ab..81c5f3dc59 100644 --- a/packages/syrup/src/decode.js +++ b/packages/syrup/src/decode.js @@ -32,6 +32,9 @@ const scratchData = new DataView(scratch); const { defineProperty, freeze } = Object; +const quote = (o) => JSON.stringify(o); +const toChar = (code) => String.fromCharCode(code); + /** * @param {Uint8Array} bytes */ @@ -147,7 +150,7 @@ function seekEndOfInteger(bytes, start, end) { * @param {number} start * @param {number} end */ -function decodeInteger(bytes, start, end) { +function decodeIntegerPrefix(bytes, start, end) { const at = seekEndOfInteger(bytes, start, end); return { start: at, @@ -155,6 +158,29 @@ function decodeInteger(bytes, start, end) { }; } +function decodeInteger(bytes, start, end, name) { + const cc = bytes[start]; + if (cc >= ONE && cc <= NINE) { + const { start: next, integer } = decodeIntegerPrefix(bytes, start, end); + return decodeAfterInteger(bytes, integer, next, end, name); + } + if (cc === ZERO) { + return decodeAfterInteger(bytes, 0n, start + 1, end, name); + } + throw Error(`Unexpected byte ${quote(toChar(cc))}, Syrup integers must start with ${quote(toChar(ZERO))} or a digit at index ${start} of ${name}`); +} + +function decodeBoolean(bytes, start, end, name) { + const cc = bytes[start]; + if (cc === TRUE) { + return { start: start + 1, value: true }; + } + if (cc === FALSE) { + return { start: start + 1, value: false }; + } + throw Error(`Unexpected byte ${quote(toChar(cc))}, Syrup booleans must start with ${quote(toChar(TRUE))} or ${quote(toChar(FALSE))} at index ${start} of ${name}`); +} + /** * @param {Uint8Array} bytes * @param {number} start @@ -235,15 +261,15 @@ function decodeStringlike(bytes, start, end, name) { ); } let length; - ({ start, integer: length } = decodeInteger(bytes, start, end)); + ({ start, integer: length } = decodeIntegerPrefix(bytes, start, end)); const typeCode = bytes[start]; - if (typeCode !== STRING_START && typeCode !== SYMBOL_START) { + if (typeCode !== STRING_START && typeCode !== SYMBOL_START && typeCode !== BYTES_START) { // TODO: error message implies this is only for dictionaries, but it's not throw Error( `Unexpected byte ${JSON.stringify( String.fromCharCode(typeCode), - )}, Syrup dictionary keys must be strings or symbols at index ${start} of ${name}`, + )}, Syrup string-likes must be strings, symbols, or bytestrings at index ${start} of ${name}`, ); } start += 1; @@ -264,7 +290,7 @@ function decodeSymbol(bytes, start, end, name) { if (typeCode === SYMBOL_START) { return { start: next, value: SyrupSymbolFor(value) }; } - throw Error(`Unexpected type ${typeCode}, Syrup symbols must start with ${SYMBOL_START} at index ${start} of ${name}`); + throw Error(`Unexpected type ${quote(toChar(typeCode))}, Syrup symbols must start with ${quote(toChar(SYMBOL_START))} at index ${start} of ${name}`); } function decodeString(bytes, start, end, name) { @@ -272,18 +298,30 @@ function decodeString(bytes, start, end, name) { if (typeCode === STRING_START) { return { start: next, value }; } - throw Error(`Unexpected type ${typeCode}, Syrup strings must start with ${STRING_START} at index ${start} of ${name}`); + throw Error(`Unexpected type ${quote(toChar(typeCode))}, Syrup strings must start with ${quote(toChar(STRING_START))} at index ${start} of ${name}`); } -function decodeDictionaryKey(bytes, start, end, name) { +function decodeBytestring(bytes, start, end, name) { const { start: next, value, typeCode } = decodeStringlike(bytes, start, end, name); - if (typeCode === SYMBOL_START) { - return { start: next, value: SyrupSymbolFor(value), typeCode }; + if (typeCode === BYTES_START) { + return { start: next, value }; } - if (typeCode === STRING_START) { - return { start: next, value, typeCode }; + throw Error(`Unexpected byte ${quote(toChar(typeCode))}, Syrup bytestrings must start with ${quote(toChar(BYTES_START))} at index ${start} of ${name}`); +} + +function decodeDictionaryKey(bytes, start, end, name) { + const { start: integerPostixIndex } = decodeIntegerPrefix(bytes, start, end); + const typeCode = bytes[integerPostixIndex]; + if (typeCode === SYMBOL_START || typeCode === STRING_START) { + const { start: next, value } = decodeStringlike(bytes, start, end, name); + if (typeCode === SYMBOL_START) { + return { start: next, value: SyrupSymbolFor(value), typeCode }; + } + if (typeCode === STRING_START) { + return { start: next, value, typeCode }; + } } - throw Error(`Unexpected type ${typeCode}, Syrup dictionary keys must be strings or symbols at index ${start} of ${name}`); + throw Error(`Unexpected byte ${JSON.stringify(String.fromCharCode(typeCode))}, Syrup dictionary keys must be strings or symbols at index ${integerPostixIndex} of ${name}`); } /** @@ -301,12 +339,12 @@ function seekStringOfType(bytes, start, end, name, typeCode, typeName) { ); } let length; - ({ start, integer: length } = decodeInteger(bytes, start, end)); + ({ start, integer: length } = decodeIntegerPrefix(bytes, start, end)); const cc = bytes[start]; if (cc !== typeCode) { throw Error( - `Unexpected character ${JSON.stringify(String.fromCharCode(cc))}, Syrup ${typeName} must start with ${JSON.stringify(String.fromCharCode(typeCode))}, got ${JSON.stringify(String.fromCharCode(cc))} at index ${start} of ${name}`, + `Unexpected character ${quote(toChar(cc))}, Syrup ${typeName} must start with ${JSON.stringify(String.fromCharCode(typeCode))}, got ${quote(toChar(cc))} at index ${start} of ${name}`, ); } start += 1; @@ -614,19 +652,11 @@ function decodeAny(bytes, start, end, name) { ); } const cc = bytes[start]; - if (cc >= ONE && cc <= NINE) { - let integer; - ({ start, integer } = decodeInteger(bytes, start, end)); - return decodeAfterInteger(bytes, integer, start, end, name); + if (cc >= ZERO && cc <= NINE) { + return decodeInteger(bytes, start, end, name); } - if (cc === ZERO) { - return decodeAfterInteger(bytes, 0n, start + 1, end, name); - } - if (cc === TRUE) { - return { start: start + 1, value: true }; - } - if (cc === FALSE) { - return { start: start + 1, value: false }; + if (cc === TRUE || cc === FALSE) { + return decodeBoolean(bytes, start, end, name); } const { type, start: next } = peekType(bytes, start, end, name); if (type === 'float64') { @@ -636,9 +666,7 @@ function decodeAny(bytes, start, end, name) { return decodeString(bytes, next, end, name); } if (type === 'bytestring') { - throw Error( - `decode Bytestrings are not yet supported.`, - ); + return decodeBytestring(bytes, next, end, name); } if (type === 'symbol') { return decodeSymbol(bytes, next, end, name); @@ -646,14 +674,14 @@ function decodeAny(bytes, start, end, name) { if (type === 'list') { return decodeList(bytes, next, end, name); } + if (type === 'dictionary') { + return decodeDictionary(bytes, next, end, name); + } if (type === 'set') { throw Error( `decode Sets are not yet supported.`, ); } - if (type === 'dictionary') { - return decodeDictionary(bytes, next, end, name); - } if (type === 'record') { throw Error( `decode Records are not yet supported.`, @@ -781,7 +809,7 @@ class SyrupParser { const cc = this.bytes[start]; if (cc !== RECORD_END) { throw Error( - `Unexpected character ${JSON.stringify(String.fromCharCode(cc))}, Syrup records must end with "}", got ${JSON.stringify(String.fromCharCode(cc))} at index ${start} of ${name}`, + `Unexpected character ${quote(toChar(cc))}, Syrup records must end with "}", got ${quote(toChar(cc))} at index ${start} of ${name}`, ); } this.state.start = start + 1; @@ -902,7 +930,7 @@ class SyrupParser { const cc = this.bytes[start]; if (cc !== LIST_START) { throw Error( - `Unexpected character ${JSON.stringify(String.fromCharCode(cc))}, Syrup lists must start with "[", got ${JSON.stringify(String.fromCharCode(cc))} at index ${start} of ${name}`, + `Unexpected character ${quote(toChar(cc))}, Syrup lists must start with "[", got ${quote(toChar(cc))} at index ${start} of ${name}`, ); } this.state.start = start + 1; @@ -917,7 +945,7 @@ class SyrupParser { const cc = this.bytes[start]; if (cc !== LIST_END) { throw Error( - `Unexpected character ${JSON.stringify(String.fromCharCode(cc))}, Syrup lists must end with "]", got ${JSON.stringify(String.fromCharCode(cc))} at index ${start} of ${name}`, + `Unexpected character ${quote(toChar(cc))}, Syrup lists must end with "]", got ${quote(toChar(cc))} at index ${start} of ${name}`, ); } this.state.start = start + 1; @@ -938,7 +966,7 @@ class SyrupParser { const cc = this.bytes[start]; if (cc !== SET_START) { throw Error( - `Unexpected character ${JSON.stringify(String.fromCharCode(cc))}, Syrup sets must start with "#", got ${JSON.stringify(String.fromCharCode(cc))} at index ${start} of ${name}`, + `Unexpected character ${quote(toChar(cc))}, Syrup sets must start with "#", got ${quote(toChar(cc))} at index ${start} of ${name}`, ); } this.state.start = start + 1; @@ -953,7 +981,7 @@ class SyrupParser { const cc = this.bytes[start]; if (cc !== SET_END) { throw Error( - `Unexpected character ${JSON.stringify(String.fromCharCode(cc))}, Syrup sets must end with "$", got ${JSON.stringify(String.fromCharCode(cc))} at index ${start} of ${name}`, + `Unexpected character ${quote(toChar(cc))}, Syrup sets must end with "$", got ${quote(toChar(cc))} at index ${start} of ${name}`, ); } this.state.start = start + 1; From 32f79c5831e113a067da3fb3ca261d06a07c5995 Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 31 Mar 2025 22:34:10 -1000 Subject: [PATCH 05/31] wip: basic ocapn descriptor decoding --- packages/syrup/src/decode.js | 54 ++++++- packages/syrup/src/ocapn.js | 261 ++++++++++++++++++++++++++++++ packages/syrup/test/_ocapn.js | 35 ++++ packages/syrup/test/ocapn.test.js | 35 ++++ packages/syrup/test/parse.test.js | Bin 8027 -> 8047 bytes 5 files changed, 384 insertions(+), 1 deletion(-) create mode 100644 packages/syrup/src/ocapn.js create mode 100644 packages/syrup/test/_ocapn.js create mode 100644 packages/syrup/test/ocapn.test.js diff --git a/packages/syrup/src/decode.js b/packages/syrup/src/decode.js index 81c5f3dc59..e3875b75b8 100644 --- a/packages/syrup/src/decode.js +++ b/packages/syrup/src/decode.js @@ -992,9 +992,61 @@ class SyrupParser { this.state.start = next; return value; } + readString() { + const { start, end, name } = this.state; + const { start: next, value, typeCode } = decodeStringlike(this.bytes, start, end, name); + if (typeCode !== STRING_START) { + throw Error(`Unexpected type ${quote(typeCode)}, Syrup strings must start with ${quote(toChar(STRING_START))} at index ${start} of ${name}`); + } + this.state.start = next; + return value; + } + readInteger() { + const { start, end, name } = this.state; + const { start: next, value } = decodeInteger(this.bytes, start, end, name); + this.state.start = next; + return value; + } + readBytestring() { + const { start, end, name } = this.state; + const { start: next, value } = decodeBytestring(this.bytes, start, end, name); + this.state.start = next; + return value; + } + readBoolean() { + const { start, end, name } = this.state; + const { start: next, value } = decodeBoolean(this.bytes, start, end, name); + this.state.start = next; + return value; + } + readSymbolAsString() { + const { start, end, name } = this.state; + const { start: next, value, typeCode } = decodeStringlike(this.bytes, start, end, name); + if (typeCode !== SYMBOL_START) { + throw Error(`Unexpected type ${quote(typeCode)}, Syrup symbols must start with ${quote(toChar(SYMBOL_START))} at index ${start} of ${name}`); + } + this.state.start = next; + return value; + } + readOfType(typeString, opts = {}) { + switch (typeString) { + case 'symbol': + return this.readSymbolAsString(); + case 'string': + return this.readString(); + case 'integer': + return this.readInteger(); + case 'bytestring': + return this.readBytestring(); + case 'boolean': + return this.readBoolean(); + default: + throw Error(`Unknown field type: ${JSON.stringify(typeString)}`); + } + } } -export function parseSyrup(bytes, options = {}) { +export function makeSyrupParser(bytes, options = {}) { const { start = 0, end = bytes.byteLength, name = '' } = options; if (end > bytes.byteLength) { throw Error( diff --git a/packages/syrup/src/ocapn.js b/packages/syrup/src/ocapn.js new file mode 100644 index 0000000000..d05b75d920 --- /dev/null +++ b/packages/syrup/src/ocapn.js @@ -0,0 +1,261 @@ + +class Codec { + marshal(value) { + throw Error('Virtual method: marshal'); + } + unmarshal(parser) { + throw Error('Virtual method: unmarshal'); + } +} + +class OcapnRecordCodec extends Codec { + constructor(label, definition) { + super(); + this.label = label; + this.definition = definition; + for (const [fieldName] of definition) { + if (fieldName === 'type') { + throw new Error('OcapnRecordCodec: The "type" field is reserved for internal use.'); + } + } + } + unmarshal(parser) { + parser.enterRecord(); + const label = parser.readSymbolAsString(); + if (label !== this.label) { + throw Error(`Expected label ${this.label}, got ${label}`); + } + const result = this.unmarshalBody(parser); + parser.exitRecord(); + return result; + } + unmarshalBody(parser) { + const result = {}; + for (const field of this.definition) { + const [fieldName, fieldType] = field; + let fieldValue; + if (typeof fieldType === 'string') { + fieldValue = parser.readOfType(fieldType); + } else { + const fieldDefinition = fieldType; + fieldValue = fieldDefinition.unmarshal(parser); + } + result[fieldName] = fieldValue; + } + result.type = this.label; + return result; + } + marshal(value) { + const result = []; + for (const field of this.definition) { + const [fieldName, fieldType] = field; + let fieldValue; + if (typeof fieldType === 'string') { + // TODO: WRONG + fieldValue = value[fieldName]; + } else { + const fieldDefinition = fieldType; + fieldValue = fieldDefinition.marshal(value[fieldName]); + } + result.push(fieldValue); + } + return result; + } +} + +// OCapN Descriptors and Subtypes + +const OCapNNode = new OcapnRecordCodec( + 'ocapn-node', [ + ['transport', 'symbol'], + ['address', 'bytestring'], + ['hints', 'boolean'], +]) + +const OCapNSturdyRef = new OcapnRecordCodec( + 'ocapn-sturdyref', [ + ['node', OCapNNode], + ['swissNum', 'string'], +]) + +const OCapNPublicKey = new OcapnRecordCodec( + 'public-key', [ + ['scheme', 'symbol'], + ['curve', 'symbol'], + ['flags', 'symbol'], + ['q', 'bytestring'], +]) + +const OCapNSignature = new OcapnRecordCodec( + 'sig-val', [ + ['scheme', 'symbol'], + // TODO: list type + ['r', [ + ['label', 'symbol'], + ['value', 'bytestring'], + ]], + ['s', [ + ['label', 'symbol'], + ['value', 'bytestring'], + ]], +]) + +const DescSigEnvelope = new OcapnRecordCodec( + 'desc:sig-envelope', [ + // TODO: union type, can be DescHandoffReceive, DescHandoffGive, ... + ['object', 'any'], + ['signature', OCapNSignature], +]) + + +const DescImportObject = new OcapnRecordCodec( + 'desc:import-object', [ + ['position', 'integer'], +]) + +const DescImportPromise = new OcapnRecordCodec( + 'desc:import-promise', [ + ['position', 'integer'], +]) + +const DescExport = new OcapnRecordCodec( + 'desc:export', [ + ['position', 'integer'], +]) + +const DescAnswer = new OcapnRecordCodec( + 'desc:answer', [ + ['position', 'integer'], +]) + +const DescHandoffGive = new OcapnRecordCodec( + 'desc:handoff-give', [ + ['receiverKey', OCapNPublicKey], + ['exporterLocation', OCapNNode], + ['session', 'bytestring'], + ['gifterSide', OCapNPublicKey], + ['giftId', 'bytestring'], +]) + +const DescHandoffReceive = new OcapnRecordCodec( + 'desc:handoff-receive', [ + ['receivingSession', 'bytestring'], + ['receivingSide', 'bytestring'], + ['handoffCount', 'integer'], + ['signedGive', DescSigEnvelope], +]) + +const OCapNDescriptorCodecs = { + OCapNNode, + OCapNSturdyRef, + OCapNPublicKey, + OCapNSignature, + DescSigEnvelope, + DescImportObject, + DescImportPromise, + DescExport, + DescAnswer, + DescHandoffGive, + DescHandoffReceive, +} + +// OCapN Operations + +const OpStartSession = new OcapnRecordCodec( + 'op:start-session', [ + ['captpVersion', 'string'], + ['sessionPublicKey', OCapNPublicKey], + ['location', OCapNNode], + ['locationSignature', OCapNSignature], +]) + +const OpListen = new OcapnRecordCodec( + 'op:listen', [ + ['to', DescExport], + // TODO: union type + ['resolveMeDesc', [DescImportObject, DescImportPromise]], + ['wantsPartial', 'boolean'], +]) + +const OpDeliverOnly = new OcapnRecordCodec( + 'op:deliver-only', [ + // TODO: union type + ['to', [DescExport, DescAnswer]], + // TODO: list type, can include OCapNSturdyRef, ... + ['args', 'list'], +]) + +const OpDeliver = new OcapnRecordCodec( + 'op:deliver', [ + // TODO: union type + ['to', [DescExport, DescAnswer]], + // TODO: list type, can be DescSigEnvelope + ['args', 'list'], + ['answerPosition', 'integer'], + // TODO: union type + ['resolveMeDesc', [DescImportObject, DescImportPromise]], +]) + +const OpAbort = new OcapnRecordCodec( + 'op:abort', [ + ['reason', 'string'], +]) + +const OpGcExport = new OcapnRecordCodec( + 'op:gc-export', [ + ['exportPosition', 'integer'], + ['wireDelta', 'integer'], +]) + +const OpGcAnswer = new OcapnRecordCodec( + 'op:gc-answer', [ + ['answerPosition', 'integer'], +]) + +const OpGcSession = new OcapnRecordCodec( + 'op:gc-session', [ + ['session', 'bytestring'], +]) + +const OCapNOpCodecs = { + OpStartSession, + OpListen, + OpDeliverOnly, + OpDeliver, + OpAbort, + OpGcExport, + OpGcAnswer, + OpGcSession, +} + +const OCapNMessageCodecTable = Object.fromEntries( + Object.values(OCapNOpCodecs).map(recordCodec => [recordCodec.label, recordCodec]) +); + +const OCapNDescriptorCodecTable = Object.fromEntries( + Object.values(OCapNDescriptorCodecs).map(recordCodec => [recordCodec.label, recordCodec]) +); + +export const readOCapNMessage = (parser) => { + parser.enterRecord(); + const label = parser.readSymbolAsString(); + const recordCodec = OCapNMessageCodecTable[label]; + if (!recordCodec) { + throw Error(`Unknown OCapN message type: ${label}`); + } + const result = recordCodec.unmarshalBody(parser); + parser.exitRecord(); + return result; +} + +export const readOCapDescriptor = (parser) => { + parser.enterRecord(); + const label = parser.readSymbolAsString(); + const recordCodec = OCapNDescriptorCodecTable[label]; + if (!recordCodec) { + throw Error(`Unknown OCapN descriptor type: ${label}`); + } + const result = recordCodec.unmarshalBody(parser); + parser.exitRecord(); + return result; +} diff --git a/packages/syrup/test/_ocapn.js b/packages/syrup/test/_ocapn.js new file mode 100644 index 0000000000..73facea986 --- /dev/null +++ b/packages/syrup/test/_ocapn.js @@ -0,0 +1,35 @@ +const sym = (s) => `${s.length}'${s}`; +const str = (s) => `${s.length}"${s}`; +const bts = (s) => `${s.length}:${s}`; +const bool = (b) => b ? 't' : 'f'; +const int = (i) => `${Math.floor(Math.abs(i))}${i === 0 ? '' : i < 0 ? '-' : '+'}`; + +const makeNode = (transport, address, hints) => { + return `<10'ocapn-node${sym(transport)}${bts(address)}${bool(hints)}>`; +} + +const makePubKey = (scheme, curve, flags, q) => { + return `<${sym('public-key')}${sym(scheme)}${sym(curve)}${sym(flags)}${bts(q)}>`; +} + +// I made up these syrup values by hand, they may be wrong, sorry. +// Would like external test data for this. +export const descriptorsTable = [ + { syrup: `<10'ocapn-node3'tcp1:0f>`, value: { type: 'ocapn-node', transport: 'tcp', address: '0', hints: false } }, + { syrup: `<15'ocapn-sturdyref${makeNode('tcp', '0', false)}${str('1')}>`, value: { type: 'ocapn-sturdyref', node: { type: 'ocapn-node', transport: 'tcp', address: '0', hints: false }, swissNum: '1' } }, + { syrup: makePubKey('ecc', 'Ed25519', 'eddsa', '1'), value: { type: 'public-key', scheme: 'ecc', curve: 'Ed25519', flags: 'eddsa', q: '1' } }, + // TODO: sig-val, needs s/r-value + // TODO: desc:sig-envelope, needs sig-value, any + // { syrup: '<17\'desc:sig-envelope123+>', value: { type: 'desc:sig-envelope', object: { type: 'desc:handoff-give', receiverKey: { type: 'public-key', scheme: 'ed25519', curve: 'ed25519', flags: 'ed25519', q: new Uint8Array(32) }, exporterLocation: { type: 'ocapn-node', transport: 'tcp', address: '127.0.0.1', hints: false }, session: new Uint8Array(32), gifterSide: { type: 'public-key', scheme: 'ed25519', curve: 'ed25519', flags: 'ed25519', q: new Uint8Array(32) }, giftId: new Uint8Array(32) }, signature: new Uint8Array(32) } }, + { syrup: `<18'desc:import-object123+>`, value: { type: 'desc:import-object', position: 123n } }, + { syrup: `<19'desc:import-promise456+>`, value: { type: 'desc:import-promise', position: 456n } }, + { syrup: `<11'desc:export123+>`, value: { type: 'desc:export', position: 123n } }, + { syrup: `<11'desc:answer456+>`, value: { type: 'desc:answer', position: 456n } }, + { syrup: `<${sym('desc:handoff-give')}${makePubKey('ecc', 'Ed25519', 'eddsa', '1')}${makeNode('tcp', '127.0.0.1', false)}${bts('123')}${makePubKey('ecc', 'Ed25519', 'eddsa', '2')}${bts('456')}>`, value: { type: 'desc:handoff-give', receiverKey: { type: 'public-key', scheme: 'ecc', curve: 'Ed25519', flags: 'eddsa', q: '1' }, exporterLocation: { type: 'ocapn-node', transport: 'tcp', address: '127.0.0.1', hints: false }, session: '123', gifterSide: { type: 'public-key', scheme: 'ecc', curve: 'Ed25519', flags: 'eddsa', q: '2' }, giftId: '456' } }, + // TODO: desc:handoff-receive, needs desc:sig-envelope + // { syrup: `<${sym('desc:handoff-receive')}${bts('123')}${bts('456')}${int(1)}${makeSig()}>`, value: { type: 'desc:handoff-receive', receivingSession: '123', receivingSide: '456' } }, +]; + +export const operationsTable = [ + { syrup: '<8\'op:abort7"explode>', value: { type: 'op:abort', reason: 'explode' } }, +]; diff --git a/packages/syrup/test/ocapn.test.js b/packages/syrup/test/ocapn.test.js new file mode 100644 index 0000000000..63646fcd86 --- /dev/null +++ b/packages/syrup/test/ocapn.test.js @@ -0,0 +1,35 @@ +// @ts-check + +import test from 'ava'; +import { makeSyrupParser } from '../src/decode.js'; +import { readOCapDescriptor, readOCapNMessage } from '../src/ocapn.js'; +import { descriptorsTable, operationsTable } from './_ocapn.js'; + +test('affirmative descriptor read cases', t => { + for (const { syrup, value } of descriptorsTable) { + // We test with a length guess of 1 to maximize the probability + // of discovering a fault in the buffer resize code. + const bytes = new Uint8Array(syrup.length); + for (let i = 0; i < syrup.length; i += 1) { + bytes[i] = syrup.charCodeAt(i); + } + const parser = makeSyrupParser(bytes, { name: syrup }); + let descriptor; + t.notThrows(() => { + descriptor = readOCapDescriptor(parser); + }, `for ${JSON.stringify(syrup)}`); + t.deepEqual(descriptor, value, `for ${JSON.stringify(syrup)}`); + } +}); + +test('affirmative operation read cases', t => { + for (const { syrup, value } of operationsTable) { + const bytes = new Uint8Array(syrup.length); + for (let i = 0; i < syrup.length; i += 1) { + bytes[i] = syrup.charCodeAt(i); + } + const parser = makeSyrupParser(bytes); + const message = readOCapNMessage(parser); + t.deepEqual(message, value, `for ${JSON.stringify(syrup)}`); + } +}); diff --git a/packages/syrup/test/parse.test.js b/packages/syrup/test/parse.test.js index 847f4c19594403156c9d3b489e6b639e2577abe6..9c0fcfae59fdf0f1b3c6471cd6b0074702726cab 100644 GIT binary patch delta 93 zcmca@_ug(o7JqJHc4}~CQE5RyVo`Bw(Z&)dW^}R52bjy)F$5jByLizBCp&Q0Zw{8^ GU;_Z%ZX>Y( delta 66 zcmaEFciV147FR)HQE_T;Wl?Ft#!4sV$%Rb9n=doxvLlIa_UG>6MdDBP=dRzJF3G_L E0Cp-E?*IS* From 7063cc47156edf5936e6ef70b776df528325711f Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 1 Apr 2025 12:48:33 -1000 Subject: [PATCH 06/31] test(syrup): make fuzz test less noisy in test output --- packages/syrup/test/fuzz.test.js | 38 +++++++++++++++++--------------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/packages/syrup/test/fuzz.test.js b/packages/syrup/test/fuzz.test.js index cc0166aab6..cef0337e9a 100644 --- a/packages/syrup/test/fuzz.test.js +++ b/packages/syrup/test/fuzz.test.js @@ -26,13 +26,13 @@ function fuzzyString(budget, random) { } else if (partition < 0.5) { // string mostly printable return Array(length) - .fill() + .fill(undefined) .map(() => String.fromCharCode(random() * 128)) .join(''); } else { // lower-case strings return Array(length) - .fill() + .fill(undefined) .map(() => String.fromCharCode('a'.charCodeAt(0) + random() * 26)) .join(''); } @@ -49,7 +49,7 @@ function largeFuzzySyrupable(budget, random) { // bigint return BigInt( Array(length) - .fill() + .fill(undefined) .map(() => `${Math.floor(random() * 10)}`) .join(''), ); @@ -60,7 +60,7 @@ function largeFuzzySyrupable(budget, random) { // array return ( new Array(length) - .fill() + .fill(undefined) // Recursion is a thing, yo. // eslint-disable-next-line no-use-before-define .map(() => fuzzySyrupable(budget / length, random)) @@ -68,7 +68,7 @@ function largeFuzzySyrupable(budget, random) { } else { // object return Object.fromEntries( - new Array(length).fill().map(() => [ + new Array(length).fill(undefined).map(() => [ fuzzyString(20, random), // Recursion is a thing, yo. // eslint-disable-next-line no-use-before-define @@ -102,18 +102,20 @@ const defaultSeed = [0xb0b5c0ff, 0xeefacade, 0xb0b5c0ff, 0xeefacade]; const prng = new XorShift(defaultSeed); const random = () => prng.random(); -for (let i = 0; i < 1000; i += 1) { - (index => { - const object1 = fuzzySyrupable(random() * 100, random); - const syrup2 = encodeSyrup(object1); - const desc = JSON.stringify(new TextDecoder().decode(syrup2)); - test(`fuzz ${index}`, t => { - // t.log(random()); - // t.log(object1); - const object3 = decodeSyrup(syrup2); - const syrup4 = encodeSyrup(object3); +test('fuzz', t => { + for (let i = 0; i < 1000; i += 1) { + (index => { + const object1 = fuzzySyrupable(random() * 100, random); + const syrup2 = encodeSyrup(object1); + const desc = JSON.stringify(new TextDecoder().decode(syrup2)); + let object3; + let syrup4; + t.notThrows(() => { + object3 = decodeSyrup(syrup2); + syrup4 = encodeSyrup(object3); + }, `fuzz ${index} for ${desc}`); t.deepEqual(object1, object3, desc); t.deepEqual(syrup2, syrup4, desc); - }); - })(i); -} + })(i); + } +}); From f2c470fe9217a3fc2004a2659f3fd0d0598c00c6 Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 1 Apr 2025 12:55:46 -1000 Subject: [PATCH 07/31] refactor(syrup): encode via BufferWriter --- packages/syrup/src/buffer-reader.js | 272 ++++++++++++++++++++++++++++ packages/syrup/src/buffer-writer.js | 225 +++++++++++++++++++++++ packages/syrup/src/encode.js | 231 ++++++++++------------- 3 files changed, 590 insertions(+), 138 deletions(-) create mode 100644 packages/syrup/src/buffer-reader.js create mode 100644 packages/syrup/src/buffer-writer.js diff --git a/packages/syrup/src/buffer-reader.js b/packages/syrup/src/buffer-reader.js new file mode 100644 index 0000000000..de869a1b5f --- /dev/null +++ b/packages/syrup/src/buffer-reader.js @@ -0,0 +1,272 @@ +// @ts-check +/* eslint no-bitwise: ["off"] */ + +const q = JSON.stringify; + +/** + * @typedef {object} BufferReaderState + * @property {Uint8Array} bytes + * @property {DataView} data + * @property {number} length + * @property {number} index + * @property {number} offset + */ + +/** @type {WeakMap} */ +const privateFields = new WeakMap(); + +/** @type {(bufferReader: BufferReader) => BufferReaderState} */ +const privateFieldsGet = privateFields.get.bind(privateFields); + +export class BufferReader { + /** + * @param {ArrayBuffer} buffer + */ + constructor(buffer) { + const bytes = new Uint8Array(buffer); + const data = new DataView(bytes.buffer); + privateFields.set(this, { + bytes, + data, + length: bytes.length, + index: 0, + offset: 0, + }); + } + + /** + * @returns {number} + */ + get length() { + return privateFieldsGet(this).length; + } + + /** + * @returns {number} + */ + get index() { + return privateFieldsGet(this).index; + } + + /** + * @param {number} index + */ + set index(index) { + this.seek(index); + } + + /** + * @param {number} offset + */ + set offset(offset) { + const fields = privateFieldsGet(this); + if (offset > fields.data.byteLength) { + throw Error('Cannot set offset beyond length of underlying data'); + } + if (offset < 0) { + throw Error('Cannot set negative offset'); + } + fields.offset = offset; + fields.length = fields.data.byteLength - fields.offset; + } + + /** + * @param {number} index + * @returns {boolean} whether the read head can move to the given absolute + * index. + */ + canSeek(index) { + const fields = privateFieldsGet(this); + return index >= 0 && fields.offset + index <= fields.length; + } + + /** + * @param {number} index the index to check. + * @throws {Error} an Error if the index is out of bounds. + */ + assertCanSeek(index) { + const fields = privateFieldsGet(this); + if (!this.canSeek(index)) { + throw Error( + `End of data reached (data length = ${fields.length}, asked index ${index}`, + ); + } + } + + /** + * @param {number} index + * @returns {number} prior index + */ + seek(index) { + const fields = privateFieldsGet(this); + const restore = fields.index; + this.assertCanSeek(index); + fields.index = index; + return restore; + } + + /** + * @param {number} size + * @returns {Uint8Array} + */ + peek(size) { + const fields = privateFieldsGet(this); + // Clamp size. + size = Math.max(0, Math.min(fields.length - fields.index, size)); + if (size === 0) { + // in IE10, when using subarray(idx, idx), we get the array [0x00] instead of []. + return new Uint8Array(0); + } + const result = fields.bytes.subarray( + fields.offset + fields.index, + fields.offset + fields.index + size, + ); + return result; + } + + /** + * @param {number} offset + */ + canRead(offset) { + const fields = privateFieldsGet(this); + return this.canSeek(fields.index + offset); + } + + /** + * Check that the offset will not go too far. + * + * @param {number} offset the additional offset to check. + * @throws {Error} an Error if the offset is out of bounds. + */ + assertCanRead(offset) { + const fields = privateFieldsGet(this); + this.assertCanSeek(fields.index + offset); + } + + /** + * Get raw data without conversion, bytes. + * + * @param {number} size the number of bytes to read. + * @returns {Uint8Array} the raw data. + */ + read(size) { + const fields = privateFieldsGet(this); + this.assertCanRead(size); + const result = this.peek(size); + fields.index += size; + return result; + } + + /** + * @returns {number} + */ + readUint8() { + const fields = privateFieldsGet(this); + this.assertCanRead(1); + const index = fields.offset + fields.index; + const value = fields.data.getUint8(index); + fields.index += 1; + return value; + } + + /** + * @returns {number} + * @param {boolean=} littleEndian + */ + readUint16(littleEndian) { + const fields = privateFieldsGet(this); + this.assertCanRead(2); + const index = fields.offset + fields.index; + const value = fields.data.getUint16(index, littleEndian); + fields.index += 2; + return value; + } + + /** + * @returns {number} + * @param {boolean=} littleEndian + */ + readUint32(littleEndian) { + const fields = privateFieldsGet(this); + this.assertCanRead(4); + const index = fields.offset + fields.index; + const value = fields.data.getUint32(index, littleEndian); + fields.index += 4; + return value; + } + + /** + * @param {number} index + * @returns {number} + */ + byteAt(index) { + const fields = privateFieldsGet(this); + return fields.bytes[fields.offset + index]; + } + + /** + * @param {number} offset + */ + skip(offset) { + const fields = privateFieldsGet(this); + this.seek(fields.index + offset); + } + + /** + * @param {Uint8Array} expected + * @returns {boolean} + */ + expect(expected) { + const fields = privateFieldsGet(this); + if (!this.matchAt(fields.index, expected)) { + return false; + } + fields.index += expected.length; + return true; + } + + /** + * @param {number} index + * @param {Uint8Array} expected + * @returns {boolean} + */ + matchAt(index, expected) { + const fields = privateFieldsGet(this); + if (index + expected.length > fields.length || index < 0) { + return false; + } + for (let i = 0; i < expected.length; i += 1) { + if (expected[i] !== this.byteAt(index + i)) { + return false; + } + } + return true; + } + + /** + * @param {Uint8Array} expected + */ + assert(expected) { + const fields = privateFieldsGet(this); + if (!this.expect(expected)) { + throw Error( + `Expected ${q(expected)} at ${fields.index}, got ${this.peek( + expected.length, + )}`, + ); + } + } + + /** + * @param {Uint8Array} expected + * @returns {number} + */ + findLast(expected) { + const fields = privateFieldsGet(this); + let index = fields.length - expected.length; + while (index >= 0 && !this.matchAt(index, expected)) { + index -= 1; + } + return index; + } +} \ No newline at end of file diff --git a/packages/syrup/src/buffer-writer.js b/packages/syrup/src/buffer-writer.js new file mode 100644 index 0000000000..f715150352 --- /dev/null +++ b/packages/syrup/src/buffer-writer.js @@ -0,0 +1,225 @@ +// @ts-check +/* eslint no-bitwise: ["off"] */ + +const textEncoder = new TextEncoder(); + +/** + * @type {WeakMap} +*/ +const privateFields = new WeakMap(); + +/** +* @param {BufferWriter} self +*/ +const getPrivateFields = self => { + const fields = privateFields.get(self); + if (!fields) { + throw Error('BufferWriter fields are not initialized'); + } + return fields; +}; + +const assertNatNumber = n => { + if (Number.isSafeInteger(n) && /** @type {number} */ (n) >= 0) { + return; + } + throw TypeError(`must be a non-negative integer, got ${n}`); +}; + +export class BufferWriter { + /** + * @returns {number} + */ + get length() { + return getPrivateFields(this).length; + } + + /** + * @returns {number} + */ + get index() { + return getPrivateFields(this).index; + } + + /** + * @param {number} index + */ + set index(index) { + this.seek(index); + } + + /** + * @param {number=} capacity + */ + constructor(capacity = 16) { + const bytes = new Uint8Array(capacity); + const data = new DataView(bytes.buffer); + privateFields.set(this, { + bytes, + data, + index: 0, + length: 0, + capacity, + }); + } + + /** + * @param {number} required + */ + ensureCanSeek(required) { + assertNatNumber(required); + const fields = getPrivateFields(this); + let capacity = fields.capacity; + if (capacity >= required) { + return; + } + while (capacity < required) { + capacity *= 2; + } + const bytes = new Uint8Array(capacity); + const data = new DataView(bytes.buffer); + bytes.set(fields.bytes.subarray(0, fields.length)); + fields.bytes = bytes; + fields.data = data; + fields.capacity = capacity; + } + + /** + * @param {number} index + */ + seek(index) { + const fields = getPrivateFields(this); + this.ensureCanSeek(index); + fields.index = index; + fields.length = Math.max(fields.index, fields.length); + } + + /** + * @param {number} size + */ + ensureCanWrite(size) { + assertNatNumber(size); + const fields = getPrivateFields(this); + this.ensureCanSeek(fields.index + size); + } + + /** + * @param {Uint8Array} bytes + */ + write(bytes) { + const fields = getPrivateFields(this); + this.ensureCanWrite(bytes.byteLength); + fields.bytes.set(bytes, fields.index); + fields.index += bytes.byteLength; + fields.length = Math.max(fields.index, fields.length); + } + + /** + * @param {number} byte + */ + writeByte(byte) { + assertNatNumber(byte); + if (byte > 0xFF) { + throw RangeError(`byte must be in range 0..255, got ${byte}`); + } + this.writeUint8(byte); + } + + /** + * @param {number} start + * @param {number} end + */ + writeCopy(start, end) { + assertNatNumber(start); + assertNatNumber(end); + const fields = getPrivateFields(this); + const size = end - start; + this.ensureCanWrite(size); + fields.bytes.copyWithin(fields.index, start, end); + fields.index += size; + fields.length = Math.max(fields.index, fields.length); + } + + /** + * @param {number} value + */ + writeUint8(value) { + const fields = getPrivateFields(this); + this.ensureCanWrite(1); + fields.data.setUint8(fields.index, value); + fields.index += 1; + fields.length = Math.max(fields.index, fields.length); + } + + /** + * @param {number} value + * @param {boolean=} littleEndian + */ + writeUint16(value, littleEndian) { + const fields = getPrivateFields(this); + this.ensureCanWrite(2); + const index = fields.index; + fields.data.setUint16(index, value, littleEndian); + fields.index += 2; + fields.length = Math.max(fields.index, fields.length); + } + + /** + * @param {number} value + * @param {boolean=} littleEndian + */ + writeUint32(value, littleEndian) { + const fields = getPrivateFields(this); + this.ensureCanWrite(4); + const index = fields.index; + fields.data.setUint32(index, value, littleEndian); + fields.index += 4; + fields.length = Math.max(fields.index, fields.length); + } + + /** + * @param {number} value + * @param {boolean=} littleEndian + */ + writeFloat64(value, littleEndian) { + const fields = getPrivateFields(this); + this.ensureCanWrite(8); + const index = fields.index; + fields.data.setFloat64(index, value, littleEndian); + fields.index += 8; + fields.length = Math.max(fields.index, fields.length); + } + + /** + * @param {string} string + */ + writeString(string) { + const bytes = textEncoder.encode(string); + this.write(bytes); + } + + /** + * @param {number=} begin + * @param {number=} end + * @returns {Uint8Array} + */ + subarray(begin, end) { + const fields = getPrivateFields(this); + return fields.bytes.subarray(0, fields.length).subarray(begin, end); + } + + /** + * @param {number=} begin + * @param {number=} end + * @returns {Uint8Array} + */ + slice(begin, end) { + return this.subarray(begin, end).slice(); + } +} \ No newline at end of file diff --git a/packages/syrup/src/encode.js b/packages/syrup/src/encode.js index 56c6ba9c28..977416924a 100644 --- a/packages/syrup/src/encode.js +++ b/packages/syrup/src/encode.js @@ -1,5 +1,6 @@ // @ts-check +import { BufferWriter } from './buffer-writer.js'; import { compareByteArrays } from './compare.js'; import { getSyrupSymbolName } from './symbol.js'; @@ -21,126 +22,79 @@ const NAN64 = freeze([0x7f, 0xf8, 0, 0, 0, 0, 0, 0]); const textEncoder = new TextEncoder(); /** - * @typedef {object} Buffer - * @property {Uint8Array} bytes - * @property {DataView} data - * @property {number} length - */ - -/** - * @param {Buffer} buffer - * @param {number} increaseBy - * @returns {number} old length + * @param {import('./buffer-writer.js').BufferWriter} bufferWriter + * @param {Uint8Array} bytes + * @param {string} typeChar */ -function grow(buffer, increaseBy) { - const cursor = buffer.length; - if (increaseBy === 0) { - return cursor; - } - buffer.length += increaseBy; - let capacity = buffer.bytes.length; - // Expand backing storage, leaving headroom for another similar-size increase. - if (buffer.length + increaseBy > capacity) { - while (buffer.length + increaseBy > capacity) { - capacity *= 2; - } - const bytes = new Uint8Array(capacity); - const data = new DataView(bytes.buffer); - bytes.set(buffer.bytes.subarray(0, buffer.length), 0); - buffer.bytes = bytes; - buffer.data = data; - } - return cursor; +function writeStringlike(bufferWriter, bytes, typeChar) { + // write length prefix as ascii string + const length = bytes.byteLength; + const lengthPrefix = `${length}`; + bufferWriter.writeString(lengthPrefix); + bufferWriter.writeString(typeChar); + bufferWriter.write(bytes); } /** - * @param {Buffer} buffer + * @param {import('./buffer-writer.js').BufferWriter} bufferWriter * @param {string} value - * @param {string} typeChar */ -function encodeStringlike(buffer, value, typeChar) { - const stringLength = value.length; - const likelyPrefixLength = `${stringLength}`.length + 1; - // buffer.length will be incorrect until we fix it before returning. - const start = grow(buffer, likelyPrefixLength + stringLength); - const likelyDataStart = start + likelyPrefixLength; - - for (let remaining = value, read = 0, written = 0; ; ) { - const chunk = textEncoder.encodeInto( - remaining, - buffer.bytes.subarray(likelyDataStart + written), - ); - written += chunk.written || 0; - read += chunk.read || 0; - if (read === stringLength) { - const prefix = `${written}${typeChar}`; // length prefix, typeChar suffix - const prefixLength = prefix.length; - buffer.length = start; - grow(buffer, prefixLength + written); - if (prefixLength !== likelyPrefixLength) { - buffer.bytes.copyWithin(start + prefixLength, likelyDataStart); // shift right - } - textEncoder.encodeInto( - prefix, - buffer.bytes.subarray(start, start + prefixLength), - ); - return; - } - remaining = remaining.substring(chunk.read || 0); - grow(buffer, buffer.bytes.length); - } +function writeString(bufferWriter, value) { + const bytes = textEncoder.encode(value); + writeStringlike(bufferWriter, bytes, '"'); } /** - * @param {Buffer} buffer + * @param {import('./buffer-writer.js').BufferWriter} bufferWriter * @param {string} value */ -function encodeString(buffer, value) { - encodeStringlike(buffer, value, '"'); +function writeSymbol(bufferWriter, value) { + const bytes = textEncoder.encode(value); + writeStringlike(bufferWriter, bytes, '\''); } /** - * @param {Buffer} buffer - * @param {string} value + * @param {import('./buffer-writer.js').BufferWriter} bufferWriter + * @param {Uint8Array} value */ -function encodeSymbol(buffer, value) { - encodeStringlike(buffer, value, '\''); +function writeBytestring(bufferWriter, value) { + writeStringlike(bufferWriter, value, ':'); } /** - * @param {Buffer} buffer + * @param {import('./buffer-writer.js').BufferWriter} bufferWriter * @param {Record} record * @param {Array} path */ -function encodeDictionary(buffer, record, path) { - const restart = buffer.length; +function writeDictionary(bufferWriter, record, path) { const indexes = []; const keys = []; const keyBytes = []; - const encodeKey = (key) => { + const writeKey = (bufferWriter, key) => { if (typeof key === 'string') { - encodeString(buffer, key); + writeString(bufferWriter, key); return; } if (typeof key === 'symbol') { const syrupSymbol = getSyrupSymbolName(key); - encodeSymbol(buffer, syrupSymbol); + writeSymbol(bufferWriter, syrupSymbol); return; } throw TypeError(`Dictionary keys must be strings or symbols, got ${typeof key} at ${path.join('/')}`); }; + // We need to sort the keys, so we write them to a scratch buffer first + const scratchWriter = new BufferWriter(); for (const key of ownKeys(record)) { - const start = buffer.length; - encodeKey(key); - const end = buffer.length; + const start = scratchWriter.length; + writeKey(scratchWriter, key); + const end = scratchWriter.length; keys.push(key); - keyBytes.push(buffer.bytes.subarray(start, end)); + keyBytes.push(scratchWriter.subarray(start, end)); indexes.push(indexes.length); } - indexes.sort((i, j) => compareByteArrays( keyBytes[i], @@ -152,116 +106,120 @@ function encodeDictionary(buffer, record, path) { ), ); - buffer.length = restart; - - let cursor = grow(buffer, 1); - buffer.bytes[cursor] = DICT_START; - + // Now we write the dictionary + bufferWriter.writeByte(DICT_START); for (const index of indexes) { const key = keys[index]; const value = record[key]; + const bytes = keyBytes[index]; - encodeKey(key); + bufferWriter.write(bytes); // Recursion, it's a thing! // eslint-disable-next-line no-use-before-define - encodeAny(buffer, value, path, key); + writeAny(bufferWriter, value, path, key); } - - cursor = grow(buffer, 1); - buffer.bytes[cursor] = DICT_END; + bufferWriter.writeByte(DICT_END); } /** - * @param {Buffer} buffer + * @param {import('./buffer-writer.js').BufferWriter} bufferWriter * @param {Array} array * @param {Array} path */ -function encodeArray(buffer, array, path) { - let cursor = grow(buffer, 2 + array.length); - buffer.length = cursor + 1; - buffer.bytes[cursor] = LIST_START; +function writeList(bufferWriter, array, path) { + bufferWriter.writeByte(LIST_START); let index = 0; for (const value of array) { // Recursion, it's a thing! // eslint-disable-next-line no-use-before-define - encodeAny(buffer, value, path, index); + writeAny(bufferWriter, value, path, index); index += 1; } - cursor = grow(buffer, 1); - buffer.bytes[cursor] = LIST_END; + bufferWriter.writeByte(LIST_END); } /** - * @param {Buffer} buffer + * @param {import('./buffer-writer.js').BufferWriter} bufferWriter + * @param {number} value + */ +function writeDouble(bufferWriter, value) { + bufferWriter.writeByte(DOUBLE); + if (value === 0) { + // no-op + } else if (Number.isNaN(value)) { + // Canonicalize NaN + // @ts-expect-error using frozen array as Uint8Array + bufferWriter.write(NAN64); + } else { + bufferWriter.writeFloat64(value, false); // big end + } +} + +/** + * @param {import('./buffer-writer.js').BufferWriter} bufferWriter + * @param {bigint} value + */ +function writeInteger(bufferWriter, value) { + const string = value >= 0 ? `${value}+` : `${-value}-`; + bufferWriter.writeString(string); +} + +/** + * @param {import('./buffer-writer.js').BufferWriter} bufferWriter + * @param {boolean} value + */ +function writeBoolean(bufferWriter, value) { + bufferWriter.writeByte(value ? TRUE : FALSE); +} + +/** + * @param {import('./buffer-writer.js').BufferWriter} bufferWriter * @param {any} value * @param {Array} path * @param {string | symbol | number} pathSuffix */ -function encodeAny(buffer, value, path, pathSuffix) { +function writeAny(bufferWriter, value, path, pathSuffix) { if (typeof value === 'symbol') { - encodeSymbol(buffer, getSyrupSymbolName(value)); + writeSymbol(bufferWriter, getSyrupSymbolName(value)); return; } if (typeof value === 'string') { - encodeString(buffer, value); + writeString(bufferWriter, value); return; } if (typeof value === 'number') { - const cursor = grow(buffer, 9); - buffer.bytes[cursor] = DOUBLE; - if (value === 0) { - // no-op - } else if (Number.isNaN(value)) { - // Canonicalize NaN - buffer.bytes.set(NAN64, cursor + 1); - } else { - buffer.data.setFloat64(cursor + 1, value, false); // big end - } + writeDouble(bufferWriter, value); return; } if (typeof value === 'bigint') { - const string = value >= 0 ? `${value}+` : `${-value}-`; - const cursor = grow(buffer, string.length); - textEncoder.encodeInto(string, buffer.bytes.subarray(cursor)); + writeInteger(bufferWriter, value); return; } if (value instanceof Uint8Array) { - const prefix = `${value.length}:`; // decimal and colon suffix - const cursor = grow(buffer, prefix.length + value.length); - textEncoder.encodeInto(prefix, buffer.bytes.subarray(cursor)); - buffer.bytes.set(value, cursor + prefix.length); + writeBytestring(bufferWriter, value); return; } if (Array.isArray(value)) { - path.push(pathSuffix); - encodeArray(buffer, value, path); - path.pop(); + writeList(bufferWriter, value, path); return; } if (Object(value) === value) { path.push(pathSuffix); - encodeDictionary(buffer, value, path); + writeDictionary(bufferWriter, value, path); path.pop(); return; } - if (value === false) { - const cursor = grow(buffer, 1); - buffer.bytes[cursor] = FALSE; - return; - } - - if (value === true) { - const cursor = grow(buffer, 1); - buffer.bytes[cursor] = TRUE; + if (value === true || value === false) { + writeBoolean(bufferWriter, value); return; } @@ -278,10 +236,7 @@ function encodeAny(buffer, value, path, pathSuffix) { */ export function encodeSyrup(value, options = {}) { const { length: capacity = defaultCapacity } = options; - const bytes = new Uint8Array(capacity); - const data = new DataView(bytes.buffer); - const length = 0; - const buffer = { bytes, data, length }; - encodeAny(buffer, value, [], '/'); - return buffer.bytes.subarray(0, buffer.length); + const bufferWriter = new BufferWriter(capacity); + writeAny(bufferWriter, value, [], '/'); + return bufferWriter.subarray(0, bufferWriter.length); } From 40ad072737229d431c161da2cb353ee9fd8ff2c6 Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 1 Apr 2025 15:54:00 -1000 Subject: [PATCH 08/31] wip(syrup): refactor decode to use bufferReader --- packages/syrup/src/buffer-reader.js | 56 +- packages/syrup/src/decode.js | 1352 +++++++++------------------ packages/syrup/test/decode.test.js | 22 +- packages/syrup/test/ocapn.test.js | 64 +- packages/syrup/test/parse.test.js | Bin 8047 -> 8809 bytes 5 files changed, 564 insertions(+), 930 deletions(-) diff --git a/packages/syrup/src/buffer-reader.js b/packages/syrup/src/buffer-reader.js index de869a1b5f..f61dec3353 100644 --- a/packages/syrup/src/buffer-reader.js +++ b/packages/syrup/src/buffer-reader.js @@ -34,6 +34,18 @@ export class BufferReader { }); } + static fromBytes(bytes) { + const empty = new ArrayBuffer(0); + const reader = new BufferReader(empty); + const fields = privateFieldsGet(reader); + fields.bytes = bytes; + fields.data = new DataView(bytes.buffer); + fields.length = bytes.length; + fields.index = 0; + fields.offset = 0; + return reader; + } + /** * @returns {number} */ @@ -87,9 +99,14 @@ export class BufferReader { assertCanSeek(index) { const fields = privateFieldsGet(this); if (!this.canSeek(index)) { - throw Error( + const err = Error( `End of data reached (data length = ${fields.length}, asked index ${index}`, ); + // @ts-expect-error + err.code = 'EOD'; + // @ts-expect-error + err.index = index; + throw err; } } @@ -124,6 +141,12 @@ export class BufferReader { return result; } + peekByte() { + const fields = privateFieldsGet(this); + this.assertCanRead(1); + return fields.bytes[fields.offset + fields.index]; + } + /** * @param {number} offset */ @@ -157,6 +180,13 @@ export class BufferReader { return result; } + /** + * @returns {number} + */ + readByte() { + return this.readUint8(); + } + /** * @returns {number} */ @@ -195,6 +225,19 @@ export class BufferReader { return value; } + /** + * @param {boolean=} littleEndian + * @returns {number} + */ + readFloat64(littleEndian = false) { + const fields = privateFieldsGet(this); + this.assertCanRead(8); + const index = fields.offset + fields.index; + const value = fields.data.getFloat64(index, littleEndian); + fields.index += 8; + return value; + } + /** * @param {number} index * @returns {number} @@ -204,6 +247,17 @@ export class BufferReader { return fields.bytes[fields.offset + index]; } + /** + * @param {number} index + * @param {number} size + * @returns {Uint8Array} + */ + bytesAt(index, size) { + this.assertCanSeek(index + size); + const fields = privateFieldsGet(this); + return fields.bytes.subarray(fields.offset + index, fields.offset + index + size); + } + /** * @param {number} offset */ diff --git a/packages/syrup/src/decode.js b/packages/syrup/src/decode.js index e3875b75b8..6957598e7a 100644 --- a/packages/syrup/src/decode.js +++ b/packages/syrup/src/decode.js @@ -1,5 +1,6 @@ // @ts-check +import { BufferReader } from './buffer-reader.js'; import { compareByteArrays } from './compare.js'; import { SyrupSymbolFor } from './symbol.js'; @@ -26,445 +27,119 @@ const DOUBLE = 'D'.charCodeAt(0); const textDecoder = new TextDecoder(); -const scratch = new ArrayBuffer(8); -const scratchBytes = new Uint8Array(scratch); -const scratchData = new DataView(scratch); - const { defineProperty, freeze } = Object; const quote = (o) => JSON.stringify(o); const toChar = (code) => String.fromCharCode(code); -/** - * @param {Uint8Array} bytes - */ -function isCanonicalNaN64(bytes) { - const [a, b, c, d, e, f, g, h] = bytes; - return ( - a === 0x7f && - b === 0xf8 && - c === 0 && - d === 0 && - e === 0 && - f === 0 && - g === 0 && - h === 0 - ); -} +const canonicalNaN64 = freeze([0x7f, 0xf8, 0, 0, 0, 0, 0, 0]); +const canonicalZero64 = freeze([0, 0, 0, 0, 0, 0, 0, 0]); /** - * @param {Uint8Array} bytes - */ -function isCanonicalZero64(bytes) { - const [a, b, c, d, e, f, g, h] = bytes; - return ( - a === 0 && - b === 0 && - c === 0 && - d === 0 && - e === 0 && - f === 0 && - g === 0 && - h === 0 - ); -} -/** - * @param {Uint8Array} bytes - * @param {bigint} integer - * @param {number} start - * @param {number} end + * @param {BufferReader} bufferReader * @param {string} name + * @returns {boolean} */ -function decodeAfterInteger(bytes, integer, start, end, name) { - if (start >= end) { - throw Error(`Unexpected end of Syrup, expected integer suffix in ${name}`); - } - const cc = bytes[start]; - if (cc === PLUS) { - return { - start: start + 1, - value: integer, - }; - } - if (cc === MINUS) { - if (integer === 0n) { - throw Error(`Unexpected non-canonical -0`); - } - return { - start: start + 1, - value: -integer, - }; - } - if (cc === BYTES_START) { - start += 1; - const subStart = start; - start += Number(integer); - if (start > end) { - throw Error( - `Unexpected end of Syrup, expected ${integer} bytes after Syrup bytestring starting at index ${subStart} in ${name}`, - ); - } - const value = bytes.subarray(subStart, start); - return { start, value }; - } - if (cc === STRING_START) { - start += 1; - const subStart = start; - start += Number(integer); - if (start > end) { - throw Error( - `Unexpected end of Syrup, expected ${integer} bytes after string starting at index ${subStart} in ${name}`, - ); - } - const value = textDecoder.decode(bytes.subarray(subStart, start)); - return { start, value }; - } - if (cc === SYMBOL_START) { - start += 1; - const subStart = start; - start += Number(integer); - if (start > end) { - throw Error( - `Unexpected end of Syrup, expected ${integer} bytes after symbol starting at index ${subStart} in ${name}`, - ); - } - const value = textDecoder.decode(bytes.subarray(subStart, start)); - return { start, value: SyrupSymbolFor(value) }; - } - throw Error( - `Unexpected character ${JSON.stringify( - String.fromCharCode(cc), - )} at Syrup index ${start} of ${name}`, - ); -} - -function seekEndOfInteger(bytes, start, end) { - let at = start + 1; - // eslint-disable-next-line no-empty - for (; at < end && bytes[at] >= ZERO && bytes[at] <= NINE; at += 1) {} - return at; -} - -/** - * @param {Uint8Array} bytes - * @param {number} start - * @param {number} end - */ -function decodeIntegerPrefix(bytes, start, end) { - const at = seekEndOfInteger(bytes, start, end); - return { - start: at, - integer: BigInt(textDecoder.decode(bytes.subarray(start, at))), - }; -} - -function decodeInteger(bytes, start, end, name) { - const cc = bytes[start]; - if (cc >= ONE && cc <= NINE) { - const { start: next, integer } = decodeIntegerPrefix(bytes, start, end); - return decodeAfterInteger(bytes, integer, next, end, name); - } - if (cc === ZERO) { - return decodeAfterInteger(bytes, 0n, start + 1, end, name); - } - throw Error(`Unexpected byte ${quote(toChar(cc))}, Syrup integers must start with ${quote(toChar(ZERO))} or a digit at index ${start} of ${name}`); -} - -function decodeBoolean(bytes, start, end, name) { - const cc = bytes[start]; +function readBoolean(bufferReader, name) { + const cc = bufferReader.readByte(); if (cc === TRUE) { - return { start: start + 1, value: true }; + return true; } if (cc === FALSE) { - return { start: start + 1, value: false }; + return false; } - throw Error(`Unexpected byte ${quote(toChar(cc))}, Syrup booleans must start with ${quote(toChar(TRUE))} or ${quote(toChar(FALSE))} at index ${start} of ${name}`); + throw Error(`Unexpected byte ${quote(toChar(cc))}, Syrup booleans must start with ${quote(toChar(TRUE))} or ${quote(toChar(FALSE))} at index ${bufferReader.index} of ${name}`); } /** - * @param {Uint8Array} bytes - * @param {number} start - * @param {number} end + * @param {BufferReader} bufferReader * @param {string} name + * @returns {any[]} */ -function decodeList(bytes, start, end, name) { - const list = []; - for (;;) { - if (start >= end) { - throw Error( - `Unexpected end of Syrup, expected Syrup value or end of Syrup list marker "]" at index ${start} in ${name}`, - ); - } - const cc = bytes[start]; - if (cc === LIST_END) { - return { - start: start + 1, - value: list, - }; - } - let value; - // eslint-disable-next-line no-use-before-define - ({ start, value } = decodeAny(bytes, start, end, name)); - list.push(value); +function readList(bufferReader, name) { + let cc = bufferReader.readByte(); + if (cc !== LIST_START) { + throw Error(`Unexpected byte ${quote(toChar(cc))}, Syrup lists must start with ${quote(toChar(LIST_START))} at index ${bufferReader.index} of ${name}`); } -} - -function seekList(bytes, start, end, name) { - for (;;) { - if (start >= end) { - throw Error( - `Unexpected end of Syrup, expected Syrup value or end of Syrup list marker "]" at index ${start} in ${name}`, - ); - } - const cc = bytes[start]; - if (cc === LIST_END) { - return { - start: start + 1, - }; - } - ({ start } = seekAny(bytes, start, end, name)); - } -} - -function seekSet(bytes, start, end, name) { + const list = []; for (;;) { - if (start >= end) { - throw Error( - `Unexpected end of Syrup, expected Syrup value or end of Syrup set marker "$" at index ${start} in ${name}`, - ); - } - const cc = bytes[start]; - if (cc === SET_END) { - return { - start: start + 1, - }; - } - ({ start } = seekAny(bytes, start, end, name)); - } -} - -export function decodeSymbolName(bytes, start, end, name) { - const { value } = decodeStringlike(bytes, start, end, name); - return value; -} - -/** - * @param {Uint8Array} bytes - * @param {number} start - * @param {number} end - * @param {string} name - */ -function decodeStringlike(bytes, start, end, name) { - if (start >= end) { - throw Error( - `Unexpected end of Syrup, expected Syrup string at end of ${name}`, - ); - } - let length; - ({ start, integer: length } = decodeIntegerPrefix(bytes, start, end)); - - const typeCode = bytes[start]; - if (typeCode !== STRING_START && typeCode !== SYMBOL_START && typeCode !== BYTES_START) { - // TODO: error message implies this is only for dictionaries, but it's not - throw Error( - `Unexpected byte ${JSON.stringify( - String.fromCharCode(typeCode), - )}, Syrup string-likes must be strings, symbols, or bytestrings at index ${start} of ${name}`, - ); - } - start += 1; - - const subStart = start; - start += Number(length); - if (start > end) { - throw Error( - `Unexpected end of Syrup, expected ${length} bytes after index ${subStart} of ${name}`, - ); - } - const value = textDecoder.decode(bytes.subarray(subStart, start)); - return { start, value, typeCode }; -} - -function decodeSymbol(bytes, start, end, name) { - const { start: next, value, typeCode } = decodeStringlike(bytes, start, end, name); - if (typeCode === SYMBOL_START) { - return { start: next, value: SyrupSymbolFor(value) }; - } - throw Error(`Unexpected type ${quote(toChar(typeCode))}, Syrup symbols must start with ${quote(toChar(SYMBOL_START))} at index ${start} of ${name}`); -} - -function decodeString(bytes, start, end, name) { - const { start: next, value, typeCode } = decodeStringlike(bytes, start, end, name); - if (typeCode === STRING_START) { - return { start: next, value }; - } - throw Error(`Unexpected type ${quote(toChar(typeCode))}, Syrup strings must start with ${quote(toChar(STRING_START))} at index ${start} of ${name}`); -} - -function decodeBytestring(bytes, start, end, name) { - const { start: next, value, typeCode } = decodeStringlike(bytes, start, end, name); - if (typeCode === BYTES_START) { - return { start: next, value }; - } - throw Error(`Unexpected byte ${quote(toChar(typeCode))}, Syrup bytestrings must start with ${quote(toChar(BYTES_START))} at index ${start} of ${name}`); -} - -function decodeDictionaryKey(bytes, start, end, name) { - const { start: integerPostixIndex } = decodeIntegerPrefix(bytes, start, end); - const typeCode = bytes[integerPostixIndex]; - if (typeCode === SYMBOL_START || typeCode === STRING_START) { - const { start: next, value } = decodeStringlike(bytes, start, end, name); - if (typeCode === SYMBOL_START) { - return { start: next, value: SyrupSymbolFor(value), typeCode }; - } - if (typeCode === STRING_START) { - return { start: next, value, typeCode }; + if (bufferReader.peekByte() === LIST_END) { + bufferReader.skip(1); + return list; } + + list.push(readAny(bufferReader, name)); } - throw Error(`Unexpected byte ${JSON.stringify(String.fromCharCode(typeCode))}, Syrup dictionary keys must be strings or symbols at index ${integerPostixIndex} of ${name}`); -} - -/** - * @param {Uint8Array} bytes - * @param {number} start - * @param {number} end - * @param {string} name - * @param {number} typeCode - * @param {string} typeName - */ -function seekStringOfType(bytes, start, end, name, typeCode, typeName) { - if (start >= end) { - throw Error( - `Unexpected end of Syrup, expected Syrup ${typeName} at end of ${name}`, - ); - } - let length; - ({ start, integer: length } = decodeIntegerPrefix(bytes, start, end)); - - const cc = bytes[start]; - if (cc !== typeCode) { - throw Error( - `Unexpected character ${quote(toChar(cc))}, Syrup ${typeName} must start with ${JSON.stringify(String.fromCharCode(typeCode))}, got ${quote(toChar(cc))} at index ${start} of ${name}`, - ); - } - start += 1; - const subStart = start; - start += Number(length); - if (start > end) { - throw Error( - `Unexpected end of Syrup, expected ${length} bytes after index ${subStart} of ${name}`, - ); - } - return { start }; } /** - * @param {Uint8Array} bytes - * @param {number} start - * @param {number} end + * @param {BufferReader} bufferReader * @param {string} name + * @returns {{value: any, type: 'string' | 'symbol', bytes: Uint8Array}} */ -function seekString(bytes, start, end, name) { - return seekStringOfType(bytes, start, end, name, STRING_START, 'string'); +function readDictionaryKey(bufferReader, name) { + const start = bufferReader.index; + const { value, type } = readNumberPrefixed(bufferReader, name); + if (type === 'string' || type === 'symbol') { + const end = bufferReader.index; + const bytes = bufferReader.bytesAt(start, end - start); + return { value, type, bytes }; + } + throw Error(`Unexpected type ${quote(type)}, Syrup dictionary keys must be strings or symbols at index ${start} of ${name}`); } /** - * @param {Uint8Array} bytes - * @param {number} start - * @param {number} end + * @param {BufferReader} bufferReader * @param {string} name */ -function seekSymbol(bytes, start, end, name) { - return seekStringOfType(bytes, start, end, name, SYMBOL_START, 'symbol'); -} - -/** - * @param {Uint8Array} bytes - * @param {number} start - * @param {number} end - * @param {string} name - */ -function seekBytestring(bytes, start, end, name) { - return seekStringOfType(bytes, start, end, name, BYTES_START, 'bytestring'); -} - -function seekDictionaryKey(bytes, start, end, name) { - const typeInfo = peekType(bytes, start, end, name); - if (typeInfo.type === 'string') { - return seekString(bytes, start, end, name); +function readDictionary(bufferReader, name) { + let cc = bufferReader.readByte(); + if (cc !== DICT_START) { + throw Error(`Unexpected character ${quote(toChar(cc))}, Syrup dictionaries must start with ${quote(toChar(DICT_START))} at index ${bufferReader.index} of ${name}`); } - if (typeInfo.type === 'symbol') { - return seekSymbol(bytes, start, end, name); - } - throw Error( - `Unexpected type ${typeInfo.type}, Syrup dictionary keys must be strings or symbols at index ${start} of ${name}`, - ); -} - -/** - * @param {Uint8Array} bytes - * @param {number} start - * @param {number} end - * @param {string} name - */ -function decodeDictionary(bytes, start, end, name) { const record = {}; let priorKey = undefined; - let priorKeyStart = -1; - let priorKeyEnd = -1; + let priorKeyBytes = undefined; for (;;) { - if (start >= end) { - throw Error( - `Unexpected end of Syrup, expected Syrup string or end of Syrup dictionary marker "}" at ${start} of ${name}`, - ); + if (bufferReader.peekByte() === DICT_END) { + bufferReader.skip(1); + return freeze(record); } - const cc = bytes[start]; - if (cc === DICT_END) { - return { - start: start + 1, - value: freeze(record), - }; - } - const keyStart = start; - let key; - ({ start, value: key } = decodeDictionaryKey(bytes, start, end, name)); - const keyEnd = start; + const start = bufferReader.index; + const { value: newKey, bytes: newKeyBytes } = readDictionaryKey(bufferReader, name); // Validate strictly non-descending keys. - if (priorKeyStart !== -1) { + if (priorKeyBytes !== undefined) { const order = compareByteArrays( - bytes, - bytes, - priorKeyStart, - priorKeyEnd, - keyStart, - keyEnd, + priorKeyBytes, + newKeyBytes, + 0, + priorKeyBytes.length, + 0, + newKeyBytes.length, ); if (order === 0) { throw Error( `Syrup dictionary keys must be unique, got repeated ${JSON.stringify( - key, + newKey, )} at index ${start} of ${name}`, ); } else if (order > 0) { throw Error( `Syrup dictionary keys must be in bytewise sorted order, got ${JSON.stringify( - key, + newKey, )} immediately after ${JSON.stringify( priorKey, )} at index ${start} of ${name}`, ); } } - priorKey = key; - priorKeyStart = keyStart; - priorKeyEnd = keyEnd; + priorKey = newKey; + priorKeyBytes = newKeyBytes; - let value; - // eslint-disable-next-line no-use-before-define - ({ start, value } = decodeAny(bytes, start, end, name)); + const value = readAny(bufferReader, name); - defineProperty(record, key, { + defineProperty(record, newKey, { value, enumerable: true, writable: false, @@ -473,209 +148,139 @@ function decodeDictionary(bytes, start, end, name) { } } -function seekDictionary(bytes, start, end, name) { - let priorKey = undefined; - let priorKeyStart = -1; - let priorKeyEnd = -1; - for (;;) { - if (start >= end) { - throw Error( - `Unexpected end of Syrup, expected Syrup string or end of Syrup dictionary marker "}" at ${start} of ${name}`, - ); - } - const cc = bytes[start]; - if (cc === DICT_END) { - return { - start: start + 1, - }; - } - const keyStart = start; - let key; - ({ start, value: key } = decodeDictionaryKey(bytes, start, end, name)); - const keyEnd = start; - - // Validate strictly non-descending keys. - if (priorKeyStart !== -1) { - const order = compareByteArrays( - bytes, - bytes, - priorKeyStart, - priorKeyEnd, - keyStart, - keyEnd, - ); - if (order === 0) { - throw Error( - `Syrup dictionary keys must be unique, got repeated ${JSON.stringify( - key, - )} at index ${start} of ${name}`, - ); - } else if (order > 0) { - throw Error( - `Syrup dictionary keys must be in bytewise sorted order, got ${JSON.stringify( - key, - )} immediately after ${JSON.stringify( - priorKey, - )} at index ${start} of ${name}`, - ); - } - } - priorKey = key; - priorKeyStart = keyStart; - priorKeyEnd = keyEnd; - - ({ start } = seekAny(bytes, start, end, name)); - } -} - /** - * @param {Uint8Array} bytes - * @param {number} start - * @param {number} end + * @param {BufferReader} bufferReader * @param {string} name */ -function decodeFloat64(bytes, start, end, name) { - const floatStart = start; - start += 8; - if (start > end) { - throw Error( - `Unexpected end of Syrup, expected 8 bytes of a 64 bit floating point number at index ${floatStart} of ${name}`, +function readFloat64(bufferReader, name) { + const cc = bufferReader.readByte(); + if (cc !== DOUBLE) { + throw Error(`Unexpected character ${quote(toChar(cc))}, at index ${bufferReader.index} of ${name}`, ); } - const subarray = bytes.subarray(floatStart, start); - scratchBytes.set(subarray); - const value = scratchData.getFloat64(0, false); // big end + const floatStart = bufferReader.index; + const value = bufferReader.readFloat64(false); // big end if (value === 0) { - if (!isCanonicalZero64(subarray)) { + // @ts-expect-error canonicalZero64 is a frozen array, not a Uint8Array + if (!bufferReader.matchAt(floatStart, canonicalZero64)) { throw Error(`Non-canonical zero at index ${floatStart} of Syrup ${name}`); } } if (Number.isNaN(value)) { - if (!isCanonicalNaN64(subarray)) { + // @ts-expect-error canonicalNaN64 is a frozen array, not a Uint8Array + if (!bufferReader.matchAt(floatStart, canonicalNaN64)) { throw Error(`Non-canonical NaN at index ${floatStart} of Syrup ${name}`); - } + } } - return { start, value }; + return value; } -function peekTypeWithNumberPrefix(bytes, start, end, name) { - const at = seekEndOfInteger(bytes, start, end); - const typePostfix = bytes[at]; - if (typePostfix === PLUS) { - return { type: 'integer', start: at + 1 }; +/** + * @param {BufferReader} bufferReader + * @param {string} name + * @returns {'float64' | 'number-prefix' | 'list' | 'set' | 'dictionary' | 'record' | 'boolean'} + */ +export function peekTypeHint(bufferReader, name) { + const cc = bufferReader.peekByte(); + if (cc >= ZERO && cc <= NINE) { + return 'number-prefix' } - if (typePostfix === MINUS) { - return { type: 'integer', start: at + 1 }; + if (cc === TRUE || cc === FALSE) { + return 'boolean'; + } + if (cc === DOUBLE) { + return 'float64'; + } + if (cc === LIST_START) { + return 'list'; } - // TODO: these start values are not correct bc they dont include the actual string length (?) - // we need to clarify what the string parser/seeker wants - if (typePostfix === BYTES_START) { - const { start: next } = seekBytestring(bytes, start, end, name); - return { type: 'bytestring', start: next }; + if (cc === SET_START) { + return 'set'; } - if (typePostfix === STRING_START) { - const { start: next } = seekString(bytes, start, end, name); - return { type: 'string', start: next }; + if (cc === DICT_START) { + return 'dictionary'; } - if (typePostfix === SYMBOL_START) { - const { start: next } = seekSymbol(bytes, start, end, name); - return { type: 'symbol', start: next }; + if (cc === RECORD_START) { + return 'record'; } + const index = bufferReader.index; throw Error( - `Unexpected character ${JSON.stringify( - String.fromCharCode(typePostfix), - )} at index ${start} of ${name}`, + `Unexpected character ${quote(toChar(cc))}, at index ${index} of ${name}`, ); } /** - * @param {Uint8Array} bytes - * @param {number} start - * @param {number} end + * @param {BufferReader} bufferReader * @param {string} name - * @returns {{type: string, start: number}} + * @returns {{value: any, type: 'integer' | 'bytestring' | 'string' | 'symbol'}} */ -export function peekType(bytes, start, end, name) { - if (start >= end) { - throw Error( - `Unexpected end of Syrup, expected any value at index ${start} of ${name}`, - ); - } - const cc = bytes[start]; - if (cc === DOUBLE) { - return { type: 'float64', start: start + 1 }; - } - if (cc >= ONE && cc <= NINE) { - return peekTypeWithNumberPrefix(bytes, start, end, name); - } - if (cc === ZERO) { - return { type: 'integer', start: start + 1 }; - } - if (cc === LIST_START) { - return { type: 'list', start: start + 1 }; +function readNumberPrefixed(bufferReader, name) { + let start = bufferReader.index; + let end; + let byte; + let nextToken; + // eslint-disable-next-line no-empty + for (;;) { + byte = bufferReader.readByte(); + if (byte < ZERO || byte > NINE) { + end = bufferReader.index - 1; + nextToken = byte; + break; + } } - if (cc === SET_START) { - return { type: 'set', start: start + 1 }; + const numberBuffer = bufferReader.bytesAt(start, end - start); + const numberString = textDecoder.decode(numberBuffer); + + if (nextToken === PLUS) { + const integer = BigInt(numberString); + return { value: integer, type: 'integer' }; } - if (cc === DICT_START) { - return { type: 'dictionary', start: start + 1 }; + if (nextToken === MINUS) { + const integer = BigInt(numberString); + return { value: -integer, type: 'integer' }; } - if (cc === RECORD_START) { - return { type: 'record', start: start + 1 }; + + const number = Number.parseInt(numberString, 10); + const valueBytes = bufferReader.read(number); + if (nextToken === BYTES_START) { + return { value: valueBytes, type: 'bytestring' }; } - if (cc === TRUE) { - return { type: 'boolean', start: start + 1 }; + if (nextToken === STRING_START) { + return { value: textDecoder.decode(valueBytes), type: 'string' }; } - if (cc === FALSE) { - return { type: 'boolean', start: start + 1 }; + if (nextToken === SYMBOL_START) { + const value = textDecoder.decode(valueBytes); + return { value: SyrupSymbolFor(value), type: 'symbol' }; } throw Error( - `Unexpected character ${JSON.stringify( - String.fromCharCode(cc), - )} at index ${start} of ${name}`, + `Unexpected character ${quote(toChar(nextToken))}, at index ${bufferReader.index} of ${name}`, ); } /** - * @param {Uint8Array} bytes - * @param {number} start - * @param {number} end + * @param {BufferReader} bufferReader * @param {string} name - * @returns {{start: number, value: any}} + * @returns {any} */ -function decodeAny(bytes, start, end, name) { - if (start >= end) { - throw Error( - `Unexpected end of Syrup, expected any value at index ${start} of ${name}`, - ); - } - const cc = bytes[start]; - if (cc >= ZERO && cc <= NINE) { - return decodeInteger(bytes, start, end, name); +function readAny(bufferReader, name) { + const type = peekTypeHint(bufferReader, name); + + if (type === 'number-prefix') { + return readNumberPrefixed(bufferReader, name).value; } - if (cc === TRUE || cc === FALSE) { - return decodeBoolean(bytes, start, end, name); + if (type === 'boolean') { + return readBoolean(bufferReader, name); } - const { type, start: next } = peekType(bytes, start, end, name); if (type === 'float64') { - return decodeFloat64(bytes, next, end, name); - } - if (type === 'string') { - return decodeString(bytes, next, end, name); - } - if (type === 'bytestring') { - return decodeBytestring(bytes, next, end, name); - } - if (type === 'symbol') { - return decodeSymbol(bytes, next, end, name); + return readFloat64(bufferReader, name); } if (type === 'list') { - return decodeList(bytes, next, end, name); + return readList(bufferReader, name); } if (type === 'dictionary') { - return decodeDictionary(bytes, next, end, name); + return readDictionary(bufferReader, name); } if (type === 'set') { throw Error( @@ -687,37 +292,13 @@ function decodeAny(bytes, start, end, name) { `decode Records are not yet supported.`, ); } + const index = bufferReader.index; + const cc = bufferReader.readByte(); throw Error( - `Unexpected character ${JSON.stringify( - String.fromCharCode(cc), - )} at index ${start} of ${name}`, + `Unexpected character ${quote(toChar(cc))}, at index ${index} of ${name}`, ); } -function seekAny(bytes, start, end, name) { - const { type, start: next } = peekType(bytes, start, end, name); - // String-likes operate on the start index - if (type === 'symbol') { - return seekSymbol(bytes, start, end, name); - } - if (type === 'bytestring') { - return seekBytestring(bytes, start, end, name); - } - // Non-string-likes operate on the next index - if (type === 'list') { - return seekList(bytes, next, end, name); - } - if (type === 'set') { - return seekSet(bytes, next, end, name); - } - if (type === 'dictionary') { - return seekDictionary(bytes, next, end, name); - } - // TODO: We want to seek to the end of the value, not decode it. - // Decode any provided as a fallback. - return decodeAny(bytes, start, end, name); -} - /** * @param {Uint8Array} bytes * @param {object} options @@ -726,332 +307,327 @@ function seekAny(bytes, start, end, name) { * @param {number} [options.end] */ export function decodeSyrup(bytes, options = {}) { - const { start = 0, end = bytes.byteLength, name = '' } = options; - if (end > bytes.byteLength) { - throw Error( - `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${bytes.byteLength}`, - ); - } - const { start: next, value } = decodeAny(bytes, start, end, name); - if (next !== end) { - throw Error( - `Unexpected trailing bytes after Syrup, length = ${end - next}`, - ); - } - return value; -} - -class SyrupParser { - constructor(bytes, options) { - this.bytes = bytes; - this.state = { - start: options.start ?? 0, - end: options.end ?? bytes.byteLength, - name: options.name ?? '', - }; - } - next() { - const { start, end, name } = this.state; - if (end > this.bytes.byteLength) { - throw Error( - `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, - ); - } - const { start: next, value } = decodeAny(this.bytes, start, end, name); - this.state.start = next; - return value; - } - skip() { - const { start, end, name } = this.state; - const { start: next } = seekAny(this.bytes, start, end, name); - this.state.start = next; - } - peekType() { - const { start, end, name } = this.state; - if (end > this.bytes.byteLength) { - throw Error( - `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, - ); - } - const { type, start: next } = peekType(this.bytes, start, end, name); - return { type, start: next }; - } - nextType() { - const { start, end, name } = this.state; - const { type, start: next } = peekType(this.bytes, start, end, name); - this.state.start = next; - return { type, start: next }; - } - enterRecord() { - const { start, end, name } = this.state; - if (end > this.bytes.byteLength) { - throw Error( - `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, - ); - } - const cc = this.bytes[start]; - if (cc !== RECORD_START) { - throw Error( - `Unexpected character ${JSON.stringify( - String.fromCharCode(cc), - )} at index ${start} of ${name}`, - ); - } - this.state.start = start + 1; - } - exitRecord() { - const { start, end, name } = this.state; - if (end > this.bytes.byteLength) { - throw Error( - `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, - ); - } - const cc = this.bytes[start]; - if (cc !== RECORD_END) { - throw Error( - `Unexpected character ${quote(toChar(cc))}, Syrup records must end with "}", got ${quote(toChar(cc))} at index ${start} of ${name}`, - ); - } - this.state.start = start + 1; - } - nextRecordLabel() { - const { start, end, name } = this.state; - if (end > this.bytes.byteLength) { - throw Error( - `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, - ); - } - const { start: next, value } = decodeSymbol(this.bytes, start, end, name); - this.state.start = next; - return value; - } - enterDictionary() { - const { start, end, name } = this.state; - if (end > this.bytes.byteLength) { - throw Error( - `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, - ); - } - const cc = this.bytes[start]; - if (cc !== DICT_START) { - throw Error( - `Unexpected character ${JSON.stringify( - String.fromCharCode(cc), - )} at index ${start} of ${name}`, - ); - } - this.state.start = start + 1; - } - exitDictionary() { - const { start, end, name } = this.state; - if (end > this.bytes.byteLength) { - throw Error( - `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, - ); - } - const cc = this.bytes[start]; - if (cc !== DICT_END) { - throw Error( - `Unexpected character ${JSON.stringify( - String.fromCharCode(cc), - )} at index ${start} of ${name}`, - ); - } - this.state.start = start + 1; + const { start = 0, name = '' } = options; + const bufferReader = BufferReader.fromBytes(bytes); + if (start !== 0) { + bufferReader.seek(start); } - nextDictionaryKey() { - const { start, end, name } = this.state; - if (end > this.bytes.byteLength) { - throw Error( - `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, - ); - } - const { start: next, value } = decodeDictionaryKey(this.bytes, start, end, name); - this.state.start = next; - return value; - } - nextDictionaryValue() { - const { start, end, name } = this.state; - if (end > this.bytes.byteLength) { - throw Error( - `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, - ); - } - const { start: next, value } = decodeAny(this.bytes, start, end, name); - this.state.start = next; - return value; - } - *iterateDictionaryEntries() { - const { start, end, name } = this.state; - if (end > this.bytes.byteLength) { - throw Error( - `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, - ); - } - let next = start; - while (next < end) { - const cc = this.bytes[next]; - if (cc === DICT_END) { - break; - } - const key = this.nextDictionaryKey(); - const value = this.nextDictionaryValue(); - yield { key, value }; - } - } - *seekDictionaryEntries() { - const { start, end, name } = this.state; - if (end > this.bytes.byteLength) { - throw Error( - `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, - ); - } - let next = start; - while (next < end) { - const cc = this.bytes[next]; - if (cc === DICT_END) { - this.state.start = next + 1; - break; - } - const { start: afterKey } = seekDictionaryKey(this.bytes, next, end, name); - const { start: afterValue } = seekAny(this.bytes, afterKey, end, name); - yield { key: next, value: afterKey, start: afterValue }; - next = afterValue; - this.state.start = next; - } - } - enterList() { - const { start, end, name } = this.state; - if (end > this.bytes.byteLength) { - throw Error( - `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, - ); - } - const cc = this.bytes[start]; - if (cc !== LIST_START) { - throw Error( - `Unexpected character ${quote(toChar(cc))}, Syrup lists must start with "[", got ${quote(toChar(cc))} at index ${start} of ${name}`, - ); - } - this.state.start = start + 1; - } - exitList() { - const { start, end, name } = this.state; - if (end > this.bytes.byteLength) { - throw Error( - `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, - ); - } - const cc = this.bytes[start]; - if (cc !== LIST_END) { - throw Error( - `Unexpected character ${quote(toChar(cc))}, Syrup lists must end with "]", got ${quote(toChar(cc))} at index ${start} of ${name}`, - ); - } - this.state.start = start + 1; - } - nextListValue() { - const { start, end, name } = this.state; - const { start: next, value } = decodeAny(this.bytes, start, end, name); - this.state.start = next; - return value; - } - enterSet() { - const { start, end, name } = this.state; - if (end > this.bytes.byteLength) { - throw Error( - `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, - ); - } - const cc = this.bytes[start]; - if (cc !== SET_START) { - throw Error( - `Unexpected character ${quote(toChar(cc))}, Syrup sets must start with "#", got ${quote(toChar(cc))} at index ${start} of ${name}`, - ); - } - this.state.start = start + 1; - } - exitSet() { - const { start, end, name } = this.state; - if (end > this.bytes.byteLength) { - throw Error( - `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, - ); - } - const cc = this.bytes[start]; - if (cc !== SET_END) { - throw Error( - `Unexpected character ${quote(toChar(cc))}, Syrup sets must end with "$", got ${quote(toChar(cc))} at index ${start} of ${name}`, - ); - } - this.state.start = start + 1; - } - nextSetValue() { - const { start, end, name } = this.state; - const { start: next, value } = decodeAny(this.bytes, start, end, name); - this.state.start = next; - return value; - } - readString() { - const { start, end, name } = this.state; - const { start: next, value, typeCode } = decodeStringlike(this.bytes, start, end, name); - if (typeCode !== STRING_START) { - throw Error(`Unexpected type ${quote(typeCode)}, Syrup strings must start with ${quote(toChar(STRING_START))} at index ${start} of ${name}`); - } - this.state.start = next; - return value; - } - readInteger() { - const { start, end, name } = this.state; - const { start: next, value } = decodeInteger(this.bytes, start, end, name); - this.state.start = next; - return value; - } - readBytestring() { - const { start, end, name } = this.state; - const { start: next, value } = decodeBytestring(this.bytes, start, end, name); - this.state.start = next; - return value; - } - readBoolean() { - const { start, end, name } = this.state; - const { start: next, value } = decodeBoolean(this.bytes, start, end, name); - this.state.start = next; - return value; - } - readSymbolAsString() { - const { start, end, name } = this.state; - const { start: next, value, typeCode } = decodeStringlike(this.bytes, start, end, name); - if (typeCode !== SYMBOL_START) { - throw Error(`Unexpected type ${quote(typeCode)}, Syrup symbols must start with ${quote(toChar(SYMBOL_START))} at index ${start} of ${name}`); - } - this.state.start = next; - return value; - } - readOfType(typeString, opts = {}) { - switch (typeString) { - case 'symbol': - return this.readSymbolAsString(); - case 'string': - return this.readString(); - case 'integer': - return this.readInteger(); - case 'bytestring': - return this.readBytestring(); - case 'boolean': - return this.readBoolean(); - default: - throw Error(`Unknown field type: ${JSON.stringify(typeString)}`); + try { + return readAny(bufferReader, name); + } catch (err) { + if (err.code === 'EOD') { + const err2 = Error(`Unexpected end of Syrup at index ${bufferReader.length} of ${name}`) + err2.cause = err; + throw err2; } + throw err; } } -export function makeSyrupParser(bytes, options = {}) { - const { start = 0, end = bytes.byteLength, name = '' } = options; - if (end > bytes.byteLength) { - throw Error( - `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${bytes.byteLength}`, - ); - } - return new SyrupParser(bytes, options); -} +// class SyrupReader { +// constructor(bytes, options) { +// this.bytes = bytes; +// this.state = { +// start: options.start ?? 0, +// end: options.end ?? bytes.byteLength, +// name: options.name ?? '', +// }; +// } +// next() { +// const { start, end, name } = this.state; +// if (end > this.bytes.byteLength) { +// throw Error( +// `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, +// ); +// } +// const { start: next, value } = decodeAny(this.bytes, start, end, name); +// this.state.start = next; +// return value; +// } +// skip() { +// const { start, end, name } = this.state; +// const { start: next } = seekAny(this.bytes, start, end, name); +// this.state.start = next; +// } +// peekType() { +// const { start, end, name } = this.state; +// if (end > this.bytes.byteLength) { +// throw Error( +// `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, +// ); +// } +// const { type, start: next } = peekType(this.bytes, start, end, name); +// return { type, start: next }; +// } +// nextType() { +// const { start, end, name } = this.state; +// const { type, start: next } = peekType(this.bytes, start, end, name); +// this.state.start = next; +// return { type, start: next }; +// } +// enterRecord() { +// const { start, end, name } = this.state; +// if (end > this.bytes.byteLength) { +// throw Error( +// `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, +// ); +// } +// const cc = this.bytes[start]; +// if (cc !== RECORD_START) { +// throw Error( +// `Unexpected character ${JSON.stringify( +// String.fromCharCode(cc), +// )} at index ${start} of ${name}`, +// ); +// } +// this.state.start = start + 1; +// } +// exitRecord() { +// const { start, end, name } = this.state; +// if (end > this.bytes.byteLength) { +// throw Error( +// `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, +// ); +// } +// const cc = this.bytes[start]; +// if (cc !== RECORD_END) { +// throw Error( +// `Unexpected character ${quote(toChar(cc))}, Syrup records must end with "}", got ${quote(toChar(cc))} at index ${start} of ${name}`, +// ); +// } +// this.state.start = start + 1; +// } +// nextRecordLabel() { +// const { start, end, name } = this.state; +// if (end > this.bytes.byteLength) { +// throw Error( +// `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, +// ); +// } +// const { start: next, value } = decodeSymbol(this.bytes, start, end, name); +// this.state.start = next; +// return value; +// } +// enterDictionary() { +// const { start, end, name } = this.state; +// if (end > this.bytes.byteLength) { +// throw Error( +// `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, +// ); +// } +// const cc = this.bytes[start]; +// if (cc !== DICT_START) { +// throw Error( +// `Unexpected character ${JSON.stringify( +// String.fromCharCode(cc), +// )} at index ${start} of ${name}`, +// ); +// } +// this.state.start = start + 1; +// } +// exitDictionary() { +// const { start, end, name } = this.state; +// if (end > this.bytes.byteLength) { +// throw Error( +// `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, +// ); +// } +// const cc = this.bytes[start]; +// if (cc !== DICT_END) { +// throw Error( +// `Unexpected character ${JSON.stringify( +// String.fromCharCode(cc), +// )} at index ${start} of ${name}`, +// ); +// } +// this.state.start = start + 1; +// } +// nextDictionaryKey() { +// const { start, end, name } = this.state; +// if (end > this.bytes.byteLength) { +// throw Error( +// `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, +// ); +// } +// const { start: next, value } = decodeDictionaryKey(this.bytes, start, end, name); +// this.state.start = next; +// return value; +// } +// nextDictionaryValue() { +// const { start, end, name } = this.state; +// if (end > this.bytes.byteLength) { +// throw Error( +// `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, +// ); +// } +// const { start: next, value } = decodeAny(this.bytes, start, end, name); +// this.state.start = next; +// return value; +// } +// *iterateDictionaryEntries() { +// const { start, end, name } = this.state; +// if (end > this.bytes.byteLength) { +// throw Error( +// `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, +// ); +// } +// let next = start; +// while (next < end) { +// const cc = this.bytes[next]; +// if (cc === DICT_END) { +// break; +// } +// const key = this.nextDictionaryKey(); +// const value = this.nextDictionaryValue(); +// yield { key, value }; +// } +// } +// *seekDictionaryEntries() { +// const { start, end, name } = this.state; +// if (end > this.bytes.byteLength) { +// throw Error( +// `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, +// ); +// } +// let next = start; +// while (next < end) { +// const cc = this.bytes[next]; +// if (cc === DICT_END) { +// this.state.start = next + 1; +// break; +// } +// const { start: afterKey } = seekDictionaryKey(this.bytes, next, end, name); +// const { start: afterValue } = seekAny(this.bytes, afterKey, end, name); +// yield { key: next, value: afterKey, start: afterValue }; +// next = afterValue; +// this.state.start = next; +// } +// } +// enterList() { +// const { start, end, name } = this.state; +// if (end > this.bytes.byteLength) { +// throw Error( +// `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, +// ); +// } +// const cc = this.bytes[start]; +// if (cc !== LIST_START) { +// throw Error( +// `Unexpected character ${quote(toChar(cc))}, Syrup lists must start with "[", got ${quote(toChar(cc))} at index ${start} of ${name}`, +// ); +// } +// this.state.start = start + 1; +// } +// exitList() { +// const { start, end, name } = this.state; +// if (end > this.bytes.byteLength) { +// throw Error( +// `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, +// ); +// } +// const cc = this.bytes[start]; +// if (cc !== LIST_END) { +// throw Error( +// `Unexpected character ${quote(toChar(cc))}, Syrup lists must end with "]", got ${quote(toChar(cc))} at index ${start} of ${name}`, +// ); +// } +// this.state.start = start + 1; +// } +// nextListValue() { +// const { start, end, name } = this.state; +// const { start: next, value } = decodeAny(this.bytes, start, end, name); +// this.state.start = next; +// return value; +// } +// enterSet() { +// const { start, end, name } = this.state; +// if (end > this.bytes.byteLength) { +// throw Error( +// `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, +// ); +// } +// const cc = this.bytes[start]; +// if (cc !== SET_START) { +// throw Error( +// `Unexpected character ${quote(toChar(cc))}, Syrup sets must start with "#", got ${quote(toChar(cc))} at index ${start} of ${name}`, +// ); +// } +// this.state.start = start + 1; +// } +// exitSet() { +// const { start, end, name } = this.state; +// if (end > this.bytes.byteLength) { +// throw Error( +// `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, +// ); +// } +// const cc = this.bytes[start]; +// if (cc !== SET_END) { +// throw Error( +// `Unexpected character ${quote(toChar(cc))}, Syrup sets must end with "$", got ${quote(toChar(cc))} at index ${start} of ${name}`, +// ); +// } +// this.state.start = start + 1; +// } +// nextSetValue() { +// const { start, end, name } = this.state; +// const { start: next, value } = decodeAny(this.bytes, start, end, name); +// this.state.start = next; +// return value; +// } + +// readString() { +// const { start, end, name } = this.state; +// const { start: next, value, typeCode } = decodeStringlike(this.bytes, start, end, name); +// if (typeCode !== STRING_START) { +// throw Error(`Unexpected type ${quote(typeCode)}, Syrup strings must start with ${quote(toChar(STRING_START))} at index ${start} of ${name}`); +// } +// this.state.start = next; +// return value; +// } +// readInteger() { +// const { start, end, name } = this.state; +// const { start: next, value } = decodeInteger(this.bytes, start, end, name); +// this.state.start = next; +// return value; +// } +// readBytestring() { +// const { start, end, name } = this.state; +// const { start: next, value } = decodeBytestring(this.bytes, start, end, name); +// this.state.start = next; +// return value; +// } +// readBoolean() { +// const { start, end, name } = this.state; +// const { start: next, value } = decodeBoolean(this.bytes, start, end, name); +// this.state.start = next; +// return value; +// } +// readSymbolAsString() { +// const { start, end, name } = this.state; +// const { start: next, value, typeCode } = decodeStringlike(this.bytes, start, end, name); +// if (typeCode !== SYMBOL_START) { +// throw Error(`Unexpected type ${quote(typeCode)}, Syrup symbols must start with ${quote(toChar(SYMBOL_START))} at index ${start} of ${name}`); +// } +// this.state.start = next; +// return value; +// } +// readOfType(typeString, opts = {}) { +// switch (typeString) { +// case 'symbol': +// return this.readSymbolAsString(); +// case 'string': +// return this.readString(); +// case 'integer': +// return this.readInteger(); +// case 'bytestring': +// return this.readBytestring(); +// case 'boolean': +// return this.readBoolean(); +// default: +// throw Error(`Unknown field type: ${JSON.stringify(typeString)}`); +// } +// } +// } + +// export const makeSyrupReader = (bytes, options) => new SyrupReader(bytes, options); \ No newline at end of file diff --git a/packages/syrup/test/decode.test.js b/packages/syrup/test/decode.test.js index 4482cba898..bc293f8aec 100644 --- a/packages/syrup/test/decode.test.js +++ b/packages/syrup/test/decode.test.js @@ -12,8 +12,12 @@ test('affirmative decode cases', t => { for (let i = 0; i < syrup.length; i += 1) { bytes[i] = syrup.charCodeAt(i); } - const actual = decodeSyrup(bytes); - t.deepEqual(actual, value, `for ${String(syrup)}`); + const desc = `for ${String(syrup)}` + let actual; + t.notThrows(() => { + actual = decodeSyrup(bytes); + }, desc); + t.deepEqual(actual, value, desc); } }); @@ -24,7 +28,7 @@ test('must not be empty', t => { }, { message: - 'Unexpected end of Syrup, expected any value at index 0 of known.sup', + 'Unexpected end of Syrup at index 0 of known.sup', }, ); }); @@ -36,7 +40,7 @@ test('dictionary keys must be unique', t => { }, { message: - 'Syrup dictionary keys must be unique, got repeated "a" at index 10 of ', + 'Syrup dictionary keys must be unique, got repeated "a" at index 7 of ', }, ); }); @@ -48,7 +52,7 @@ test('dictionary keys must be in bytewise order', t => { }, { message: - 'Syrup dictionary keys must be in bytewise sorted order, got "a" immediately after "b" at index 10 of ', + 'Syrup dictionary keys must be in bytewise sorted order, got "a" immediately after "b" at index 7 of ', }, ); }); @@ -56,23 +60,23 @@ test('dictionary keys must be in bytewise order', t => { test('must reject out-of-order prefix key', t => { t.throws( () => { - decodeSyrup(textEncoder.encode('{1"i10+0"')); + decodeSyrup(textEncoder.encode('{1"i10+0"1-}')); }, { message: - 'Syrup dictionary keys must be in bytewise sorted order, got "" immediately after "i" at index 9 of ', + 'Syrup dictionary keys must be in bytewise sorted order, got "" immediately after "i" at index 7 of ', }, ); }); -test('dictionary keys must be strings', t => { +test('dictionary keys must be strings or symbols', t => { t.throws( () => { decodeSyrup(textEncoder.encode('{1+')); }, { message: - 'Unexpected byte "+", Syrup dictionary keys must be strings or symbols at index 2 of ', + 'Unexpected type "integer", Syrup dictionary keys must be strings or symbols at index 1 of ', }, ); }); diff --git a/packages/syrup/test/ocapn.test.js b/packages/syrup/test/ocapn.test.js index 63646fcd86..19d6c9a934 100644 --- a/packages/syrup/test/ocapn.test.js +++ b/packages/syrup/test/ocapn.test.js @@ -1,35 +1,35 @@ -// @ts-check +// // @ts-check -import test from 'ava'; -import { makeSyrupParser } from '../src/decode.js'; -import { readOCapDescriptor, readOCapNMessage } from '../src/ocapn.js'; -import { descriptorsTable, operationsTable } from './_ocapn.js'; +// import test from 'ava'; +// import { makeSyrupReader } from '../src/decode.js'; +// import { readOCapDescriptor, readOCapNMessage } from '../src/ocapn.js'; +// import { descriptorsTable, operationsTable } from './_ocapn.js'; -test('affirmative descriptor read cases', t => { - for (const { syrup, value } of descriptorsTable) { - // We test with a length guess of 1 to maximize the probability - // of discovering a fault in the buffer resize code. - const bytes = new Uint8Array(syrup.length); - for (let i = 0; i < syrup.length; i += 1) { - bytes[i] = syrup.charCodeAt(i); - } - const parser = makeSyrupParser(bytes, { name: syrup }); - let descriptor; - t.notThrows(() => { - descriptor = readOCapDescriptor(parser); - }, `for ${JSON.stringify(syrup)}`); - t.deepEqual(descriptor, value, `for ${JSON.stringify(syrup)}`); - } -}); +// test('affirmative descriptor read cases', t => { +// for (const { syrup, value } of descriptorsTable) { +// // We test with a length guess of 1 to maximize the probability +// // of discovering a fault in the buffer resize code. +// const bytes = new Uint8Array(syrup.length); +// for (let i = 0; i < syrup.length; i += 1) { +// bytes[i] = syrup.charCodeAt(i); +// } +// const syrupReader = makeSyrupReader(bytes, { name: syrup }); +// let descriptor; +// t.notThrows(() => { +// descriptor = readOCapDescriptor(syrupReader); +// }, `for ${JSON.stringify(syrup)}`); +// t.deepEqual(descriptor, value, `for ${JSON.stringify(syrup)}`); +// } +// }); -test('affirmative operation read cases', t => { - for (const { syrup, value } of operationsTable) { - const bytes = new Uint8Array(syrup.length); - for (let i = 0; i < syrup.length; i += 1) { - bytes[i] = syrup.charCodeAt(i); - } - const parser = makeSyrupParser(bytes); - const message = readOCapNMessage(parser); - t.deepEqual(message, value, `for ${JSON.stringify(syrup)}`); - } -}); +// test('affirmative operation read cases', t => { +// for (const { syrup, value } of operationsTable) { +// const bytes = new Uint8Array(syrup.length); +// for (let i = 0; i < syrup.length; i += 1) { +// bytes[i] = syrup.charCodeAt(i); +// } +// const syrupReader = makeSyrupReader(bytes); +// const message = readOCapNMessage(syrupReader); +// t.deepEqual(message, value, `for ${JSON.stringify(syrup)}`); +// } +// }); diff --git a/packages/syrup/test/parse.test.js b/packages/syrup/test/parse.test.js index 9c0fcfae59fdf0f1b3c6471cd6b0074702726cab..7f4159291a479859831c782537657f9ff65232a6 100644 GIT binary patch literal 8809 zcmdT}U2@wt5bkqNfpIfKI+dwNR^(W*)iiaQho)&KcKedaWIz&<7*nJQLXK6BkJjmt zdWG)dpOR?FiQ`Tc$EJwI;@fW*iv`eblXjbY4ga(0tmJ|SRtYlV<&*^Uf(B2$riX-9 zWCTC5byU?FXH-md3qID`u1Ly~GG(WWX6Ep|l%B(PKA*i|G-bR+W{jPmEoO{d zYc)~Su6WW`U84^;9D;$LKg%+TvqH&DgqT%>c6*$O$vlpda@sBvIxE^0YHth9Si9Tp z^pZF}NFFiv=t-x4csNSgsNZ90oOFA=9vyY#xRZ90A_Mb z(6UH>4GZ>>{PyNmsECWEOwee~^W6xgu*0q-PiXuDmkml z{DOsctQHC0Yb%3Y(4!4Ucwvwg31?GQ2$};TUS-9Ylq2$SQs#_Y(0tB<77^rlNUn50 zqHU3s0Epmfe}|4)r~7Sh2az7^oY5Fi(}SIA#*&Ox`#T90*UF1Pg;5~brFc=GFFB|| z2F~1@P#Nrr#|uCVf?l_X$?l2!-0L9+)W&~^i1+HiOWmIvh8j)+glV4DBW)R8nt(pdgyc+scQ1vUkd|XJMK#Za_#_)~q*=f`1|rie5m{MKzCeBj z{aFYx5G_R1_WAH^RNom$Jvr_gC(bm3QO{#`nF;4$?FcgkVHQ4pN}iXH=x9+5Ajy@S zjC7KNmQzTDkON0~SP{#sU<$SxSkAeP6B1GFP}9DeK6zh+~)PGrxCJjZM)IG~08`vV}ywVm4cn3%zc zwV!YH>EZ`}!b(GVXE#JNE9daqh?tHE!KksEFNfr4YW8~L_2f^pwJ`wBl!#~KqK55|+S@?ykV z_)hg3;-80Y{M}d>-BgustVdMEY*q(T418We!ezeMSd6H&WQzbB;9#pvQ?P>AR4vmF zI@Q!auCQ1smo^qmPOZb(cLkFznX2<_X5kUs5{)trdlkr=P}ziP5wXY&cWVj@Pswuq zB&DH(T9X&;nk|QhIa)&2)MMw6`t8Vf!3ipKY;+IT&><^GTys2gtkYKMNo}oZfo|!( zjSh6G9pwE-K4j~|rm`1f$pi1fhl~Thm3wFp_}0)jf>)|N;E(ITE%c@EL&VD=s0*PQ z&h6q5wKc?|S9YV6AAR@DO|N$=Zt8QSVs46{8Cy*g6kO&7v!S=&0=>?Ay(aI;z&mCB zWI^4vm|JzIOHDPO-U9qa_-+Vi#ba;3h$!)+cM5Bw(2oe6kDSu`#eJ|G9~*ItNycp& z{f}VxSAymy*tRC{yn_3;)!@P^!8h+7v)Dm**%Qt2gITPcb2!P=(X+IYqMZ#}bZ_ZF zspXk!S5tM)=B=P8Ewksy8}|N^{FB;(iePS}r^+rJ%vf#2YZL3U=n;IbPfZaBet?;Z zT|mlmF1bH$4xijAbmXqe;#4OGE8J`-V3pO&(X*82TGe|<5cW) zYQ-$tdSkE&-|r0>@vhcRr{2-p>8`9kO?bUjs72NuWGVN`p!SPqS2lX_vZZAkV#TTU z?b@#gc9T^Aw3fD=vU|bV?JO-v>v3A{J?3k>0WIJ)*LHa3bW!bNE4YV}H!2r!-b9Hr z8TfOTD*vPHD@Gga7r~QEZC}*}ejS&-s9O3OWkFOHl1eS?y=s!OspbQahvOW$T!Je` z0HXG*cp#|yZ4vCB5a|Ba%XokFwm~6K_jCZ9FV4!-WWuHtw(_BTv5S{!zr-H^eT284 zFF*>8A4AoXAQOBMP3bHQuLx{KTB?4~mmVV^lO;)Z$Lv`5EQhn8z2s6?vR*c02 zcHy|c(fhOAmQ!Sp7>oj0x3utv6s{ZVDpW}$V9OBfM)=3-d1q9@X^T&WbkYgzmbJaW z$M>Amh2#KkF6G4=&KHzX7d?iVU7a8lt5%WaRWfFH4sE;c4mgOsls40hU1H?0)b_W# z6?vlvSEBOVJ+LO>5iHz5$sLiQOJDh^z;>egtTX^|UC_8Tby5l~cF8r$^zkTmvS&f2 z-?gF957}iKqg?y8Z&_a}u1s!`It8{5PFZyT^Q9+Kr|Yd6&9eyr8Luf|cM(8NlZ}9S z1UUg0LK2D3sS|1>)r1}NRV&|@X0^7)ZN;j@br+;sMGc(V_8Ta*GL4J^leobC3$Lse A5&!@I literal 8047 zcmd5>ZFAE`5bo#vikrZUWN>8sf1?Z!CHHg=aWna8o&Kc$ zh3?*~BqW?6;Hi1BrQ2IQ`|RHC-l^9kd&zQ@b3p_v1ex)CNj!Q%y~pPMJwgjIgTHcU zR=ANB6$^bEf4YGy60<0e*}Kg$%+uF&$+~1o&*7S{S8pgU81Ir5W9Mg^6(iSLM-cQ1 z9`)4J;17JzUav>~%JU#hG9|PSVpU9fy?G)Q>oAD&WiOBDD(e-fx+geey}_VAio)

i6CsT5TlL%2JNE+br z-qmoQ&RKtOf3%MP5BJY#2w>^qez9Uv!iwR3M8!35T$(M|rFfp9=Q!vaePa}8(GL5KZvbLZs zorI)Jfb%YeTwRt+v)+(TLds+sS)^0IiUh*qBoaxUQNBSQ1?2)rmJAZ(8;DKDPpzC% zYaabG+f=>i1YxKd0t~=<0DpQCp;7fa278+_IoPaD4s|g!q&hV6>rr8BDOjlImXK_S zegj}S#=jjlTq+WmUV`j05zcu?wOl}u2_8Qt&vHmdG%F^MigHe7m>}TxJIGd$re;y7W99bgVi*#X?8VhS0{50g1zS)9##KOR85TK-7ijjlNX6dE*ME5 zD(8#?qGR|uy{{F&W^>v!exS#nVKx&S5W>vi1V}KZ*qfLy0d?cYUyIqnBQOSYXKbud z)*T`d3 z6#C+h{w8EJnI*-dbh%6-GntU_lI-|h{cGgmO4b*)DC+C6M^lKQAN8fMw?DCD zzA%@kPsmJuj22I?L4B>FN11dj?==}X-eTY3B%~SqF_sf@I7zaCMV1Rf8jTw7zhpVL zMAmGYwb|4^S^fLZ-|+XO#jNpUnP(7tdbr)P{@_5%w79MX_D>XeYEq@6#tg z;HL%k!^wP+7sho8^aqfg?%nIPHvz4k*484d`ahXJP5rt>73Oluez^jxWvxn}3N*;U za)CpFwOOqr`7wpwRnuxyu(d643LU7iZVPN9B&Uwe=$tcZ zbdC(9cg#jRYIQO6xwBmMaQ?a}+O2q#X6_?F#E*#qnRp zDi#U132x_5$JJqo{ja+{)EQM&J4fRyI zGi-x?xga!PQ|y+wbv-f;l?L2yrgThOz?%Yj{(S+b5|lW*z{&au{C~bN!?=N65tKTn z_GZp%*Nc>eRHW;nfa2R&B%ZNC;+ZTWniWRK)}O zblC1Rj5j|&9)<_2w-kVsZ_e^}(Sj{0Y({-~ql0fVehvQs!~wo+dJa->?&QNXf=2UA zu%s*Bzar3%cU4i=htse$aPoU8N4T}0bP6{1@CyO^Hql+U_<700{l`+K1gLy->m7<4X3dE)7F9YPdTR>X)(MdlCM25 z;88}Mz#ASqSZEZ1R*R)j@?$9V?YN#RE3BQ(E-@-tY$vOxUA{?y7f1r~b!VUl)VUelwlz)a(C1|+ZVxErjB80$Ir!>^1gZAeSdrQ& ztL*BMs-0>AE;4mI@cov8%hl~`!FW=ExphD|HP)%sh;RbskSNpVBwG0+1pEgKs*e5p d5$Foab^;}qTk~hNw9seA+t{-Lt>j@r`wybcnP&h1 From bfc79da4bd6e4d6d37486e9b8ecc040d0f41fb65 Mon Sep 17 00:00:00 2001 From: kumavis Date: Thu, 3 Apr 2025 12:17:42 -1000 Subject: [PATCH 09/31] refactor(syrup): use syrupReader to iteratively read expected structure --- packages/syrup/src/buffer-reader.js | 17 +- packages/syrup/src/decode.js | 679 ++++++++++++---------------- packages/syrup/test/parse.test.js | Bin 8809 -> 0 bytes packages/syrup/test/reader.test.js | Bin 0 -> 5353 bytes 4 files changed, 302 insertions(+), 394 deletions(-) delete mode 100644 packages/syrup/test/parse.test.js create mode 100644 packages/syrup/test/reader.test.js diff --git a/packages/syrup/src/buffer-reader.js b/packages/syrup/src/buffer-reader.js index f61dec3353..25b4add9bd 100644 --- a/packages/syrup/src/buffer-reader.js +++ b/packages/syrup/src/buffer-reader.js @@ -34,6 +34,10 @@ export class BufferReader { }); } + /** + * @param {Uint8Array} bytes + * @returns {BufferReader} + */ static fromBytes(bytes) { const empty = new ArrayBuffer(0); const reader = new BufferReader(empty); @@ -42,7 +46,11 @@ export class BufferReader { fields.data = new DataView(bytes.buffer); fields.length = bytes.length; fields.index = 0; - fields.offset = 0; + fields.offset = bytes.byteOffset; + // Temporary check until we can handle non-zero byteOffset + if (fields.offset !== 0) { + throw Error('Cannot create BufferReader from Uint8Array with a non-zero byteOffset'); + } return reader; } @@ -89,6 +97,13 @@ export class BufferReader { */ canSeek(index) { const fields = privateFieldsGet(this); + if (!(index >= 0 && fields.offset + index <= fields.length)) + console.log('CANT SEEK', { + index, + offset: fields.offset, + length: fields.length, + canSeek: index >= 0 && fields.offset + index <= fields.length, + }); return index >= 0 && fields.offset + index <= fields.length; } diff --git a/packages/syrup/src/decode.js b/packages/syrup/src/decode.js index 6957598e7a..5159907963 100644 --- a/packages/syrup/src/decode.js +++ b/packages/syrup/src/decode.js @@ -51,6 +51,144 @@ function readBoolean(bufferReader, name) { throw Error(`Unexpected byte ${quote(toChar(cc))}, Syrup booleans must start with ${quote(toChar(TRUE))} or ${quote(toChar(FALSE))} at index ${bufferReader.index} of ${name}`); } +/** + * @param {BufferReader} bufferReader + * @param {string} name + * @returns {{value: any, type: 'integer' | 'bytestring' | 'string' | 'symbol'}} + */ +function readNumberPrefixed(bufferReader, name) { + let start = bufferReader.index; + let end; + let byte; + let nextToken; + + // eslint-disable-next-line no-empty + for (;;) { + byte = bufferReader.readByte(); + + if (byte < ZERO || byte > NINE) { + end = bufferReader.index - 1; + if (start === end) { + throw Error(`Unexpected character ${quote(toChar(byte))}, expected a number at index ${bufferReader.index} of ${name}`); + } + nextToken = byte; + break; + } + } + const numberBuffer = bufferReader.bytesAt(start, end - start); + const numberString = textDecoder.decode(numberBuffer); + + if (nextToken === PLUS) { + const integer = BigInt(numberString); + return { value: integer, type: 'integer' }; + } + if (nextToken === MINUS) { + const integer = BigInt(numberString); + return { value: -integer, type: 'integer' }; + } + + const number = Number.parseInt(numberString, 10); + const valueBytes = bufferReader.read(number); + if (nextToken === BYTES_START) { + return { value: valueBytes, type: 'bytestring' }; + } + if (nextToken === STRING_START) { + return { value: textDecoder.decode(valueBytes), type: 'string' }; + } + if (nextToken === SYMBOL_START) { + return { value: textDecoder.decode(valueBytes), type: 'symbol' }; + } + throw Error( + `Unexpected character ${quote(toChar(nextToken))}, at index ${bufferReader.index} of ${name}`, + ); +} + +/** + * @param {BufferReader} bufferReader + * @param {string} name + * @returns {bigint} + */ +function readInteger(bufferReader, name) { + const { value, type } = readNumberPrefixed(bufferReader, name); + if (type !== 'integer') { + throw Error(`Unexpected type ${quote(type)}, Syrup integers must start with ${quote(toChar(PLUS))} or ${quote(toChar(MINUS))} at index ${bufferReader.index} of ${name}`); + } + return value; +} + +/** + * @param {BufferReader} bufferReader + * @param {string} expectedType + * @param {string} name + * @returns {string} + */ +function readStringlikeAndAssertType(bufferReader, expectedType, name) { + const start = bufferReader.index; + const { value, type } = readNumberPrefixed(bufferReader, name); + if (type !== expectedType) { + throw Error(`Unexpected type ${quote(type)}, Syrup ${expectedType} must start with ${quote(toChar(expectedType))} at index ${start} of ${name}`); + } + return value; +} + +/** + * @param {BufferReader} bufferReader + * @param {string} name + * @returns {string} + */ +function readString(bufferReader, name) { + return readStringlikeAndAssertType(bufferReader, 'string', name); +} + +/** + * @param {BufferReader} bufferReader + * @param {string} name + * @returns {string} + */ +function readSymbolAsString(bufferReader, name) { + return readStringlikeAndAssertType(bufferReader, 'symbol', name); +} + +/** + * @param {BufferReader} bufferReader + * @param {string} name + * @returns {string} + */ +function readBytestring(bufferReader, name) { + return readStringlikeAndAssertType(bufferReader, 'bytestring', name); +} + + +/** + * @param {BufferReader} bufferReader + * @param {string} name + */ +function readFloat64(bufferReader, name) { + const cc = bufferReader.readByte(); + if (cc !== DOUBLE) { + throw Error(`Unexpected character ${quote(toChar(cc))}, at index ${bufferReader.index} of ${name}`, + ); + } + const floatStart = bufferReader.index; + const value = bufferReader.readFloat64(false); // big end + + if (value === 0) { + // @ts-expect-error canonicalZero64 is a frozen array, not a Uint8Array + if (!bufferReader.matchAt(floatStart, canonicalZero64)) { + throw Error(`Non-canonical zero at index ${floatStart} of Syrup ${name}`); + } + } + if (Number.isNaN(value)) { + // @ts-expect-error canonicalNaN64 is a frozen array, not a Uint8Array + if (!bufferReader.matchAt(floatStart, canonicalNaN64)) { + throw Error(`Non-canonical NaN at index ${floatStart} of Syrup ${name}`); + } + } + + return value; +} + + /** * @param {BufferReader} bufferReader * @param {string} name @@ -83,6 +221,9 @@ function readDictionaryKey(bufferReader, name) { if (type === 'string' || type === 'symbol') { const end = bufferReader.index; const bytes = bufferReader.bytesAt(start, end - start); + if (type === 'symbol') { + return { value: SyrupSymbolFor(value), type, bytes }; + } return { value, type, bytes }; } throw Error(`Unexpected type ${quote(type)}, Syrup dictionary keys must be strings or symbols at index ${start} of ${name}`); @@ -148,35 +289,6 @@ function readDictionary(bufferReader, name) { } } -/** - * @param {BufferReader} bufferReader - * @param {string} name - */ -function readFloat64(bufferReader, name) { - const cc = bufferReader.readByte(); - if (cc !== DOUBLE) { - throw Error(`Unexpected character ${quote(toChar(cc))}, at index ${bufferReader.index} of ${name}`, - ); - } - const floatStart = bufferReader.index; - const value = bufferReader.readFloat64(false); // big end - - if (value === 0) { - // @ts-expect-error canonicalZero64 is a frozen array, not a Uint8Array - if (!bufferReader.matchAt(floatStart, canonicalZero64)) { - throw Error(`Non-canonical zero at index ${floatStart} of Syrup ${name}`); - } - } - if (Number.isNaN(value)) { - // @ts-expect-error canonicalNaN64 is a frozen array, not a Uint8Array - if (!bufferReader.matchAt(floatStart, canonicalNaN64)) { - throw Error(`Non-canonical NaN at index ${floatStart} of Syrup ${name}`); - } - } - - return value; -} - /** * @param {BufferReader} bufferReader * @param {string} name @@ -211,83 +323,39 @@ export function peekTypeHint(bufferReader, name) { ); } -/** - * @param {BufferReader} bufferReader - * @param {string} name - * @returns {{value: any, type: 'integer' | 'bytestring' | 'string' | 'symbol'}} - */ -function readNumberPrefixed(bufferReader, name) { - let start = bufferReader.index; - let end; - let byte; - let nextToken; - // eslint-disable-next-line no-empty - for (;;) { - byte = bufferReader.readByte(); - if (byte < ZERO || byte > NINE) { - end = bufferReader.index - 1; - nextToken = byte; - break; - } - } - const numberBuffer = bufferReader.bytesAt(start, end - start); - const numberString = textDecoder.decode(numberBuffer); - - if (nextToken === PLUS) { - const integer = BigInt(numberString); - return { value: integer, type: 'integer' }; - } - if (nextToken === MINUS) { - const integer = BigInt(numberString); - return { value: -integer, type: 'integer' }; - } - - const number = Number.parseInt(numberString, 10); - const valueBytes = bufferReader.read(number); - if (nextToken === BYTES_START) { - return { value: valueBytes, type: 'bytestring' }; - } - if (nextToken === STRING_START) { - return { value: textDecoder.decode(valueBytes), type: 'string' }; - } - if (nextToken === SYMBOL_START) { - const value = textDecoder.decode(valueBytes); - return { value: SyrupSymbolFor(value), type: 'symbol' }; - } - throw Error( - `Unexpected character ${quote(toChar(nextToken))}, at index ${bufferReader.index} of ${name}`, - ); -} - /** * @param {BufferReader} bufferReader * @param {string} name * @returns {any} */ function readAny(bufferReader, name) { - const type = peekTypeHint(bufferReader, name); + const typeHint = peekTypeHint(bufferReader, name); - if (type === 'number-prefix') { - return readNumberPrefixed(bufferReader, name).value; + if (typeHint === 'number-prefix') { + const { value, type } = readNumberPrefixed(bufferReader, name); + if (type === 'symbol') { + return SyrupSymbolFor(value); + } + return value; } - if (type === 'boolean') { + if (typeHint === 'boolean') { return readBoolean(bufferReader, name); } - if (type === 'float64') { + if (typeHint === 'float64') { return readFloat64(bufferReader, name); } - if (type === 'list') { + if (typeHint === 'list') { return readList(bufferReader, name); } - if (type === 'dictionary') { + if (typeHint === 'dictionary') { return readDictionary(bufferReader, name); } - if (type === 'set') { + if (typeHint === 'set') { throw Error( `decode Sets are not yet supported.`, ); } - if (type === 'record') { + if (typeHint === 'record') { throw Error( `decode Records are not yet supported.`, ); @@ -324,310 +392,135 @@ export function decodeSyrup(bytes, options = {}) { } } -// class SyrupReader { -// constructor(bytes, options) { -// this.bytes = bytes; -// this.state = { -// start: options.start ?? 0, -// end: options.end ?? bytes.byteLength, -// name: options.name ?? '', -// }; -// } -// next() { -// const { start, end, name } = this.state; -// if (end > this.bytes.byteLength) { -// throw Error( -// `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, -// ); -// } -// const { start: next, value } = decodeAny(this.bytes, start, end, name); -// this.state.start = next; -// return value; -// } -// skip() { -// const { start, end, name } = this.state; -// const { start: next } = seekAny(this.bytes, start, end, name); -// this.state.start = next; -// } -// peekType() { -// const { start, end, name } = this.state; -// if (end > this.bytes.byteLength) { -// throw Error( -// `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, -// ); -// } -// const { type, start: next } = peekType(this.bytes, start, end, name); -// return { type, start: next }; -// } -// nextType() { -// const { start, end, name } = this.state; -// const { type, start: next } = peekType(this.bytes, start, end, name); -// this.state.start = next; -// return { type, start: next }; -// } -// enterRecord() { -// const { start, end, name } = this.state; -// if (end > this.bytes.byteLength) { -// throw Error( -// `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, -// ); -// } -// const cc = this.bytes[start]; -// if (cc !== RECORD_START) { -// throw Error( -// `Unexpected character ${JSON.stringify( -// String.fromCharCode(cc), -// )} at index ${start} of ${name}`, -// ); -// } -// this.state.start = start + 1; -// } -// exitRecord() { -// const { start, end, name } = this.state; -// if (end > this.bytes.byteLength) { -// throw Error( -// `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, -// ); -// } -// const cc = this.bytes[start]; -// if (cc !== RECORD_END) { -// throw Error( -// `Unexpected character ${quote(toChar(cc))}, Syrup records must end with "}", got ${quote(toChar(cc))} at index ${start} of ${name}`, -// ); -// } -// this.state.start = start + 1; -// } -// nextRecordLabel() { -// const { start, end, name } = this.state; -// if (end > this.bytes.byteLength) { -// throw Error( -// `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, -// ); -// } -// const { start: next, value } = decodeSymbol(this.bytes, start, end, name); -// this.state.start = next; -// return value; -// } -// enterDictionary() { -// const { start, end, name } = this.state; -// if (end > this.bytes.byteLength) { -// throw Error( -// `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, -// ); -// } -// const cc = this.bytes[start]; -// if (cc !== DICT_START) { -// throw Error( -// `Unexpected character ${JSON.stringify( -// String.fromCharCode(cc), -// )} at index ${start} of ${name}`, -// ); -// } -// this.state.start = start + 1; -// } -// exitDictionary() { -// const { start, end, name } = this.state; -// if (end > this.bytes.byteLength) { -// throw Error( -// `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, -// ); -// } -// const cc = this.bytes[start]; -// if (cc !== DICT_END) { -// throw Error( -// `Unexpected character ${JSON.stringify( -// String.fromCharCode(cc), -// )} at index ${start} of ${name}`, -// ); -// } -// this.state.start = start + 1; -// } -// nextDictionaryKey() { -// const { start, end, name } = this.state; -// if (end > this.bytes.byteLength) { -// throw Error( -// `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, -// ); -// } -// const { start: next, value } = decodeDictionaryKey(this.bytes, start, end, name); -// this.state.start = next; -// return value; -// } -// nextDictionaryValue() { -// const { start, end, name } = this.state; -// if (end > this.bytes.byteLength) { -// throw Error( -// `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, -// ); -// } -// const { start: next, value } = decodeAny(this.bytes, start, end, name); -// this.state.start = next; -// return value; -// } -// *iterateDictionaryEntries() { -// const { start, end, name } = this.state; -// if (end > this.bytes.byteLength) { -// throw Error( -// `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, -// ); -// } -// let next = start; -// while (next < end) { -// const cc = this.bytes[next]; -// if (cc === DICT_END) { -// break; -// } -// const key = this.nextDictionaryKey(); -// const value = this.nextDictionaryValue(); -// yield { key, value }; -// } -// } -// *seekDictionaryEntries() { -// const { start, end, name } = this.state; -// if (end > this.bytes.byteLength) { -// throw Error( -// `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, -// ); -// } -// let next = start; -// while (next < end) { -// const cc = this.bytes[next]; -// if (cc === DICT_END) { -// this.state.start = next + 1; -// break; -// } -// const { start: afterKey } = seekDictionaryKey(this.bytes, next, end, name); -// const { start: afterValue } = seekAny(this.bytes, afterKey, end, name); -// yield { key: next, value: afterKey, start: afterValue }; -// next = afterValue; -// this.state.start = next; -// } -// } -// enterList() { -// const { start, end, name } = this.state; -// if (end > this.bytes.byteLength) { -// throw Error( -// `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, -// ); -// } -// const cc = this.bytes[start]; -// if (cc !== LIST_START) { -// throw Error( -// `Unexpected character ${quote(toChar(cc))}, Syrup lists must start with "[", got ${quote(toChar(cc))} at index ${start} of ${name}`, -// ); -// } -// this.state.start = start + 1; -// } -// exitList() { -// const { start, end, name } = this.state; -// if (end > this.bytes.byteLength) { -// throw Error( -// `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, -// ); -// } -// const cc = this.bytes[start]; -// if (cc !== LIST_END) { -// throw Error( -// `Unexpected character ${quote(toChar(cc))}, Syrup lists must end with "]", got ${quote(toChar(cc))} at index ${start} of ${name}`, -// ); -// } -// this.state.start = start + 1; -// } -// nextListValue() { -// const { start, end, name } = this.state; -// const { start: next, value } = decodeAny(this.bytes, start, end, name); -// this.state.start = next; -// return value; -// } -// enterSet() { -// const { start, end, name } = this.state; -// if (end > this.bytes.byteLength) { -// throw Error( -// `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, -// ); -// } -// const cc = this.bytes[start]; -// if (cc !== SET_START) { -// throw Error( -// `Unexpected character ${quote(toChar(cc))}, Syrup sets must start with "#", got ${quote(toChar(cc))} at index ${start} of ${name}`, -// ); -// } -// this.state.start = start + 1; -// } -// exitSet() { -// const { start, end, name } = this.state; -// if (end > this.bytes.byteLength) { -// throw Error( -// `Cannot decode Syrup with with "end" beyond "bytes.byteLength", got ${end}, byteLength ${this.bytes.byteLength}`, -// ); -// } -// const cc = this.bytes[start]; -// if (cc !== SET_END) { -// throw Error( -// `Unexpected character ${quote(toChar(cc))}, Syrup sets must end with "$", got ${quote(toChar(cc))} at index ${start} of ${name}`, -// ); -// } -// this.state.start = start + 1; -// } -// nextSetValue() { -// const { start, end, name } = this.state; -// const { start: next, value } = decodeAny(this.bytes, start, end, name); -// this.state.start = next; -// return value; -// } - -// readString() { -// const { start, end, name } = this.state; -// const { start: next, value, typeCode } = decodeStringlike(this.bytes, start, end, name); -// if (typeCode !== STRING_START) { -// throw Error(`Unexpected type ${quote(typeCode)}, Syrup strings must start with ${quote(toChar(STRING_START))} at index ${start} of ${name}`); -// } -// this.state.start = next; -// return value; -// } -// readInteger() { -// const { start, end, name } = this.state; -// const { start: next, value } = decodeInteger(this.bytes, start, end, name); -// this.state.start = next; -// return value; -// } -// readBytestring() { -// const { start, end, name } = this.state; -// const { start: next, value } = decodeBytestring(this.bytes, start, end, name); -// this.state.start = next; -// return value; -// } -// readBoolean() { -// const { start, end, name } = this.state; -// const { start: next, value } = decodeBoolean(this.bytes, start, end, name); -// this.state.start = next; -// return value; -// } -// readSymbolAsString() { -// const { start, end, name } = this.state; -// const { start: next, value, typeCode } = decodeStringlike(this.bytes, start, end, name); -// if (typeCode !== SYMBOL_START) { -// throw Error(`Unexpected type ${quote(typeCode)}, Syrup symbols must start with ${quote(toChar(SYMBOL_START))} at index ${start} of ${name}`); -// } -// this.state.start = next; -// return value; -// } -// readOfType(typeString, opts = {}) { -// switch (typeString) { -// case 'symbol': -// return this.readSymbolAsString(); -// case 'string': -// return this.readString(); -// case 'integer': -// return this.readInteger(); -// case 'bytestring': -// return this.readBytestring(); -// case 'boolean': -// return this.readBoolean(); -// default: -// throw Error(`Unknown field type: ${JSON.stringify(typeString)}`); -// } -// } -// } - -// export const makeSyrupReader = (bytes, options) => new SyrupReader(bytes, options); \ No newline at end of file +class SyrupReaderStackEntry { + constructor(type, start) { + this.type = type; + this.start = start; + } +} + + +class SyrupReader { + /** + * @param {BufferReader} bufferReader + * @param {object} options + * @param {string} [options.name] + */ + constructor(bufferReader, options = {}) { + const { name = '' } = options; + this.name = name; + this.bufferReader = bufferReader; + this.state = { + /** @type {SyrupReaderStackEntry[]} */ + stack: [], + }; + } + + /** + * @param {number} expectedByte + */ + #_readAndAssertByte(expectedByte) { + const start = this.bufferReader.index; + const cc = this.bufferReader.readByte(); + if (cc !== expectedByte) { + throw Error(`Unexpected character ${quote(toChar(cc))}, expected ${quote(toChar(expectedByte))} at index ${start} of ${this.name}`); + } + } + /** + * @param {string} type + */ + #_pushStackEntry(type) { + this.state.stack.push(new SyrupReaderStackEntry(type, this.bufferReader.index)); + } + /** + * @param {string} expectedType + */ + #_popStackEntry(expectedType) { + const start = this.bufferReader.index; + const stackEntry = this.state.stack.pop(); + if (!stackEntry) { + throw Error(`Attempted to exit ${expectedType} without entering it at index ${start} of ${this.name}`); + } + if (stackEntry.type !== expectedType) { + throw Error(`Attempted to exit ${expectedType} while in a ${stackEntry.type} at index ${start} of ${this.name}`); + } + } + + enterRecord() { + this.#_readAndAssertByte(RECORD_START); + this.#_pushStackEntry('record'); + } + exitRecord() { + this.#_readAndAssertByte(RECORD_END); + this.#_popStackEntry('record'); + } + peekRecordEnd() { + const cc = this.bufferReader.peekByte(); + return cc === RECORD_END; + } + + enterDictionary() { + this.#_readAndAssertByte(DICT_START); + this.#_pushStackEntry('dictionary'); + } + exitDictionary() { + this.#_readAndAssertByte(DICT_END); + this.#_popStackEntry('dictionary'); + } + peekDictionaryEnd() { + const cc = this.bufferReader.peekByte(); + return cc === DICT_END; + } + + enterList() { + this.#_readAndAssertByte(LIST_START); + this.#_pushStackEntry('list'); + } + exitList() { + this.#_readAndAssertByte(LIST_END); + this.#_popStackEntry('list'); + } + peekListEnd() { + const cc = this.bufferReader.peekByte(); + return cc === LIST_END; + } + + enterSet() { + this.#_readAndAssertByte(SET_START); + this.#_pushStackEntry('set'); + } + exitSet() { + this.#_readAndAssertByte(SET_END); + this.#_popStackEntry('set'); + } + peekSetEnd() { + const cc = this.bufferReader.peekByte(); + return cc === SET_END; + } + + readBoolean() { + return readBoolean(this.bufferReader, this.name); + } + readInteger() { + return readInteger(this.bufferReader, this.name); + } + readFloat64() { + return readFloat64(this.bufferReader, this.name); + } + readString() { + return readString(this.bufferReader, this.name); + } + readBytestring() { + return readBytestring(this.bufferReader, this.name); + } + readSymbolAsString() { + return readSymbolAsString(this.bufferReader, this.name); + } + +} + +export const makeSyrupReader = (bytes, options = {}) => { + const bufferReader = BufferReader.fromBytes(bytes); + const syrupReader = new SyrupReader(bufferReader, options); + return syrupReader; +}; diff --git a/packages/syrup/test/parse.test.js b/packages/syrup/test/parse.test.js deleted file mode 100644 index 7f4159291a479859831c782537657f9ff65232a6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8809 zcmdT}U2@wt5bkqNfpIfKI+dwNR^(W*)iiaQho)&KcKedaWIz&<7*nJQLXK6BkJjmt zdWG)dpOR?FiQ`Tc$EJwI;@fW*iv`eblXjbY4ga(0tmJ|SRtYlV<&*^Uf(B2$riX-9 zWCTC5byU?FXH-md3qID`u1Ly~GG(WWX6Ep|l%B(PKA*i|G-bR+W{jPmEoO{d zYc)~Su6WW`U84^;9D;$LKg%+TvqH&DgqT%>c6*$O$vlpda@sBvIxE^0YHth9Si9Tp z^pZF}NFFiv=t-x4csNSgsNZ90oOFA=9vyY#xRZ90A_Mb z(6UH>4GZ>>{PyNmsECWEOwee~^W6xgu*0q-PiXuDmkml z{DOsctQHC0Yb%3Y(4!4Ucwvwg31?GQ2$};TUS-9Ylq2$SQs#_Y(0tB<77^rlNUn50 zqHU3s0Epmfe}|4)r~7Sh2az7^oY5Fi(}SIA#*&Ox`#T90*UF1Pg;5~brFc=GFFB|| z2F~1@P#Nrr#|uCVf?l_X$?l2!-0L9+)W&~^i1+HiOWmIvh8j)+glV4DBW)R8nt(pdgyc+scQ1vUkd|XJMK#Za_#_)~q*=f`1|rie5m{MKzCeBj z{aFYx5G_R1_WAH^RNom$Jvr_gC(bm3QO{#`nF;4$?FcgkVHQ4pN}iXH=x9+5Ajy@S zjC7KNmQzTDkON0~SP{#sU<$SxSkAeP6B1GFP}9DeK6zh+~)PGrxCJjZM)IG~08`vV}ywVm4cn3%zc zwV!YH>EZ`}!b(GVXE#JNE9daqh?tHE!KksEFNfr4YW8~L_2f^pwJ`wBl!#~KqK55|+S@?ykV z_)hg3;-80Y{M}d>-BgustVdMEY*q(T418We!ezeMSd6H&WQzbB;9#pvQ?P>AR4vmF zI@Q!auCQ1smo^qmPOZb(cLkFznX2<_X5kUs5{)trdlkr=P}ziP5wXY&cWVj@Pswuq zB&DH(T9X&;nk|QhIa)&2)MMw6`t8Vf!3ipKY;+IT&><^GTys2gtkYKMNo}oZfo|!( zjSh6G9pwE-K4j~|rm`1f$pi1fhl~Thm3wFp_}0)jf>)|N;E(ITE%c@EL&VD=s0*PQ z&h6q5wKc?|S9YV6AAR@DO|N$=Zt8QSVs46{8Cy*g6kO&7v!S=&0=>?Ay(aI;z&mCB zWI^4vm|JzIOHDPO-U9qa_-+Vi#ba;3h$!)+cM5Bw(2oe6kDSu`#eJ|G9~*ItNycp& z{f}VxSAymy*tRC{yn_3;)!@P^!8h+7v)Dm**%Qt2gITPcb2!P=(X+IYqMZ#}bZ_ZF zspXk!S5tM)=B=P8Ewksy8}|N^{FB;(iePS}r^+rJ%vf#2YZL3U=n;IbPfZaBet?;Z zT|mlmF1bH$4xijAbmXqe;#4OGE8J`-V3pO&(X*82TGe|<5cW) zYQ-$tdSkE&-|r0>@vhcRr{2-p>8`9kO?bUjs72NuWGVN`p!SPqS2lX_vZZAkV#TTU z?b@#gc9T^Aw3fD=vU|bV?JO-v>v3A{J?3k>0WIJ)*LHa3bW!bNE4YV}H!2r!-b9Hr z8TfOTD*vPHD@Gga7r~QEZC}*}ejS&-s9O3OWkFOHl1eS?y=s!OspbQahvOW$T!Je` z0HXG*cp#|yZ4vCB5a|Ba%XokFwm~6K_jCZ9FV4!-WWuHtw(_BTv5S{!zr-H^eT284 zFF*>8A4AoXAQOBMP3bHQuLx{KTB?4~mmVV^lO;)Z$Lv`5EQhn8z2s6?vR*c02 zcHy|c(fhOAmQ!Sp7>oj0x3utv6s{ZVDpW}$V9OBfM)=3-d1q9@X^T&WbkYgzmbJaW z$M>Amh2#KkF6G4=&KHzX7d?iVU7a8lt5%WaRWfFH4sE;c4mgOsls40hU1H?0)b_W# z6?vlvSEBOVJ+LO>5iHz5$sLiQOJDh^z;>egtTX^|UC_8Tby5l~cF8r$^zkTmvS&f2 z-?gF957}iKqg?y8Z&_a}u1s!`It8{5PFZyT^Q9+Kr|Yd6&9eyr8Luf|cM(8NlZ}9S z1UUg0LK2D3sS|1>)r1}NRV&|@X0^7)ZN;j@br+;sMGc(V_8Ta*GL4J^leobC3$Lse A5&!@I diff --git a/packages/syrup/test/reader.test.js b/packages/syrup/test/reader.test.js new file mode 100644 index 0000000000000000000000000000000000000000..c0535ba4130fdfd950bd0f11728b4fb98c25fc28 GIT binary patch literal 5353 zcmd5=>2ljP5bke3#n#CTsl+0qBeEmQYV116WF|evPX09G$$%tiA(jXggnY!~N9**F z`U>3zz)PlMx9N|{PACGqi#@(Q;4p*@u~TWMbH zESa9UW%^oeT*HE$@#8DCS{`wha0NGxE(pS0#bLr@negB<3EK!m_#$NxiOdqsv|i@p zFgz7{wu*vSEB29QY8uBEH`@`XorTr-C zCH;`B>LyIHjaX)A;HOWCP#Ifr*aK2JaIb#GFW|$`JKuUESa8jPm6~?~q9TUQt`mXE z9*FFSU7(0G4-{JXTFlYhEcT5^#dA47=YDzC7I>eWiCzcgpv4*5|2c=4Ww2E8TqGPA zM5{DKXI-G-_?F46`-LkB(Us=Eq-oCe7F^6kJOi{PUFAFnJ>vxz>A_V)M^x~G$n?&> zQtT?A0Q#l!sD`7}@`Fxykk+Sz@(%1x;lOBOS?p?{<*`;GJN3O4l6%BY&hjl`%rDH1JGwVgsSQEzl(VdIQJ;jE0FQp6uNq5Q;_3FZHX80HSb<;_|*) z@$uY(&?tr^HHLs2M&Agok(zgh#l3ZOTD!O48Ys?V@W?I?ZVW%3rUN}w@*?+rv0ne_aFZs%$GbCJRfYvO#csJQFy-LY64Q- z*A)=X_*D;aP^Tu>IK0+iaL(o{-bVTsg`aX&M;8k1KV0a;NKxkC;WF5vLis-3uejIf_`Nar$JEynPAA~@&W+^lwSmFXz} z7Bq2+1t<|L>o~?kxr%Cxhn6uht6ydORpwab&N(KifeLCg!LQS&PVmqCl(p@5-Tnd9 zR#QNcb1+6r3scHPN1i_)u_?t8!lDp$Tgc%r^?7%v#GbY{Oiv@@ORHqY_FL$ z8ZTsq*mK)=Th{A$9nBq~yF=|C8CD=w>emRxU23Dg`!Bzv&mCGnAD_--UWCpneS);p zjVEC-VNo;sP0F{~j737T45uJWgROe?Hj@Q~(GEs9LaydSc6~#28jD^k6b?+LQi5M~ z`ws3lnm}8ZwD2f}ppOA3Bq|-m-fRPPVWx>1y8XOprdXxR_HkP=R~)TaYeazLEj_Q% zag88^p2YP1R_!R6ZHuW(b&kUsx}h5pqjDcl$jdFBJm%WY1VEEFHV*&$x`URSpCQSs zEb%*?vdn~`JIxHJro7uj0ZSdlHK1dIXDh?&4r@s7AV-X<094T$5Y3z zX@~)KJ}j*L_c5+oz44-+36&J+vu&*5M_w7DZJc#yM8u#YjI9RS_zKq z)X-4TaJ1;?CX6D?sQH?Pn$fVPrfC_a zuy4?_F_)QeXPC#O~+OuSeCOx|L zT+DDhv~arP-eZ#rwOsp?gu4mXZhgfK-Cg_|no?D@7Shqwo; zDjr{hN~7G@6Rs|~uA-^cMgzKqdM)EnO?tCT%yGE0Agl{C7k8Tb3dmWTo85v=ZFIKv zwub&Sm6fV1Hi9u-FSg1Rs)IOHdNgTa{5%@5=xJvw@1&xn0)sc|;)+pin6$>*fm-ou z(Pq&Yc7lGj=m3jS=$cWi+gm=VSNH5cw` Date: Thu, 3 Apr 2025 13:52:09 -1000 Subject: [PATCH 10/31] feat(syrup): use syrup codecs to decode ocapn messages --- packages/syrup/src/buffer-reader.js | 7 - packages/syrup/src/codec.js | 198 +++++++++++++++++++++++++ packages/syrup/src/decode.js | 26 +++- packages/syrup/src/encode.js | 75 ++++++++++ packages/syrup/src/ocapn.js | 221 ++++++++++++---------------- packages/syrup/test/_ocapn.js | 146 ++++++++++++++++-- packages/syrup/test/codec.test.js | 54 +++++++ packages/syrup/test/ocapn.test.js | 76 ++++++---- packages/syrup/test/reader.test.js | Bin 5353 -> 5337 bytes 9 files changed, 627 insertions(+), 176 deletions(-) create mode 100644 packages/syrup/src/codec.js create mode 100644 packages/syrup/test/codec.test.js diff --git a/packages/syrup/src/buffer-reader.js b/packages/syrup/src/buffer-reader.js index 25b4add9bd..0f6b8029f9 100644 --- a/packages/syrup/src/buffer-reader.js +++ b/packages/syrup/src/buffer-reader.js @@ -97,13 +97,6 @@ export class BufferReader { */ canSeek(index) { const fields = privateFieldsGet(this); - if (!(index >= 0 && fields.offset + index <= fields.length)) - console.log('CANT SEEK', { - index, - offset: fields.offset, - length: fields.length, - canSeek: index >= 0 && fields.offset + index <= fields.length, - }); return index >= 0 && fields.offset + index <= fields.length; } diff --git a/packages/syrup/src/codec.js b/packages/syrup/src/codec.js new file mode 100644 index 0000000000..ae8877ef7d --- /dev/null +++ b/packages/syrup/src/codec.js @@ -0,0 +1,198 @@ + +export class SyrupCodec { + /** + * @param {import('./decode.js').SyrupReader} syrupReader + */ + unmarshal(syrupReader) { + throw new Error('SyrupCodec: unmarshal must be implemented'); + } + /** + * @param {any} value + * @param {import('./encode.js').SyrupWriter} syrupWriter + */ + marshal(value, syrupWriter) { + throw new Error('SyrupCodec: marshal must be implemented'); + } +} + +export class SimpleSyrupCodecType extends SyrupCodec { + /** + * @param {object} options + * @param {function(any, import('./encode.js').SyrupWriter): void} options.marshal + * @param {function(import('./decode.js').SyrupReader): any} options.unmarshal + */ + constructor ({ marshal, unmarshal }) { + super(); + this.marshal = marshal; + this.unmarshal = unmarshal; + } + /** + * @param {import('./decode.js').SyrupReader} syrupReader + */ + unmarshal(syrupReader) { + this.unmarshal(syrupReader); + } + /** + * @param {any} value + * @param {import('./encode.js').SyrupWriter} syrupWriter + */ + marshal(value, syrupWriter) { + this.marshal(value, syrupWriter); + } +} + +export const SyrupSymbolCodec = new SimpleSyrupCodecType({ + marshal: (value, syrupWriter) => syrupWriter.writeSymbol(value), + unmarshal: (syrupReader) => syrupReader.readSymbolAsString(), +}); + +export const SyrupStringCodec = new SimpleSyrupCodecType({ + marshal: (value, syrupWriter) => syrupWriter.writeString(value), + unmarshal: (syrupReader) => syrupReader.readString(), +}); + +export const SyrupBooleanCodec = new SimpleSyrupCodecType({ + marshal: (value, syrupWriter) => syrupWriter.writeBoolean(value), + unmarshal: (syrupReader) => syrupReader.readBoolean(), +}); + +export const SyrupIntegerCodec = new SimpleSyrupCodecType({ + marshal: (value, syrupWriter) => syrupWriter.writeInteger(value), + unmarshal: (syrupReader) => syrupReader.readInteger(), +}); + +export const SyrupDoubleCodec = new SimpleSyrupCodecType({ + marshal: (value, syrupWriter) => syrupWriter.writeDouble(value), + unmarshal: (syrupReader) => syrupReader.readFloat64(), +}); + +export class SyrupRecordCodecType extends SyrupCodec { + /** + * @param {string} label + * @param {Array<[string, string | SyrupCodec]>} definition + */ + // TODO: improve definition type to restricted strings + constructor(label, definition) { + super(); + this.label = label; + this.definition = definition; + for (const [fieldName] of definition) { + if (fieldName === 'type') { + throw new Error('SyrupRecordCodec: The "type" field is reserved for internal use.'); + } + } + } + /** + * @param {import('./decode.js').SyrupReader} syrupReader + */ + unmarshal(syrupReader) { + syrupReader.enterRecord(); + const label = syrupReader.readSymbolAsString(); + if (label !== this.label) { + throw Error(`Expected label ${this.label}, got ${label}`); + } + const result = this.unmarshalBody(syrupReader); + syrupReader.exitRecord(); + return result; + } + /** + * @param {import('./decode.js').SyrupReader} syrupReader + */ + unmarshalBody(syrupReader) { + const result = {}; + for (const field of this.definition) { + const [fieldName, fieldType] = field; + let fieldValue; + if (typeof fieldType === 'string') { + // @ts-expect-error fieldType is any string + fieldValue = syrupReader.readOfType(fieldType); + } else { + const fieldDefinition = fieldType; + fieldValue = fieldDefinition.unmarshal(syrupReader); + } + result[fieldName] = fieldValue; + } + result.type = this.label; + return result; + } + /** + * @param {any} value + * @param {import('./encode.js').SyrupWriter} syrupWriter + */ + marshal(value, syrupWriter) { + syrupWriter.enterRecord(); + syrupWriter.writeSymbol(value.type); + for (const field of this.definition) { + const [fieldName, fieldType] = field; + const fieldValue = value[fieldName]; + if (typeof fieldType === 'string') { + // @ts-expect-error fieldType is any string + syrupWriter.writeOfType(fieldType, fieldValue); + } else { + fieldType.marshal(fieldValue, syrupWriter); + } + } + syrupWriter.exitRecord(); + } +} + +export class RecordUnionCodec extends SyrupCodec { + /** + * @param {Record} recordTypes + */ + constructor(recordTypes) { + super(); + this.recordTypes = recordTypes; + this.recordTable = Object.fromEntries( + Object.values(recordTypes).map(recordCodec => [recordCodec.label, recordCodec]) + ); + } + unmarshal(syrupReader) { + syrupReader.enterRecord(); + const label = syrupReader.readSymbolAsString(); + const recordCodec = this.recordTable[label]; + if (!recordCodec) { + throw Error(`Unknown record type: ${label}`); + } + const result = recordCodec.unmarshalBody(syrupReader); + syrupReader.exitRecord(); + return result; + } + marshal(value, syrupWriter) { + const { type } = value; + const recordCodec = this.recordTable[type]; + if (!recordCodec) { + throw Error(`Unknown record type: ${type}`); + } + recordCodec.marshal(value, syrupWriter); + } +} + +// export class SyrupListCodec extends SyrupCodec { +// /** +// * @param {SyrupCodec[]} definition +// */ +// constructor(definition) { +// super(); +// this.definition = definition; +// } +// /** +// * @param {import('./decode.js').SyrupReader} syrupReader +// */ +// unmarshal(syrupReader) { +// syrupReader.enterList(); +// const result = []; +// for (const entry of this.definition) { +// result.push(entry.unmarshal(syrupReader)); +// } +// syrupReader.exitList(); +// return result; +// } +// /** +// * @param {any} value +// * @param {import('./encode.js').SyrupWriter} syrupWriter +// */ +// marshal(value, syrupWriter) { +// return this.definition.map((entry, index) => entry.marshal(value[index], syrupWriter)); +// } +// } \ No newline at end of file diff --git a/packages/syrup/src/decode.js b/packages/syrup/src/decode.js index 5159907963..7ea4683686 100644 --- a/packages/syrup/src/decode.js +++ b/packages/syrup/src/decode.js @@ -399,8 +399,7 @@ class SyrupReaderStackEntry { } } - -class SyrupReader { +export class SyrupReader { /** * @param {BufferReader} bufferReader * @param {object} options @@ -516,7 +515,28 @@ class SyrupReader { readSymbolAsString() { return readSymbolAsString(this.bufferReader, this.name); } - + /** + * @param {'boolean' | 'integer' | 'float64' | 'string' | 'bytestring' | 'symbol'} type + * @returns {any} + */ + readOfType(type) { + switch (type) { + case 'boolean': + return this.readBoolean(); + case 'integer': + return this.readInteger(); + case 'float64': + return this.readFloat64(); + case 'string': + return this.readString(); + case 'bytestring': + return this.readBytestring(); + case 'symbol': + return this.readSymbolAsString(); + default: + throw Error(`Unexpected type ${type}`); + } + } } export const makeSyrupReader = (bytes, options = {}) => { diff --git a/packages/syrup/src/encode.js b/packages/syrup/src/encode.js index 977416924a..b792665c4f 100644 --- a/packages/syrup/src/encode.js +++ b/packages/syrup/src/encode.js @@ -13,6 +13,8 @@ const LIST_START = '['.charCodeAt(0); const LIST_END = ']'.charCodeAt(0); const DICT_START = '{'.charCodeAt(0); const DICT_END = '}'.charCodeAt(0); +const RECORD_START = '<'.charCodeAt(0); +const RECORD_END = '>'.charCodeAt(0); const DOUBLE = 'D'.charCodeAt(0); const TRUE = 't'.charCodeAt(0); const FALSE = 'f'.charCodeAt(0); @@ -227,6 +229,73 @@ function writeAny(bufferWriter, value, path, pathSuffix) { throw TypeError(`Cannot encode value ${value} at ${path.join('/')}`); } +export class SyrupWriter { + constructor(bufferWriter) { + this.bufferWriter = bufferWriter; + } + writeSymbol(value) { + writeSymbol(this.bufferWriter, value); + } + writeString(value) { + writeString(this.bufferWriter, value); + } + writeBytestring(value) { + writeBytestring(this.bufferWriter, value); + } + writeBoolean(value) { + writeBoolean(this.bufferWriter, value); + } + writeInteger(value) { + writeInteger(this.bufferWriter, value); + } + writeDouble(value) { + writeDouble(this.bufferWriter, value); + } + // writeList(value) { + // writeList(this.bufferWriter, value, []); + // } + // writeDictionary(value) { + // writeDictionary(this.bufferWriter, value, []); + // } + // writeRecord(value) { + // throw Error('writeRecord is not implemented'); + // } + enterRecord() { + this.bufferWriter.writeByte(RECORD_START); + } + exitRecord() { + this.bufferWriter.writeByte(RECORD_END); + } + /** + * @param {'boolean' | 'integer' | 'float64' | 'string' | 'bytestring' | 'symbol'} type + * @param {any} value + */ + writeOfType(type, value) { + switch (type) { + case 'symbol': + this.writeSymbol(value); + break; + case 'bytestring': + this.writeBytestring(value); + break; + case 'string': + this.writeString(value); + break; + case 'float64': + this.writeDouble(value); + break; + case 'integer': + this.writeInteger(value); + break; + case 'boolean': + this.writeBoolean(value); + break; + default: + throw Error(`writeTypeOf: unknown type ${typeof value}`); + } + } +} + /** * @param {any} value * @param {object} [options] @@ -240,3 +309,9 @@ export function encodeSyrup(value, options = {}) { writeAny(bufferWriter, value, [], '/'); return bufferWriter.subarray(0, bufferWriter.length); } + +export function makeSyrupWriter(options = {}) { + const { length: capacity = defaultCapacity } = options; + const bufferWriter = new BufferWriter(capacity); + return new SyrupWriter(bufferWriter); +} diff --git a/packages/syrup/src/ocapn.js b/packages/syrup/src/ocapn.js index d05b75d920..e06b168a0f 100644 --- a/packages/syrup/src/ocapn.js +++ b/packages/syrup/src/ocapn.js @@ -1,84 +1,53 @@ +import { SyrupRecordCodecType, SyrupCodec, RecordUnionCodec } from './codec.js'; -class Codec { - marshal(value) { - throw Error('Virtual method: marshal'); - } - unmarshal(parser) { - throw Error('Virtual method: unmarshal'); - } -} +// OCapN Components -class OcapnRecordCodec extends Codec { - constructor(label, definition) { +export class OCapNSignatureValueCodec extends SyrupCodec { + /** + * @param {string} expectedLabel + */ + constructor(expectedLabel) { super(); - this.label = label; - this.definition = definition; - for (const [fieldName] of definition) { - if (fieldName === 'type') { - throw new Error('OcapnRecordCodec: The "type" field is reserved for internal use.'); - } - } + this.expectedLabel = expectedLabel; } - unmarshal(parser) { - parser.enterRecord(); - const label = parser.readSymbolAsString(); - if (label !== this.label) { - throw Error(`Expected label ${this.label}, got ${label}`); + unmarshal(syrupReader) { + const label = syrupReader.readSymbolAsString(); + if (label !== this.expectedLabel) { + throw Error(`Expected label ${this.expectedLabel}, got ${label}`); } - const result = this.unmarshalBody(parser); - parser.exitRecord(); - return result; + const value = syrupReader.readBytestring(); + return value; } - unmarshalBody(parser) { - const result = {}; - for (const field of this.definition) { - const [fieldName, fieldType] = field; - let fieldValue; - if (typeof fieldType === 'string') { - fieldValue = parser.readOfType(fieldType); - } else { - const fieldDefinition = fieldType; - fieldValue = fieldDefinition.unmarshal(parser); - } - result[fieldName] = fieldValue; - } - result.type = this.label; - return result; - } - marshal(value) { - const result = []; - for (const field of this.definition) { - const [fieldName, fieldType] = field; - let fieldValue; - if (typeof fieldType === 'string') { - // TODO: WRONG - fieldValue = value[fieldName]; - } else { - const fieldDefinition = fieldType; - fieldValue = fieldDefinition.marshal(value[fieldName]); - } - result.push(fieldValue); - } - return result; + marshal(value, syrupWriter) { + syrupWriter.writeSymbol(this.expectedLabel); + syrupWriter.writeBytestring(value); } } -// OCapN Descriptors and Subtypes +const OCapNSignatureRValue = new OCapNSignatureValueCodec('r'); +const OCapNSignatureSValue = new OCapNSignatureValueCodec('s'); + +const OCapNSignature = new SyrupRecordCodecType( + 'sig-val', [ + ['scheme', 'symbol'], + ['r', OCapNSignatureRValue], + ['s', OCapNSignatureSValue], +]) -const OCapNNode = new OcapnRecordCodec( +const OCapNNode = new SyrupRecordCodecType( 'ocapn-node', [ ['transport', 'symbol'], ['address', 'bytestring'], ['hints', 'boolean'], ]) -const OCapNSturdyRef = new OcapnRecordCodec( +const OCapNSturdyRef = new SyrupRecordCodecType( 'ocapn-sturdyref', [ ['node', OCapNNode], ['swissNum', 'string'], ]) -const OCapNPublicKey = new OcapnRecordCodec( +const OCapNPublicKey = new SyrupRecordCodecType( 'public-key', [ ['scheme', 'symbol'], ['curve', 'symbol'], @@ -86,49 +55,44 @@ const OCapNPublicKey = new OcapnRecordCodec( ['q', 'bytestring'], ]) -const OCapNSignature = new OcapnRecordCodec( - 'sig-val', [ - ['scheme', 'symbol'], - // TODO: list type - ['r', [ - ['label', 'symbol'], - ['value', 'bytestring'], - ]], - ['s', [ - ['label', 'symbol'], - ['value', 'bytestring'], - ]], -]) -const DescSigEnvelope = new OcapnRecordCodec( +const OCapNComponentCodecs = { + OCapNNode, + OCapNSturdyRef, + OCapNPublicKey, + OCapNSignature, +} + +// OCapN Descriptors + +const DescSigEnvelope = new SyrupRecordCodecType( 'desc:sig-envelope', [ // TODO: union type, can be DescHandoffReceive, DescHandoffGive, ... ['object', 'any'], ['signature', OCapNSignature], ]) - -const DescImportObject = new OcapnRecordCodec( +const DescImportObject = new SyrupRecordCodecType( 'desc:import-object', [ ['position', 'integer'], ]) -const DescImportPromise = new OcapnRecordCodec( +const DescImportPromise = new SyrupRecordCodecType( 'desc:import-promise', [ ['position', 'integer'], ]) -const DescExport = new OcapnRecordCodec( +const DescExport = new SyrupRecordCodecType( 'desc:export', [ ['position', 'integer'], ]) -const DescAnswer = new OcapnRecordCodec( +const DescAnswer = new SyrupRecordCodecType( 'desc:answer', [ ['position', 'integer'], ]) -const DescHandoffGive = new OcapnRecordCodec( +const DescHandoffGive = new SyrupRecordCodecType( 'desc:handoff-give', [ ['receiverKey', OCapNPublicKey], ['exporterLocation', OCapNNode], @@ -137,7 +101,7 @@ const DescHandoffGive = new OcapnRecordCodec( ['giftId', 'bytestring'], ]) -const DescHandoffReceive = new OcapnRecordCodec( +const DescHandoffReceive = new SyrupRecordCodecType( 'desc:handoff-receive', [ ['receivingSession', 'bytestring'], ['receivingSide', 'bytestring'], @@ -161,7 +125,7 @@ const OCapNDescriptorCodecs = { // OCapN Operations -const OpStartSession = new OcapnRecordCodec( +const OpStartSession = new SyrupRecordCodecType( 'op:start-session', [ ['captpVersion', 'string'], ['sessionPublicKey', OCapNPublicKey], @@ -169,50 +133,62 @@ const OpStartSession = new OcapnRecordCodec( ['locationSignature', OCapNSignature], ]) -const OpListen = new OcapnRecordCodec( + +const OCapNDeliverResolveMeDescs = { + DescImportObject, + DescImportPromise, +} + +const OCapNResolveMeDescCodec = new RecordUnionCodec(OCapNDeliverResolveMeDescs); + +const OpListen = new SyrupRecordCodecType( 'op:listen', [ ['to', DescExport], - // TODO: union type - ['resolveMeDesc', [DescImportObject, DescImportPromise]], + ['resolveMeDesc', OCapNResolveMeDescCodec], ['wantsPartial', 'boolean'], ]) -const OpDeliverOnly = new OcapnRecordCodec( +const OCapNDeliverTargets = { + DescExport, + DescAnswer, +} + +const OCapNDeliverTargetCodec = new RecordUnionCodec(OCapNDeliverTargets); + + +const OpDeliverOnly = new SyrupRecordCodecType( 'op:deliver-only', [ - // TODO: union type - ['to', [DescExport, DescAnswer]], + ['to', OCapNDeliverTargetCodec], // TODO: list type, can include OCapNSturdyRef, ... ['args', 'list'], ]) -const OpDeliver = new OcapnRecordCodec( +const OpDeliver = new SyrupRecordCodecType( 'op:deliver', [ - // TODO: union type - ['to', [DescExport, DescAnswer]], + ['to', OCapNDeliverTargetCodec], // TODO: list type, can be DescSigEnvelope ['args', 'list'], ['answerPosition', 'integer'], - // TODO: union type - ['resolveMeDesc', [DescImportObject, DescImportPromise]], + ['resolveMeDesc', OCapNResolveMeDescCodec], ]) -const OpAbort = new OcapnRecordCodec( +const OpAbort = new SyrupRecordCodecType( 'op:abort', [ ['reason', 'string'], ]) -const OpGcExport = new OcapnRecordCodec( +const OpGcExport = new SyrupRecordCodecType( 'op:gc-export', [ ['exportPosition', 'integer'], ['wireDelta', 'integer'], ]) -const OpGcAnswer = new OcapnRecordCodec( +const OpGcAnswer = new SyrupRecordCodecType( 'op:gc-answer', [ ['answerPosition', 'integer'], ]) -const OpGcSession = new OcapnRecordCodec( +const OpGcSession = new SyrupRecordCodecType( 'op:gc-session', [ ['session', 'bytestring'], ]) @@ -228,34 +204,33 @@ const OCapNOpCodecs = { OpGcSession, } -const OCapNMessageCodecTable = Object.fromEntries( - Object.values(OCapNOpCodecs).map(recordCodec => [recordCodec.label, recordCodec]) -); +export const OCapNMessageUnionCodec = new RecordUnionCodec(OCapNOpCodecs); +export const OCapNDescriptorUnionCodec = new RecordUnionCodec(OCapNDescriptorCodecs); +export const OCapNComponentUnionCodec = new RecordUnionCodec(OCapNComponentCodecs); -const OCapNDescriptorCodecTable = Object.fromEntries( - Object.values(OCapNDescriptorCodecs).map(recordCodec => [recordCodec.label, recordCodec]) -); +export const readOCapNMessage = (syrupReader) => { + return OCapNMessageUnionCodec.unmarshal(syrupReader); +} -export const readOCapNMessage = (parser) => { - parser.enterRecord(); - const label = parser.readSymbolAsString(); - const recordCodec = OCapNMessageCodecTable[label]; - if (!recordCodec) { - throw Error(`Unknown OCapN message type: ${label}`); - } - const result = recordCodec.unmarshalBody(parser); - parser.exitRecord(); - return result; +export const readOCapDescriptor = (syrupReader) => { + return OCapNDescriptorUnionCodec.unmarshal(syrupReader); } -export const readOCapDescriptor = (parser) => { - parser.enterRecord(); - const label = parser.readSymbolAsString(); - const recordCodec = OCapNDescriptorCodecTable[label]; - if (!recordCodec) { - throw Error(`Unknown OCapN descriptor type: ${label}`); - } - const result = recordCodec.unmarshalBody(parser); - parser.exitRecord(); - return result; +export const readOCapComponent = (syrupReader) => { + return OCapNComponentUnionCodec.unmarshal(syrupReader); +} + +export const writeOCapNMessage = (message, syrupWriter) => { + OCapNMessageUnionCodec.marshal(message, syrupWriter); + return syrupWriter.bufferWriter.subarray(0, syrupWriter.bufferWriter.length); +} + +export const writeOCapDescriptor = (descriptor, syrupWriter) => { + OCapNDescriptorUnionCodec.marshal(descriptor, syrupWriter); + return syrupWriter.bufferWriter.subarray(0, syrupWriter.bufferWriter.length); +} + +export const writeOCapComponent = (component, syrupWriter) => { + OCapNComponentUnionCodec.marshal(component, syrupWriter); + return syrupWriter.bufferWriter.subarray(0, syrupWriter.bufferWriter.length); } diff --git a/packages/syrup/test/_ocapn.js b/packages/syrup/test/_ocapn.js index 73facea986..e17280b1f9 100644 --- a/packages/syrup/test/_ocapn.js +++ b/packages/syrup/test/_ocapn.js @@ -12,20 +12,144 @@ const makePubKey = (scheme, curve, flags, q) => { return `<${sym('public-key')}${sym(scheme)}${sym(curve)}${sym(flags)}${bts(q)}>`; } +const makeSigComp = (label, value) => { + return `${sym(label)}${bts(value)}`; +} + +const makeSig = (scheme, r, s) => { + return `<${sym('sig-val')}${sym(scheme)}${makeSigComp('r', r)}${makeSigComp('s', s)}>`; +} + +export const componentsTable = [ + { + syrup: `${makeSig('eddsa', '1', '2')}`, + value: { + type: 'sig-val', + scheme: 'eddsa', + r: new Uint8Array([0x31]), + s: new Uint8Array([0x32]) + } + }, +]; + // I made up these syrup values by hand, they may be wrong, sorry. // Would like external test data for this. export const descriptorsTable = [ - { syrup: `<10'ocapn-node3'tcp1:0f>`, value: { type: 'ocapn-node', transport: 'tcp', address: '0', hints: false } }, - { syrup: `<15'ocapn-sturdyref${makeNode('tcp', '0', false)}${str('1')}>`, value: { type: 'ocapn-sturdyref', node: { type: 'ocapn-node', transport: 'tcp', address: '0', hints: false }, swissNum: '1' } }, - { syrup: makePubKey('ecc', 'Ed25519', 'eddsa', '1'), value: { type: 'public-key', scheme: 'ecc', curve: 'Ed25519', flags: 'eddsa', q: '1' } }, - // TODO: sig-val, needs s/r-value - // TODO: desc:sig-envelope, needs sig-value, any - // { syrup: '<17\'desc:sig-envelope123+>', value: { type: 'desc:sig-envelope', object: { type: 'desc:handoff-give', receiverKey: { type: 'public-key', scheme: 'ed25519', curve: 'ed25519', flags: 'ed25519', q: new Uint8Array(32) }, exporterLocation: { type: 'ocapn-node', transport: 'tcp', address: '127.0.0.1', hints: false }, session: new Uint8Array(32), gifterSide: { type: 'public-key', scheme: 'ed25519', curve: 'ed25519', flags: 'ed25519', q: new Uint8Array(32) }, giftId: new Uint8Array(32) }, signature: new Uint8Array(32) } }, - { syrup: `<18'desc:import-object123+>`, value: { type: 'desc:import-object', position: 123n } }, - { syrup: `<19'desc:import-promise456+>`, value: { type: 'desc:import-promise', position: 456n } }, - { syrup: `<11'desc:export123+>`, value: { type: 'desc:export', position: 123n } }, - { syrup: `<11'desc:answer456+>`, value: { type: 'desc:answer', position: 456n } }, - { syrup: `<${sym('desc:handoff-give')}${makePubKey('ecc', 'Ed25519', 'eddsa', '1')}${makeNode('tcp', '127.0.0.1', false)}${bts('123')}${makePubKey('ecc', 'Ed25519', 'eddsa', '2')}${bts('456')}>`, value: { type: 'desc:handoff-give', receiverKey: { type: 'public-key', scheme: 'ecc', curve: 'Ed25519', flags: 'eddsa', q: '1' }, exporterLocation: { type: 'ocapn-node', transport: 'tcp', address: '127.0.0.1', hints: false }, session: '123', gifterSide: { type: 'public-key', scheme: 'ecc', curve: 'Ed25519', flags: 'eddsa', q: '2' }, giftId: '456' } }, + { + syrup: `<10'ocapn-node3'tcp1:0f>`, + value: { + type: 'ocapn-node', + transport: 'tcp', + address: new Uint8Array([0x30]), + hints: false + } + }, + { + syrup: `<15'ocapn-sturdyref${makeNode('tcp', '0', false)}${str('1')}>`, + value: { + type: 'ocapn-sturdyref', + node: { + type: 'ocapn-node', + transport: 'tcp', + address: new Uint8Array([0x30]), + hints: false + }, + swissNum: '1' + } + }, + { + syrup: makePubKey('ecc', 'Ed25519', 'eddsa', '1'), + value: { + type: 'public-key', + scheme: 'ecc', + curve: 'Ed25519', + flags: 'eddsa', + q: new Uint8Array([0x31]) + } + }, + // any + // { + // syrup: '<17\'desc:sig-envelope123+>', + // value: { + // type: 'desc:sig-envelope', + // object: { + // type: 'desc:handoff-give', + // receiverKey: { + // type: 'public-key', + // scheme: 'ed25519', + // curve: 'ed25519', + // flags: 'ed25519', + // q: new Uint8Array(32) + // }, + // exporterLocation: { + // type: 'ocapn-node', + // transport: 'tcp', + // address: '127.0.0.1', + // hints: false + // }, + // session: new Uint8Array(32), + // gifterSide: { + // type: 'public-key', + // scheme: 'ed25519', + // curve: 'ed25519', + // flags: 'ed25519', + // q: new Uint8Array(32) + // }, + // giftId: new Uint8Array(32) + // }, + // signature: new Uint8Array(32) + // } + // }, + { + syrup: `<18'desc:import-object123+>`, + value: { + type: 'desc:import-object', + position: 123n + } + }, + { + syrup: `<19'desc:import-promise456+>`, + value: { + type: 'desc:import-promise', + position: 456n + } + }, + { + syrup: `<11'desc:export123+>`, + value: { + type: 'desc:export', + position: 123n + } + }, + { + syrup: `<11'desc:answer456+>`, + value: { + type: 'desc:answer', + position: 456n + } + }, + { + syrup: `<${sym('desc:handoff-give')}${makePubKey('ecc', 'Ed25519', 'eddsa', '1')}${makeNode('tcp', '127.0.0.1', false)}${bts('123')}${makePubKey('ecc', 'Ed25519', 'eddsa', '2')}${bts('456')}>`, + value: { + type: 'desc:handoff-give', + receiverKey: { type: 'public-key', scheme: 'ecc', curve: 'Ed25519', flags: 'eddsa', q: new Uint8Array([0x31]) }, + exporterLocation: { + type: 'ocapn-node', + transport: 'tcp', + address: new Uint8Array([0x31, 0x32, 0x37, 0x2e, 0x30, 0x2e, 0x30, 0x2e, 0x31]), + hints: false + }, + session: new Uint8Array([0x31, 0x32, 0x33]), + gifterSide: { + type: 'public-key', + scheme: 'ecc', + curve: 'Ed25519', + flags: 'eddsa', + q: new Uint8Array([0x32]) + }, + giftId: new Uint8Array([0x34, 0x35, 0x36]) + } + }, // TODO: desc:handoff-receive, needs desc:sig-envelope // { syrup: `<${sym('desc:handoff-receive')}${bts('123')}${bts('456')}${int(1)}${makeSig()}>`, value: { type: 'desc:handoff-receive', receivingSession: '123', receivingSide: '456' } }, ]; diff --git a/packages/syrup/test/codec.test.js b/packages/syrup/test/codec.test.js new file mode 100644 index 0000000000..c1408cbd63 --- /dev/null +++ b/packages/syrup/test/codec.test.js @@ -0,0 +1,54 @@ +// @ts-check + +import test from 'ava'; +import { makeSyrupReader } from '../src/decode.js'; +import { makeSyrupWriter } from '../src/encode.js'; +import { RecordUnionCodec, SyrupRecordCodecType, SyrupStringCodec } from '../src/codec.js'; + + +const testCodecBidirectionally = (t, codec, value) => { + const writer = makeSyrupWriter(); + codec.marshal(value, writer); + const bytes = writer.bufferWriter.subarray(0, writer.bufferWriter.length); + const reader = makeSyrupReader(bytes); + const result = codec.unmarshal(reader); + t.deepEqual(result, value); +}; + +test('simple string codec', t => { + const codec = SyrupStringCodec; + const value = 'hello'; + testCodecBidirectionally(t, codec, value); +}); + +test('basic record codec cases', t => { + const codec = new SyrupRecordCodecType('test', [ + ['field1', 'string'], + ['field2', 'integer'], + ]); + const value = { + type: 'test', + field1: 'hello', + field2: 123n, + }; + testCodecBidirectionally(t, codec, value); +}); + +test('record union codec', t => { + const codec = new RecordUnionCodec({ + testA: new SyrupRecordCodecType('testA', [ + ['field1', 'string'], + ['field2', 'integer'], + ]), + testB: new SyrupRecordCodecType('testB', [ + ['field1', 'string'], + ['field2', 'integer'], + ]), + }); + const value = { + type: 'testA', + field1: 'hello', + field2: 123n, + }; + testCodecBidirectionally(t, codec, value); +}); diff --git a/packages/syrup/test/ocapn.test.js b/packages/syrup/test/ocapn.test.js index 19d6c9a934..3afd933aec 100644 --- a/packages/syrup/test/ocapn.test.js +++ b/packages/syrup/test/ocapn.test.js @@ -1,35 +1,47 @@ -// // @ts-check +// @ts-check -// import test from 'ava'; -// import { makeSyrupReader } from '../src/decode.js'; -// import { readOCapDescriptor, readOCapNMessage } from '../src/ocapn.js'; -// import { descriptorsTable, operationsTable } from './_ocapn.js'; +import test from 'ava'; +import { makeSyrupReader } from '../src/decode.js'; +import { makeSyrupWriter } from '../src/encode.js'; +import { OCapNComponentUnionCodec, OCapNDescriptorUnionCodec, OCapNMessageUnionCodec } from '../src/ocapn.js'; +import { componentsTable, descriptorsTable, operationsTable } from './_ocapn.js'; -// test('affirmative descriptor read cases', t => { -// for (const { syrup, value } of descriptorsTable) { -// // We test with a length guess of 1 to maximize the probability -// // of discovering a fault in the buffer resize code. -// const bytes = new Uint8Array(syrup.length); -// for (let i = 0; i < syrup.length; i += 1) { -// bytes[i] = syrup.charCodeAt(i); -// } -// const syrupReader = makeSyrupReader(bytes, { name: syrup }); -// let descriptor; -// t.notThrows(() => { -// descriptor = readOCapDescriptor(syrupReader); -// }, `for ${JSON.stringify(syrup)}`); -// t.deepEqual(descriptor, value, `for ${JSON.stringify(syrup)}`); -// } -// }); +const testBidirectionally = (t, codec, syrup, value, testName) => { + const syrupBytes = new Uint8Array(syrup.length); + for (let i = 0; i < syrup.length; i += 1) { + syrupBytes[i] = syrup.charCodeAt(i); + } + const syrupReader = makeSyrupReader(syrupBytes, { name: testName }); + let result; + t.notThrows(() => { + result = codec.unmarshal(syrupReader); + }, testName); + t.deepEqual(result, value, testName); + const syrupWriter = makeSyrupWriter(); + t.notThrows(() => { + codec.marshal(value, syrupWriter); + }, testName); + const bytes2 = syrupWriter.bufferWriter.subarray(0, syrupWriter.bufferWriter.length); + t.deepEqual(bytes2, syrupBytes, testName); +} -// test('affirmative operation read cases', t => { -// for (const { syrup, value } of operationsTable) { -// const bytes = new Uint8Array(syrup.length); -// for (let i = 0; i < syrup.length; i += 1) { -// bytes[i] = syrup.charCodeAt(i); -// } -// const syrupReader = makeSyrupReader(bytes); -// const message = readOCapNMessage(syrupReader); -// t.deepEqual(message, value, `for ${JSON.stringify(syrup)}`); -// } -// }); +test('affirmative component cases', t => { + const codec = OCapNComponentUnionCodec; + for (const { syrup, value } of componentsTable) { + testBidirectionally(t, codec, syrup, value, `for ${JSON.stringify(syrup)}`); + } +}); + +test('affirmative descriptor read cases', t => { + const codec = OCapNDescriptorUnionCodec; + for (const { syrup, value } of descriptorsTable) { + testBidirectionally(t, codec, syrup, value, `for ${JSON.stringify(syrup)}`); + } +}); + +test('affirmative operation read cases', t => { + const codec = OCapNMessageUnionCodec; + for (const { syrup, value } of operationsTable) { + testBidirectionally(t, codec, syrup, value, `for ${JSON.stringify(syrup)}`); + } +}); diff --git a/packages/syrup/test/reader.test.js b/packages/syrup/test/reader.test.js index c0535ba4130fdfd950bd0f11728b4fb98c25fc28..97e5f154ffd4dc67b5a93eb2a618f9ef67f083e1 100644 GIT binary patch delta 184 zcmaEGB)JnrxD}}PeoYGW7 z9k`egOw0%&W(*TE28q=Iafx0^YHERNVQFHH2EWS&8>YCPQa%G7*rKvz! zL!&$lIiNlxpuW7sTwEGK(jkdSi6t3{NJg6=>@@}|C@x4%&P**vQUH|)+YM5YoLHj1 JxtT4D4**JhJ`w-` From f7d3636c953debf2956c8f3b3718aa7e80d3fe8c Mon Sep 17 00:00:00 2001 From: kumavis Date: Thu, 3 Apr 2025 15:34:14 -1000 Subject: [PATCH 11/31] wip(syrup): wip passable codecs --- packages/syrup/src/codec.js | 85 ++++++++++++++++++----------- packages/syrup/src/ocapn.js | 39 +++++++++----- packages/syrup/src/passable.js | 98 ++++++++++++++++++++++++++++++++++ 3 files changed, 179 insertions(+), 43 deletions(-) create mode 100644 packages/syrup/src/passable.js diff --git a/packages/syrup/src/codec.js b/packages/syrup/src/codec.js index ae8877ef7d..55216b06be 100644 --- a/packages/syrup/src/codec.js +++ b/packages/syrup/src/codec.js @@ -51,6 +51,11 @@ export const SyrupStringCodec = new SimpleSyrupCodecType({ unmarshal: (syrupReader) => syrupReader.readString(), }); +export const SyrupBytestringCodec = new SimpleSyrupCodecType({ + marshal: (value, syrupWriter) => syrupWriter.writeBytestring(value), + unmarshal: (syrupReader) => syrupReader.readBytestring(), +}); + export const SyrupBooleanCodec = new SimpleSyrupCodecType({ marshal: (value, syrupWriter) => syrupWriter.writeBoolean(value), unmarshal: (syrupReader) => syrupReader.readBoolean(), @@ -122,6 +127,14 @@ export class SyrupRecordCodecType extends SyrupCodec { marshal(value, syrupWriter) { syrupWriter.enterRecord(); syrupWriter.writeSymbol(value.type); + this.marshalBody(value, syrupWriter); + syrupWriter.exitRecord(); + } + /** + * @param {any} value + * @param {import('./encode.js').SyrupWriter} syrupWriter + */ + marshalBody(value, syrupWriter) { for (const field of this.definition) { const [fieldName, fieldType] = field; const fieldValue = value[fieldName]; @@ -132,10 +145,26 @@ export class SyrupRecordCodecType extends SyrupCodec { fieldType.marshal(fieldValue, syrupWriter); } } - syrupWriter.exitRecord(); } } +// TODO: vestigial "definition" argument +export class CustomRecordCodec extends SyrupRecordCodecType { + /** + * @param {string} label + * @param {object} options + * @param {function(any, import('./encode.js').SyrupWriter): void} options.marshalBody + * @param {function(import('./decode.js').SyrupReader): any} options.unmarshalBody + */ + constructor(label, { marshalBody, unmarshalBody }) { + super(label, []); + this.marshalBody = marshalBody; + this.unmarshalBody = unmarshalBody; + } + + +} + export class RecordUnionCodec extends SyrupCodec { /** * @param {Record} recordTypes @@ -143,8 +172,15 @@ export class RecordUnionCodec extends SyrupCodec { constructor(recordTypes) { super(); this.recordTypes = recordTypes; + const labelSet = new Set(); this.recordTable = Object.fromEntries( - Object.values(recordTypes).map(recordCodec => [recordCodec.label, recordCodec]) + Object.values(recordTypes).map(recordCodec => { + if (labelSet.has(recordCodec.label)) { + throw Error(`Duplicate record type: ${recordCodec.label}`); + } + labelSet.add(recordCodec.label); + return [recordCodec.label, recordCodec] + }) ); } unmarshal(syrupReader) { @@ -168,31 +204,20 @@ export class RecordUnionCodec extends SyrupCodec { } } -// export class SyrupListCodec extends SyrupCodec { -// /** -// * @param {SyrupCodec[]} definition -// */ -// constructor(definition) { -// super(); -// this.definition = definition; -// } -// /** -// * @param {import('./decode.js').SyrupReader} syrupReader -// */ -// unmarshal(syrupReader) { -// syrupReader.enterList(); -// const result = []; -// for (const entry of this.definition) { -// result.push(entry.unmarshal(syrupReader)); -// } -// syrupReader.exitList(); -// return result; -// } -// /** -// * @param {any} value -// * @param {import('./encode.js').SyrupWriter} syrupWriter -// */ -// marshal(value, syrupWriter) { -// return this.definition.map((entry, index) => entry.marshal(value[index], syrupWriter)); -// } -// } \ No newline at end of file +export const SyrupListCodec = new SimpleSyrupCodecType({ + unmarshal(syrupReader) { + throw Error('SyrupListCodec: unmarshal must be implemented'); + }, + marshal(value, syrupWriter) { + throw Error('SyrupListCodec: marshal must be implemented'); + }, +}); + +export const SyrupStructCodec = new SimpleSyrupCodecType({ + unmarshal(syrupReader) { + throw Error('SyrupStructCodec: unmarshal must be implemented'); + }, + marshal(value, syrupWriter) { + throw Error('SyrupStructCodec: marshal must be implemented'); + }, +}); diff --git a/packages/syrup/src/ocapn.js b/packages/syrup/src/ocapn.js index e06b168a0f..1ae548d2d9 100644 --- a/packages/syrup/src/ocapn.js +++ b/packages/syrup/src/ocapn.js @@ -65,29 +65,22 @@ const OCapNComponentCodecs = { // OCapN Descriptors -const DescSigEnvelope = new SyrupRecordCodecType( - 'desc:sig-envelope', [ - // TODO: union type, can be DescHandoffReceive, DescHandoffGive, ... - ['object', 'any'], - ['signature', OCapNSignature], -]) - -const DescImportObject = new SyrupRecordCodecType( +export const DescImportObject = new SyrupRecordCodecType( 'desc:import-object', [ ['position', 'integer'], ]) -const DescImportPromise = new SyrupRecordCodecType( +export const DescImportPromise = new SyrupRecordCodecType( 'desc:import-promise', [ ['position', 'integer'], ]) -const DescExport = new SyrupRecordCodecType( +export const DescExport = new SyrupRecordCodecType( 'desc:export', [ ['position', 'integer'], ]) -const DescAnswer = new SyrupRecordCodecType( +export const DescAnswer = new SyrupRecordCodecType( 'desc:answer', [ ['position', 'integer'], ]) @@ -101,20 +94,38 @@ const DescHandoffGive = new SyrupRecordCodecType( ['giftId', 'bytestring'], ]) +const DescSigGiveEnvelope = new SyrupRecordCodecType( + 'desc:sig-envelope', [ + // TODO: verify union type, can be DescHandoffReceive, DescHandoffGive, ... + ['object', DescHandoffGive], + ['signature', OCapNSignature], +]) + const DescHandoffReceive = new SyrupRecordCodecType( 'desc:handoff-receive', [ ['receivingSession', 'bytestring'], ['receivingSide', 'bytestring'], ['handoffCount', 'integer'], - ['signedGive', DescSigEnvelope], + ['signedGive', DescSigGiveEnvelope], +]) + +const DescSigReceiveEnvelope = new SyrupRecordCodecType( + 'desc:sig-envelope', [ + // TODO: verify union type, can be DescHandoffReceive, DescHandoffGive, ... + ['object', DescHandoffReceive], + ['signature', OCapNSignature], ]) + +// Note: this may only be useful for testing const OCapNDescriptorCodecs = { OCapNNode, OCapNSturdyRef, OCapNPublicKey, OCapNSignature, - DescSigEnvelope, + DescSigGiveEnvelope, + // TODO: ambiguous record label for DescSigGiveEnvelope and DescSigReceiveEnvelope + // DescSigReceiveEnvelope, DescImportObject, DescImportPromise, DescExport, @@ -160,6 +171,7 @@ const OpDeliverOnly = new SyrupRecordCodecType( 'op:deliver-only', [ ['to', OCapNDeliverTargetCodec], // TODO: list type, can include OCapNSturdyRef, ... + // see https://github.com/ocapn/ocapn/blob/main/implementation-guide/Implementation%20Guide.md#stage-2-promises-opdeliver-oplisten ['args', 'list'], ]) @@ -167,6 +179,7 @@ const OpDeliver = new SyrupRecordCodecType( 'op:deliver', [ ['to', OCapNDeliverTargetCodec], // TODO: list type, can be DescSigEnvelope + // see https://github.com/ocapn/ocapn/blob/main/implementation-guide/Implementation%20Guide.md#stage-2-promises-opdeliver-oplisten ['args', 'list'], ['answerPosition', 'integer'], ['resolveMeDesc', OCapNResolveMeDescCodec], diff --git a/packages/syrup/src/passable.js b/packages/syrup/src/passable.js new file mode 100644 index 0000000000..bbd2bb2da7 --- /dev/null +++ b/packages/syrup/src/passable.js @@ -0,0 +1,98 @@ +import { SyrupRecordCodecType, RecordUnionCodec, SimpleSyrupCodecType, SyrupBooleanCodec, SyrupIntegerCodec, SyrupDoubleCodec, SyrupSymbolCodec, SyrupStringCodec, SyrupBytestringCodec, SyrupListCodec, SyrupStructCodec, CustomRecordCodec } from './codec.js'; +import { DescAnswer, DescExport, DescImportObject, DescImportPromise } from './ocapn.js'; + +// OCapN Passable Atoms + +const UndefinedCodec = new SimpleSyrupCodecType({ + unmarshal(syrupReader) { + return undefined; + }, + marshal(value, syrupWriter) { + syrupWriter.enterRecord(); + syrupWriter.writeSymbol('void'); + syrupWriter.exitRecord(); + }, +}); + +const NullCodec = new SimpleSyrupCodecType({ + unmarshal(syrupReader) { + return null; + }, + marshal(value, syrupWriter) { + syrupWriter.enterRecord(); + syrupWriter.writeSymbol('null'); + syrupWriter.exitRecord(); + }, +}); + + +const AtomCodecs = { + undefined: UndefinedCodec, + null: NullCodec, + boolean: SyrupBooleanCodec, + integer: SyrupIntegerCodec, + float64: SyrupDoubleCodec, + string: SyrupStringCodec, + // TODO: Pass Invariant Equality + symbol: SyrupSymbolCodec, + // TODO: Pass Invariant Equality + byteArray: SyrupBytestringCodec, +} + +// OCapN Passable Containers + +// const OCapNTaggedCodec = new SyrupRecordCodecType( +// 'desc:tagged', [ +// ['tagName', 'symbol'], +// // TODO: any type +// ['value', 'any'], +// ]) +const OCapNTaggedCodec = new CustomRecordCodec('desc:tagged', { + unmarshalBody(syrupReader) { + const tagName = syrupReader.readSymbolAsString(); + // @ts-expect-error any type + const value = syrupReader.readOfType('any'); + // TODO: Pass Invariant Equality + return { + [Symbol.for('passStyle')]: 'tagged', + [Symbol.toStringTag]: tagName, + value, + } + }, + marshalBody(value, syrupWriter) { + syrupWriter.writeSymbol(value.tagName); + value.value.marshal(syrupWriter); + }, + +}) + +const ContainerCodecs = { + list: SyrupListCodec, + struct: SyrupStructCodec, + tagged: OCapNTaggedCodec, +} + +// OCapN Reference (Capability) + +const OCapNTargetCodec = new RecordUnionCodec({ + DescExport, + DescImportObject, +}) + +const OCapNPromiseCodec = new RecordUnionCodec({ + DescImportPromise, + DescAnswer, +}) + +const OCapNReferenceCodecs = { + OCapNTargetCodec, + OCapNPromiseCodec, +} + +// OCapN Error + +const OCapNErrorCodec = new SyrupRecordCodecType( + 'desc:error', [ + ['message', 'string'], +]) + From 82aecc156036cde351a6231799cab73c12d36719 Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 4 Apr 2025 15:05:05 -1000 Subject: [PATCH 12/31] refactor(syrup): refactor readAny over readTypeAndMaybeValue --- packages/syrup/src/decode.js | 306 ++++++++++++++++++++--------------- 1 file changed, 178 insertions(+), 128 deletions(-) diff --git a/packages/syrup/src/decode.js b/packages/syrup/src/decode.js index 7ea4683686..a8fbc775ff 100644 --- a/packages/syrup/src/decode.js +++ b/packages/syrup/src/decode.js @@ -51,84 +51,116 @@ function readBoolean(bufferReader, name) { throw Error(`Unexpected byte ${quote(toChar(cc))}, Syrup booleans must start with ${quote(toChar(TRUE))} or ${quote(toChar(FALSE))} at index ${bufferReader.index} of ${name}`); } +// Structure types, no value provided +/** @typedef {'list' | 'set' | 'dictionary' | 'record'} StructuredType */ +/** @typedef {{type: StructuredType, value: null}} ReadTypeStructuredResult */ +// Simple Atom types, value is read +/** @typedef {{type: 'boolean', value: boolean}} ReadTypeBooleanResult */ +/** @typedef {{type: 'float64', value: number}} ReadTypeFloat64Result */ +// Number-prefixed types, value is read +/** @typedef {{type: 'integer', value: bigint}} ReadTypeIntegerResult */ +/** @typedef {{type: 'bytestring', value: Uint8Array}} ReadTypeBytestringResult */ +/** @typedef {{type: 'string', value: string}} ReadTypeStringResult */ +/** @typedef {{type: 'symbol', value: string}} ReadTypeSymbolResult */ +/** @typedef {ReadTypeBooleanResult | ReadTypeFloat64Result | ReadTypeIntegerResult | ReadTypeBytestringResult | ReadTypeStringResult | ReadTypeSymbolResult} ReadTypeAtomResult */ /** * @param {BufferReader} bufferReader * @param {string} name - * @returns {{value: any, type: 'integer' | 'bytestring' | 'string' | 'symbol'}} + * @returns {ReadTypeStructuredResult | ReadTypeAtomResult} + * Reads until it can determine the type of the next value. */ -function readNumberPrefixed(bufferReader, name) { - let start = bufferReader.index; +function readTypeAndMaybeValue(bufferReader, name) { + const start = bufferReader.index; + const cc = bufferReader.readByte(); + // Structure types, don't read value + if (cc === LIST_START) { + return { type: 'list', value: null }; + } + if (cc === SET_START) { + return { type: 'set', value: null }; + } + if (cc === DICT_START) { + return { type: 'dictionary', value: null }; + } + if (cc === RECORD_START) { + return { type: 'record', value: null }; + } + // Atom types, read value + if (cc === TRUE) { + return { type: 'boolean', value: true }; + } + if (cc === FALSE) { + return { type: 'boolean', value: false }; + } + if (cc === DOUBLE) { + const value = readFloat64Body(bufferReader, name); + return { type: 'float64', value }; + } + // Number-prefixed types, read value + if (cc < ZERO || cc > NINE) { + throw Error(`Unexpected character ${quote(toChar(cc))}, at index ${bufferReader.index} of ${name}`); + } + // Parse number-prefix let end; let byte; - let nextToken; - - // eslint-disable-next-line no-empty for (;;) { byte = bufferReader.readByte(); - if (byte < ZERO || byte > NINE) { end = bufferReader.index - 1; - if (start === end) { - throw Error(`Unexpected character ${quote(toChar(byte))}, expected a number at index ${bufferReader.index} of ${name}`); - } - nextToken = byte; break; } } + const typeByte = byte; const numberBuffer = bufferReader.bytesAt(start, end - start); const numberString = textDecoder.decode(numberBuffer); - - if (nextToken === PLUS) { + if (typeByte === PLUS) { const integer = BigInt(numberString); - return { value: integer, type: 'integer' }; + return { type: 'integer', value: integer }; } - if (nextToken === MINUS) { + if (typeByte === MINUS) { const integer = BigInt(numberString); - return { value: -integer, type: 'integer' }; + return { type: 'integer', value: -integer }; } - - const number = Number.parseInt(numberString, 10); - const valueBytes = bufferReader.read(number); - if (nextToken === BYTES_START) { - return { value: valueBytes, type: 'bytestring' }; + if (typeByte === BYTES_START) { + const number = Number.parseInt(numberString, 10); + const valueBytes = bufferReader.read(number); + return { type: 'bytestring', value: valueBytes }; } - if (nextToken === STRING_START) { - return { value: textDecoder.decode(valueBytes), type: 'string' }; + if (typeByte === STRING_START) { + const number = Number.parseInt(numberString, 10); + const valueBytes = bufferReader.read(number); + return { type: 'string', value: textDecoder.decode(valueBytes) }; } - if (nextToken === SYMBOL_START) { - return { value: textDecoder.decode(valueBytes), type: 'symbol' }; + if (typeByte === SYMBOL_START) { + const number = Number.parseInt(numberString, 10); + const valueBytes = bufferReader.read(number); + return { type: 'symbol', value: textDecoder.decode(valueBytes) }; } - throw Error( - `Unexpected character ${quote(toChar(nextToken))}, at index ${bufferReader.index} of ${name}`, - ); + throw Error(`Unexpected character ${quote(toChar(typeByte))}, at index ${bufferReader.index} of ${name}`); } /** * @param {BufferReader} bufferReader + * @param {'boolean' | 'integer' | 'float64' | 'string' | 'symbol' | 'bytestring'} expectedType * @param {string} name - * @returns {bigint} + * @returns {any} */ -function readInteger(bufferReader, name) { - const { value, type } = readNumberPrefixed(bufferReader, name); - if (type !== 'integer') { - throw Error(`Unexpected type ${quote(type)}, Syrup integers must start with ${quote(toChar(PLUS))} or ${quote(toChar(MINUS))} at index ${bufferReader.index} of ${name}`); +function readAndAssertType(bufferReader, expectedType, name) { + const start = bufferReader.index; + const { value, type } = readTypeAndMaybeValue(bufferReader, name); + if (type !== expectedType) { + throw Error(`Unexpected type ${quote(type)} at index ${start} of ${name}`); } return value; } /** * @param {BufferReader} bufferReader - * @param {string} expectedType * @param {string} name - * @returns {string} + * @returns {bigint} */ -function readStringlikeAndAssertType(bufferReader, expectedType, name) { - const start = bufferReader.index; - const { value, type } = readNumberPrefixed(bufferReader, name); - if (type !== expectedType) { - throw Error(`Unexpected type ${quote(type)}, Syrup ${expectedType} must start with ${quote(toChar(expectedType))} at index ${start} of ${name}`); - } - return value; +function readInteger(bufferReader, name) { + return readAndAssertType(bufferReader, 'integer', name); } /** @@ -137,7 +169,7 @@ function readStringlikeAndAssertType(bufferReader, expectedType, name) { * @returns {string} */ function readString(bufferReader, name) { - return readStringlikeAndAssertType(bufferReader, 'string', name); + return readAndAssertType(bufferReader, 'string', name); } /** @@ -146,7 +178,7 @@ function readString(bufferReader, name) { * @returns {string} */ function readSymbolAsString(bufferReader, name) { - return readStringlikeAndAssertType(bufferReader, 'symbol', name); + return readAndAssertType(bufferReader, 'symbol', name); } /** @@ -155,50 +187,52 @@ function readSymbolAsString(bufferReader, name) { * @returns {string} */ function readBytestring(bufferReader, name) { - return readStringlikeAndAssertType(bufferReader, 'bytestring', name); + return readAndAssertType(bufferReader, 'bytestring', name); } - /** * @param {BufferReader} bufferReader * @param {string} name */ -function readFloat64(bufferReader, name) { - const cc = bufferReader.readByte(); - if (cc !== DOUBLE) { - throw Error(`Unexpected character ${quote(toChar(cc))}, at index ${bufferReader.index} of ${name}`, - ); - } - const floatStart = bufferReader.index; +function readFloat64Body(bufferReader, name) { + const start = bufferReader.index; const value = bufferReader.readFloat64(false); // big end if (value === 0) { // @ts-expect-error canonicalZero64 is a frozen array, not a Uint8Array - if (!bufferReader.matchAt(floatStart, canonicalZero64)) { - throw Error(`Non-canonical zero at index ${floatStart} of Syrup ${name}`); + if (!bufferReader.matchAt(start, canonicalZero64)) { + throw Error(`Non-canonical zero at index ${start} of Syrup ${name}`); } } if (Number.isNaN(value)) { // @ts-expect-error canonicalNaN64 is a frozen array, not a Uint8Array - if (!bufferReader.matchAt(floatStart, canonicalNaN64)) { - throw Error(`Non-canonical NaN at index ${floatStart} of Syrup ${name}`); + if (!bufferReader.matchAt(start, canonicalNaN64)) { + throw Error(`Non-canonical NaN at index ${start} of Syrup ${name}`); } } return value; } +/** + * @param {BufferReader} bufferReader + * @param {string} name + */ +function readFloat64(bufferReader, name) { + const cc = bufferReader.readByte(); + if (cc !== DOUBLE) { + throw Error(`Unexpected character ${quote(toChar(cc))}, at index ${bufferReader.index} of ${name}`, + ); + } + return readFloat64Body(bufferReader, name); +} /** * @param {BufferReader} bufferReader * @param {string} name * @returns {any[]} */ -function readList(bufferReader, name) { - let cc = bufferReader.readByte(); - if (cc !== LIST_START) { - throw Error(`Unexpected byte ${quote(toChar(cc))}, Syrup lists must start with ${quote(toChar(LIST_START))} at index ${bufferReader.index} of ${name}`); - } +function readListBody(bufferReader, name) { const list = []; for (;;) { if (bufferReader.peekByte() === LIST_END) { @@ -210,6 +244,19 @@ function readList(bufferReader, name) { } } +/** + * @param {BufferReader} bufferReader + * @param {string} name + * @returns {any[]} + */ +function readList(bufferReader, name) { + let cc = bufferReader.readByte(); + if (cc !== LIST_START) { + throw Error(`Unexpected byte ${quote(toChar(cc))}, Syrup lists must start with ${quote(toChar(LIST_START))} at index ${bufferReader.index} of ${name}`); + } + return readListBody(bufferReader, name); +} + /** * @param {BufferReader} bufferReader * @param {string} name @@ -217,7 +264,7 @@ function readList(bufferReader, name) { */ function readDictionaryKey(bufferReader, name) { const start = bufferReader.index; - const { value, type } = readNumberPrefixed(bufferReader, name); + const { value, type } = readTypeAndMaybeValue(bufferReader, name); if (type === 'string' || type === 'symbol') { const end = bufferReader.index; const bytes = bufferReader.bytesAt(start, end - start); @@ -229,23 +276,22 @@ function readDictionaryKey(bufferReader, name) { throw Error(`Unexpected type ${quote(type)}, Syrup dictionary keys must be strings or symbols at index ${start} of ${name}`); } + /** * @param {BufferReader} bufferReader * @param {string} name */ -function readDictionary(bufferReader, name) { - let cc = bufferReader.readByte(); - if (cc !== DICT_START) { - throw Error(`Unexpected character ${quote(toChar(cc))}, Syrup dictionaries must start with ${quote(toChar(DICT_START))} at index ${bufferReader.index} of ${name}`); - } - const record = {}; +function readDictionaryBody(bufferReader, name) { + const dict = {}; let priorKey = undefined; let priorKeyBytes = undefined; for (;;) { + // Check for end of dictionary if (bufferReader.peekByte() === DICT_END) { bufferReader.skip(1); - return freeze(record); + return freeze(dict); } + // Read key const start = bufferReader.index; const { value: newKey, bytes: newKeyBytes } = readDictionaryKey(bufferReader, name); @@ -278,9 +324,9 @@ function readDictionary(bufferReader, name) { priorKey = newKey; priorKeyBytes = newKeyBytes; + // Read value and add to dictionary const value = readAny(bufferReader, name); - - defineProperty(record, newKey, { + defineProperty(dict, newKey, { value, enumerable: true, writable: false, @@ -289,6 +335,19 @@ function readDictionary(bufferReader, name) { } } +/** + * @param {BufferReader} bufferReader + * @param {string} name + */ +function readDictionary(bufferReader, name) { + const start = bufferReader.index; + let cc = bufferReader.readByte(); + if (cc !== DICT_START) { + throw Error(`Unexpected character ${quote(toChar(cc))}, Syrup dictionaries must start with ${quote(toChar(DICT_START))} at index ${start} of ${name}`); + } + return readDictionaryBody(bufferReader, name); +} + /** * @param {BufferReader} bufferReader * @param {string} name @@ -329,67 +388,27 @@ export function peekTypeHint(bufferReader, name) { * @returns {any} */ function readAny(bufferReader, name) { - const typeHint = peekTypeHint(bufferReader, name); - - if (typeHint === 'number-prefix') { - const { value, type } = readNumberPrefixed(bufferReader, name); - if (type === 'symbol') { - return SyrupSymbolFor(value); - } - return value; + const { type, value } = readTypeAndMaybeValue(bufferReader, name); + // Structure types, value has not been read + if (type === 'list') { + return readListBody(bufferReader, name); } - if (typeHint === 'boolean') { - return readBoolean(bufferReader, name); + if (type === 'set') { + throw Error(`readAny for Sets is not yet supported.`); } - if (typeHint === 'float64') { - return readFloat64(bufferReader, name); + if (type === 'dictionary') { + return readDictionaryBody(bufferReader, name); } - if (typeHint === 'list') { - return readList(bufferReader, name); - } - if (typeHint === 'dictionary') { - return readDictionary(bufferReader, name); - } - if (typeHint === 'set') { - throw Error( - `decode Sets are not yet supported.`, - ); - } - if (typeHint === 'record') { - throw Error( - `decode Records are not yet supported.`, - ); + if (type === 'record') { + throw Error(`readAny for Records is not yet supported.`); } - const index = bufferReader.index; - const cc = bufferReader.readByte(); - throw Error( - `Unexpected character ${quote(toChar(cc))}, at index ${index} of ${name}`, - ); -} - -/** - * @param {Uint8Array} bytes - * @param {object} options - * @param {string} [options.name] - * @param {number} [options.start] - * @param {number} [options.end] - */ -export function decodeSyrup(bytes, options = {}) { - const { start = 0, name = '' } = options; - const bufferReader = BufferReader.fromBytes(bytes); - if (start !== 0) { - bufferReader.seek(start); - } - try { - return readAny(bufferReader, name); - } catch (err) { - if (err.code === 'EOD') { - const err2 = Error(`Unexpected end of Syrup at index ${bufferReader.length} of ${name}`) - err2.cause = err; - throw err2; - } - throw err; + // Atom types, value is already read + // For symbols, we need to convert the string to a symbol + if (type === 'symbol') { + return SyrupSymbolFor(value); } + + return value; } class SyrupReaderStackEntry { @@ -515,6 +534,9 @@ export class SyrupReader { readSymbolAsString() { return readSymbolAsString(this.bufferReader, this.name); } + readAny() { + return readAny(this.bufferReader, this.name); + } /** * @param {'boolean' | 'integer' | 'float64' | 'string' | 'bytestring' | 'symbol'} type * @returns {any} @@ -537,6 +559,9 @@ export class SyrupReader { throw Error(`Unexpected type ${type}`); } } + peekTypeHint() { + return peekTypeHint(this.bufferReader, this.name); + } } export const makeSyrupReader = (bytes, options = {}) => { @@ -544,3 +569,28 @@ export const makeSyrupReader = (bytes, options = {}) => { const syrupReader = new SyrupReader(bufferReader, options); return syrupReader; }; + +/** + * @param {Uint8Array} bytes + * @param {object} options + * @param {string} [options.name] + * @param {number} [options.start] + * @param {number} [options.end] + */ +export function decodeSyrup(bytes, options = {}) { + const { start = 0, name = '' } = options; + const bufferReader = BufferReader.fromBytes(bytes); + if (start !== 0) { + bufferReader.seek(start); + } + try { + return readAny(bufferReader, name); + } catch (err) { + if (err.code === 'EOD') { + const err2 = Error(`Unexpected end of Syrup at index ${bufferReader.length} of ${name}`) + err2.cause = err; + throw err2; + } + throw err; + } +} \ No newline at end of file From f7f1c75d223c186cbe08cc45a0866d5944c2b6ad Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 4 Apr 2025 17:07:09 -1000 Subject: [PATCH 13/31] feat(syrup): improve Passables codec --- packages/syrup/src/codec.js | 108 ++++++++++++++++------- packages/syrup/src/encode.js | 9 ++ packages/syrup/src/import-export.js | 26 ++++++ packages/syrup/src/ocapn.js | 115 +++++++++++++++---------- packages/syrup/src/passable.js | 128 +++++++++++++++++++++++----- packages/syrup/test/_ocapn.js | 105 ++++++++++++++++++++++- packages/syrup/test/codec.test.js | 8 +- packages/syrup/test/ocapn.test.js | 26 ++++-- 8 files changed, 416 insertions(+), 109 deletions(-) create mode 100644 packages/syrup/src/import-export.js diff --git a/packages/syrup/src/codec.js b/packages/syrup/src/codec.js index 55216b06be..313f022e78 100644 --- a/packages/syrup/src/codec.js +++ b/packages/syrup/src/codec.js @@ -2,6 +2,7 @@ export class SyrupCodec { /** * @param {import('./decode.js').SyrupReader} syrupReader + * @returns {any} */ unmarshal(syrupReader) { throw new Error('SyrupCodec: unmarshal must be implemented'); @@ -9,6 +10,7 @@ export class SyrupCodec { /** * @param {any} value * @param {import('./encode.js').SyrupWriter} syrupWriter + * @returns {void} */ marshal(value, syrupWriter) { throw new Error('SyrupCodec: marshal must be implemented'); @@ -71,21 +73,18 @@ export const SyrupDoubleCodec = new SimpleSyrupCodecType({ unmarshal: (syrupReader) => syrupReader.readFloat64(), }); +export const SyrupAnyCodec = new SimpleSyrupCodecType({ + marshal: (value, syrupWriter) => syrupWriter.writeAny(value), + unmarshal: (syrupReader) => syrupReader.readAny(), +}); + export class SyrupRecordCodecType extends SyrupCodec { /** * @param {string} label - * @param {Array<[string, string | SyrupCodec]>} definition */ - // TODO: improve definition type to restricted strings - constructor(label, definition) { + constructor(label) { super(); this.label = label; - this.definition = definition; - for (const [fieldName] of definition) { - if (fieldName === 'type') { - throw new Error('SyrupRecordCodec: The "type" field is reserved for internal use.'); - } - } } /** * @param {import('./decode.js').SyrupReader} syrupReader @@ -100,6 +99,45 @@ export class SyrupRecordCodecType extends SyrupCodec { syrupReader.exitRecord(); return result; } + /** + * @param {import('./decode.js').SyrupReader} syrupReader + */ + unmarshalBody(syrupReader) { + throw Error('SyrupRecordCodecType: unmarshalBody must be implemented'); + } + /** + * @param {any} value + * @param {import('./encode.js').SyrupWriter} syrupWriter + */ + marshal(value, syrupWriter) { + syrupWriter.enterRecord(); + syrupWriter.writeSymbol(value.type); + this.marshalBody(value, syrupWriter); + syrupWriter.exitRecord(); + } + /** + * @param {any} value + * @param {import('./encode.js').SyrupWriter} syrupWriter + */ + marshalBody(value, syrupWriter) { + throw Error('SyrupRecordCodecType: marshalBody must be implemented'); + } +} +export class SyrupStructuredRecordCodecType extends SyrupRecordCodecType { + /** + * @param {string} label + * @param {Array<[string, string | SyrupCodec]>} definition + */ + // TODO: improve definition type to restricted strings + constructor(label, definition) { + super(label); + this.definition = definition; + for (const [fieldName] of definition) { + if (fieldName === 'type') { + throw new Error('SyrupRecordCodec: The "type" field is reserved for internal use.'); + } + } + } /** * @param {import('./decode.js').SyrupReader} syrupReader */ @@ -120,16 +158,6 @@ export class SyrupRecordCodecType extends SyrupCodec { result.type = this.label; return result; } - /** - * @param {any} value - * @param {import('./encode.js').SyrupWriter} syrupWriter - */ - marshal(value, syrupWriter) { - syrupWriter.enterRecord(); - syrupWriter.writeSymbol(value.type); - this.marshalBody(value, syrupWriter); - syrupWriter.exitRecord(); - } /** * @param {any} value * @param {import('./encode.js').SyrupWriter} syrupWriter @@ -157,12 +185,10 @@ export class CustomRecordCodec extends SyrupRecordCodecType { * @param {function(import('./decode.js').SyrupReader): any} options.unmarshalBody */ constructor(label, { marshalBody, unmarshalBody }) { - super(label, []); + super(label); this.marshalBody = marshalBody; this.unmarshalBody = unmarshalBody; } - - } export class RecordUnionCodec extends SyrupCodec { @@ -183,6 +209,9 @@ export class RecordUnionCodec extends SyrupCodec { }) ); } + supports(label) { + return this.recordTable[label] !== undefined; + } unmarshal(syrupReader) { syrupReader.enterRecord(); const label = syrupReader.readSymbolAsString(); @@ -206,18 +235,39 @@ export class RecordUnionCodec extends SyrupCodec { export const SyrupListCodec = new SimpleSyrupCodecType({ unmarshal(syrupReader) { - throw Error('SyrupListCodec: unmarshal must be implemented'); + syrupReader.enterList(); + const result = []; + while (!syrupReader.peekListEnd()) { + const value = syrupReader.readAny(); + console.log('readAny', value); + result.push(value); + } + syrupReader.exitList(); + return result; }, marshal(value, syrupWriter) { throw Error('SyrupListCodec: marshal must be implemented'); }, }); -export const SyrupStructCodec = new SimpleSyrupCodecType({ +export class CustomUnionCodecType extends SyrupCodec { + /** + * @param {object} options + * @param {function(import('./decode.js').SyrupReader): SyrupCodec} options.selectCodecForUnmarshal + * @param {function(any): SyrupCodec} options.selectCodecForMarshal + */ + constructor ({ selectCodecForUnmarshal, selectCodecForMarshal }) { + super(); + this.selectCodecForUnmarshal = selectCodecForUnmarshal; + this.selectCodecForMarshal = selectCodecForMarshal; + } unmarshal(syrupReader) { - throw Error('SyrupStructCodec: unmarshal must be implemented'); - }, + const codec = this.selectCodecForUnmarshal(syrupReader); + return codec.unmarshal(syrupReader); + } marshal(value, syrupWriter) { - throw Error('SyrupStructCodec: marshal must be implemented'); - }, -}); + const codec = this.selectCodecForMarshal(value); + codec.marshal(value, syrupWriter); + } +} + diff --git a/packages/syrup/src/encode.js b/packages/syrup/src/encode.js index b792665c4f..a721062834 100644 --- a/packages/syrup/src/encode.js +++ b/packages/syrup/src/encode.js @@ -233,6 +233,9 @@ export class SyrupWriter { constructor(bufferWriter) { this.bufferWriter = bufferWriter; } + writeAny(value) { + writeAny(this.bufferWriter, value, [], '/'); + } writeSymbol(value) { writeSymbol(this.bufferWriter, value); } @@ -266,6 +269,12 @@ export class SyrupWriter { exitRecord() { this.bufferWriter.writeByte(RECORD_END); } + enterList() { + this.bufferWriter.writeByte(LIST_START); + } + exitList() { + this.bufferWriter.writeByte(LIST_END); + } /** * @param {'boolean' | 'integer' | 'float64' | 'string' | 'bytestring' | 'symbol'} type * @param {any} value diff --git a/packages/syrup/src/import-export.js b/packages/syrup/src/import-export.js new file mode 100644 index 0000000000..6c2490f949 --- /dev/null +++ b/packages/syrup/src/import-export.js @@ -0,0 +1,26 @@ +import { SyrupStructuredRecordCodecType } from './codec.js'; + +/* + These are OCapN Descriptors, they are Passables that are used both + directly in OCapN Messages and as part of Passable structures. +*/ + +export const DescImportObject = new SyrupStructuredRecordCodecType( + 'desc:import-object', [ + ['position', 'integer'], +]) + +export const DescImportPromise = new SyrupStructuredRecordCodecType( + 'desc:import-promise', [ + ['position', 'integer'], +]) + +export const DescExport = new SyrupStructuredRecordCodecType( + 'desc:export', [ + ['position', 'integer'], +]) + +export const DescAnswer = new SyrupStructuredRecordCodecType( + 'desc:answer', [ + ['position', 'integer'], +]) \ No newline at end of file diff --git a/packages/syrup/src/ocapn.js b/packages/syrup/src/ocapn.js index 1ae548d2d9..bf341cd838 100644 --- a/packages/syrup/src/ocapn.js +++ b/packages/syrup/src/ocapn.js @@ -1,4 +1,6 @@ -import { SyrupRecordCodecType, SyrupCodec, RecordUnionCodec } from './codec.js'; +import { SyrupCodec, RecordUnionCodec, SyrupStructuredRecordCodecType, SimpleSyrupCodecType } from './codec.js'; +import { OCapNPassableUnionCodec } from './passable.js'; +import { DescImportObject, DescImportPromise, DescExport, DescAnswer } from './import-export.js'; // OCapN Components @@ -27,27 +29,27 @@ export class OCapNSignatureValueCodec extends SyrupCodec { const OCapNSignatureRValue = new OCapNSignatureValueCodec('r'); const OCapNSignatureSValue = new OCapNSignatureValueCodec('s'); -const OCapNSignature = new SyrupRecordCodecType( +const OCapNSignature = new SyrupStructuredRecordCodecType( 'sig-val', [ ['scheme', 'symbol'], ['r', OCapNSignatureRValue], ['s', OCapNSignatureSValue], ]) -const OCapNNode = new SyrupRecordCodecType( +const OCapNNode = new SyrupStructuredRecordCodecType( 'ocapn-node', [ ['transport', 'symbol'], ['address', 'bytestring'], ['hints', 'boolean'], ]) -const OCapNSturdyRef = new SyrupRecordCodecType( +const OCapNSturdyRef = new SyrupStructuredRecordCodecType( 'ocapn-sturdyref', [ ['node', OCapNNode], ['swissNum', 'string'], ]) -const OCapNPublicKey = new SyrupRecordCodecType( +const OCapNPublicKey = new SyrupStructuredRecordCodecType( 'public-key', [ ['scheme', 'symbol'], ['curve', 'symbol'], @@ -65,27 +67,7 @@ const OCapNComponentCodecs = { // OCapN Descriptors -export const DescImportObject = new SyrupRecordCodecType( - 'desc:import-object', [ - ['position', 'integer'], -]) - -export const DescImportPromise = new SyrupRecordCodecType( - 'desc:import-promise', [ - ['position', 'integer'], -]) - -export const DescExport = new SyrupRecordCodecType( - 'desc:export', [ - ['position', 'integer'], -]) - -export const DescAnswer = new SyrupRecordCodecType( - 'desc:answer', [ - ['position', 'integer'], -]) - -const DescHandoffGive = new SyrupRecordCodecType( +const DescHandoffGive = new SyrupStructuredRecordCodecType( 'desc:handoff-give', [ ['receiverKey', OCapNPublicKey], ['exporterLocation', OCapNNode], @@ -94,14 +76,14 @@ const DescHandoffGive = new SyrupRecordCodecType( ['giftId', 'bytestring'], ]) -const DescSigGiveEnvelope = new SyrupRecordCodecType( +const DescSigGiveEnvelope = new SyrupStructuredRecordCodecType( 'desc:sig-envelope', [ // TODO: verify union type, can be DescHandoffReceive, DescHandoffGive, ... ['object', DescHandoffGive], ['signature', OCapNSignature], ]) -const DescHandoffReceive = new SyrupRecordCodecType( +const DescHandoffReceive = new SyrupStructuredRecordCodecType( 'desc:handoff-receive', [ ['receivingSession', 'bytestring'], ['receivingSide', 'bytestring'], @@ -109,7 +91,7 @@ const DescHandoffReceive = new SyrupRecordCodecType( ['signedGive', DescSigGiveEnvelope], ]) -const DescSigReceiveEnvelope = new SyrupRecordCodecType( +const DescSigReceiveEnvelope = new SyrupStructuredRecordCodecType( 'desc:sig-envelope', [ // TODO: verify union type, can be DescHandoffReceive, DescHandoffGive, ... ['object', DescHandoffReceive], @@ -136,7 +118,7 @@ const OCapNDescriptorCodecs = { // OCapN Operations -const OpStartSession = new SyrupRecordCodecType( +const OpStartSession = new SyrupStructuredRecordCodecType( 'op:start-session', [ ['captpVersion', 'string'], ['sessionPublicKey', OCapNPublicKey], @@ -152,7 +134,7 @@ const OCapNDeliverResolveMeDescs = { const OCapNResolveMeDescCodec = new RecordUnionCodec(OCapNDeliverResolveMeDescs); -const OpListen = new SyrupRecordCodecType( +const OpListen = new SyrupStructuredRecordCodecType( 'op:listen', [ ['to', DescExport], ['resolveMeDesc', OCapNResolveMeDescCodec], @@ -166,42 +148,87 @@ const OCapNDeliverTargets = { const OCapNDeliverTargetCodec = new RecordUnionCodec(OCapNDeliverTargets); +// Used by the deliver and deliver-only operations +// First arg is method name, rest are Passables +const OpDeliverArgsCodec = new SimpleSyrupCodecType({ + unmarshal: (syrupReader) => { + syrupReader.enterList(); + const result = [ + // method name + syrupReader.readSymbolAsString(), + ]; + while (!syrupReader.peekListEnd()) { + result.push( + OCapNPassableUnionCodec.unmarshal(syrupReader) + ) + } + syrupReader.exitList(); + return result; + }, + marshal: ([methodName, ...args], syrupWriter) => { + syrupWriter.enterList(); + syrupWriter.writeSymbol(methodName); + for (const arg of args) { + OCapNPassableUnionCodec.marshal(arg, syrupWriter); + } + syrupWriter.exitList(); + }, +}) -const OpDeliverOnly = new SyrupRecordCodecType( +const OpDeliverOnly = new SyrupStructuredRecordCodecType( 'op:deliver-only', [ ['to', OCapNDeliverTargetCodec], - // TODO: list type, can include OCapNSturdyRef, ... - // see https://github.com/ocapn/ocapn/blob/main/implementation-guide/Implementation%20Guide.md#stage-2-promises-opdeliver-oplisten - ['args', 'list'], + ['args', OpDeliverArgsCodec], ]) -const OpDeliver = new SyrupRecordCodecType( +const OpDeliverAnswerCodec = new SimpleSyrupCodecType({ + unmarshal: (syrupReader) => { + const typeHint = syrupReader.peekTypeHint(); + if (typeHint === 'number-prefix') { + // should be an integer + return syrupReader.readInteger(); + } + if (typeHint === 'boolean') { + return syrupReader.readBoolean(); + } + throw Error(`Expected integer or boolean, got ${typeHint}`); + }, + marshal: (value, syrupWriter) => { + if (typeof value === 'bigint') { + syrupWriter.writeInteger(value); + } else if (typeof value === 'boolean') { + syrupWriter.writeBoolean(value); + } else { + throw Error(`Expected integer or boolean, got ${typeof value}`); + } + }, +}); + +const OpDeliver = new SyrupStructuredRecordCodecType( 'op:deliver', [ ['to', OCapNDeliverTargetCodec], - // TODO: list type, can be DescSigEnvelope - // see https://github.com/ocapn/ocapn/blob/main/implementation-guide/Implementation%20Guide.md#stage-2-promises-opdeliver-oplisten - ['args', 'list'], - ['answerPosition', 'integer'], + ['args', OpDeliverArgsCodec], + ['answerPosition', OpDeliverAnswerCodec], ['resolveMeDesc', OCapNResolveMeDescCodec], ]) -const OpAbort = new SyrupRecordCodecType( +const OpAbort = new SyrupStructuredRecordCodecType( 'op:abort', [ ['reason', 'string'], ]) -const OpGcExport = new SyrupRecordCodecType( +const OpGcExport = new SyrupStructuredRecordCodecType( 'op:gc-export', [ ['exportPosition', 'integer'], ['wireDelta', 'integer'], ]) -const OpGcAnswer = new SyrupRecordCodecType( +const OpGcAnswer = new SyrupStructuredRecordCodecType( 'op:gc-answer', [ ['answerPosition', 'integer'], ]) -const OpGcSession = new SyrupRecordCodecType( +const OpGcSession = new SyrupStructuredRecordCodecType( 'op:gc-session', [ ['session', 'bytestring'], ]) diff --git a/packages/syrup/src/passable.js b/packages/syrup/src/passable.js index bbd2bb2da7..10ff4249f6 100644 --- a/packages/syrup/src/passable.js +++ b/packages/syrup/src/passable.js @@ -1,27 +1,23 @@ -import { SyrupRecordCodecType, RecordUnionCodec, SimpleSyrupCodecType, SyrupBooleanCodec, SyrupIntegerCodec, SyrupDoubleCodec, SyrupSymbolCodec, SyrupStringCodec, SyrupBytestringCodec, SyrupListCodec, SyrupStructCodec, CustomRecordCodec } from './codec.js'; -import { DescAnswer, DescExport, DescImportObject, DescImportPromise } from './ocapn.js'; +import { RecordUnionCodec, SimpleSyrupCodecType, SyrupBooleanCodec, SyrupIntegerCodec, SyrupDoubleCodec, SyrupSymbolCodec, SyrupStringCodec, SyrupBytestringCodec, SyrupListCodec, CustomRecordCodec, CustomUnionCodecType, SyrupAnyCodec, SyrupStructuredRecordCodecType } from './codec.js'; +import { DescImportObject, DescImportPromise, DescExport, DescAnswer } from './import-export.js'; // OCapN Passable Atoms -const UndefinedCodec = new SimpleSyrupCodecType({ - unmarshal(syrupReader) { +const UndefinedCodec = new CustomRecordCodec('void', { + unmarshalBody(syrupReader) { return undefined; }, - marshal(value, syrupWriter) { - syrupWriter.enterRecord(); - syrupWriter.writeSymbol('void'); - syrupWriter.exitRecord(); + marshalBody(value, syrupWriter) { + // body is empty }, }); -const NullCodec = new SimpleSyrupCodecType({ - unmarshal(syrupReader) { +const NullCodec = new CustomRecordCodec('null', { + unmarshalBody(syrupReader) { return null; }, - marshal(value, syrupWriter) { - syrupWriter.enterRecord(); - syrupWriter.writeSymbol('null'); - syrupWriter.exitRecord(); + marshalBody(value, syrupWriter) { + // body is empty }, }); @@ -41,12 +37,16 @@ const AtomCodecs = { // OCapN Passable Containers -// const OCapNTaggedCodec = new SyrupRecordCodecType( -// 'desc:tagged', [ -// ['tagName', 'symbol'], -// // TODO: any type -// ['value', 'any'], -// ]) +// TODO: dictionary but with only string keys +export const OCapNStructCodec = new SimpleSyrupCodecType({ + unmarshal(syrupReader) { + throw Error('OCapNStructCodec: unmarshal must be implemented'); + }, + marshal(value, syrupWriter) { + throw Error('OCapNStructCodec: marshal must be implemented'); + }, +}); + const OCapNTaggedCodec = new CustomRecordCodec('desc:tagged', { unmarshalBody(syrupReader) { const tagName = syrupReader.readSymbolAsString(); @@ -63,12 +63,11 @@ const OCapNTaggedCodec = new CustomRecordCodec('desc:tagged', { syrupWriter.writeSymbol(value.tagName); value.value.marshal(syrupWriter); }, - }) const ContainerCodecs = { list: SyrupListCodec, - struct: SyrupStructCodec, + struct: OCapNStructCodec, tagged: OCapNTaggedCodec, } @@ -91,8 +90,91 @@ const OCapNReferenceCodecs = { // OCapN Error -const OCapNErrorCodec = new SyrupRecordCodecType( +const OCapNErrorCodec = new SyrupStructuredRecordCodecType( 'desc:error', [ ['message', 'string'], ]) +const OCapNPassableCodecs = { + ...AtomCodecs, + ...ContainerCodecs, + ...OCapNReferenceCodecs, + ...OCapNErrorCodec, +} + +// all record based passables +const OCapNPassableRecordUnionCodec = new RecordUnionCodec({ + UndefinedCodec, + NullCodec, + OCapNTaggedCodec, + DescExport, + DescImportObject, + DescImportPromise, + DescAnswer, + OCapNErrorCodec, +}); + +export const OCapNPassableUnionCodec = new CustomUnionCodecType({ + selectCodecForUnmarshal(syrupReader) { + const typeHint = syrupReader.peekTypeHint(); + switch (typeHint) { + case 'boolean': + return AtomCodecs.boolean; + case 'float64': + return AtomCodecs.float64; + case 'number-prefix': + // can be string, bytestring, symbol, integer + // We'll return the any codec in place of those + return SyrupAnyCodec + case 'list': + return ContainerCodecs.list; + case 'record': + // many possible matches, the union codec will select the correct one + return OCapNPassableRecordUnionCodec; + case 'dictionary': + return ContainerCodecs.struct; + default: + throw Error(`Unknown type hint: ${typeHint}`); + } + }, + selectCodecForMarshal(value) { + if (value === undefined) { + return AtomCodecs.undefined; + } + if (value === null) { + return AtomCodecs.null; + } + if (typeof value === 'boolean') { + return AtomCodecs.boolean; + } + if (typeof value === 'number') { + return AtomCodecs.float64; + } + if (typeof value === 'string') { + return AtomCodecs.string; + } + if (typeof value === 'symbol') { + return AtomCodecs.symbol; + } + if (typeof value === 'bigint') { + return AtomCodecs.integer; + } + if (value instanceof Uint8Array) { + return AtomCodecs.byteArray; + } + if (typeof value === 'object') { + if (Array.isArray(value)) { + return ContainerCodecs.list; + } + if (value[Symbol.for('passStyle')] === 'tagged') { + return ContainerCodecs.tagged; + } + if (value.type !== undefined && OCapNPassableRecordUnionCodec.supports(value.type)) { + return OCapNPassableRecordUnionCodec; + } + // TODO: need to distinguish OCapNReferenceCodecs and OCapNErrorCodec + return ContainerCodecs.struct; + } + throw Error(`Unknown value: ${value}`); + }, +}); diff --git a/packages/syrup/test/_ocapn.js b/packages/syrup/test/_ocapn.js index e17280b1f9..c9b5f975b8 100644 --- a/packages/syrup/test/_ocapn.js +++ b/packages/syrup/test/_ocapn.js @@ -2,8 +2,8 @@ const sym = (s) => `${s.length}'${s}`; const str = (s) => `${s.length}"${s}`; const bts = (s) => `${s.length}:${s}`; const bool = (b) => b ? 't' : 'f'; -const int = (i) => `${Math.floor(Math.abs(i))}${i === 0 ? '' : i < 0 ? '-' : '+'}`; - +const int = (i) => `${Math.floor(Math.abs(i))}${i < 0 ? '-' : '+'}`; +const list = (items) => `[${items.join('')}]`; const makeNode = (transport, address, hints) => { return `<10'ocapn-node${sym(transport)}${bts(address)}${bool(hints)}>`; } @@ -20,6 +20,18 @@ const makeSig = (scheme, r, s) => { return `<${sym('sig-val')}${sym(scheme)}${makeSigComp('r', r)}${makeSigComp('s', s)}>`; } +const makeExport = (position) => { + return `<${sym('desc:export')}${int(position)}>`; +} + +const makeImport = (position) => { + return `<${sym('desc:import-object')}${int(position)}>`; +} + +const strToUint8Array = (str) => { + return new Uint8Array(str.split('').map(c => c.charCodeAt(0))); +} + export const componentsTable = [ { syrup: `${makeSig('eddsa', '1', '2')}`, @@ -156,4 +168,93 @@ export const descriptorsTable = [ export const operationsTable = [ { syrup: '<8\'op:abort7"explode>', value: { type: 'op:abort', reason: 'explode' } }, + { + // ['fulfill ]> + syrup: `<${sym('op:deliver-only')}${makeExport(1)}${list([sym('fulfill'), makeImport(1)])}>`, + value: { + type: 'op:deliver-only', + to: { + type: 'desc:export', + position: 1n + }, + args: [ + 'fulfill', + { + type: 'desc:import-object', + position: 1n + } + ] + } + }, + { + // ; Remote bootstrap object + // ['deposit-gift ; Symbol "deposit-gift" + // 42 ; gift-id, a positive integer + // ]> ; remote object being shared + syrup: `<${sym('op:deliver-only')}${makeExport(0)}${list([sym('deposit-gift'), int(42), makeImport(1)])}>`, + value: { + type: 'op:deliver-only', + to: { + type: 'desc:export', + position: 0n + }, + args: [ + 'deposit-gift', + 42n, + { type: 'desc:import-object', position: 1n } + ] + } + }, + { + // ['make-car-factory] 3 > + syrup: `<${sym('op:deliver')}${makeExport(5)}${list([sym('make-car-factory')])}${int(3)}${makeImport(15)}>`, + value: { + type: 'op:deliver', + to: { + type: 'desc:export', + position: 5n + }, + args: ['make-car-factory'], + answerPosition: 3n, + resolveMeDesc: { + type: 'desc:import-object', + position: 15n + }, + } + }, + { + // ['beep] false > + syrup: `<${sym('op:deliver')}${makeExport(1)}${list([sym('beep')])}${bool(false)}${makeImport(2)}>`, + value: { + type: 'op:deliver', + to: { + type: 'desc:export', + position: 1n + }, + args: ['beep'], + answerPosition: false, + resolveMeDesc: { + type: 'desc:import-object', + position: 2n + } + } + }, + { + // ; Remote bootstrap object + // ['fetch ; Argument 1: Symbol "fetch" + // swiss-number] ; Argument 2: Binary Data + // 3 ; Answer position: positive integer + // > ; object exported by us at position 5 should provide the answer + syrup: `<${sym('op:deliver')}${makeExport(0)}${list([sym('fetch'), bts('swiss-number')])}${int(3)}${makeImport(5)}>`, + value: { + type: 'op:deliver', + to: { type: 'desc:export', position: 0n }, + args: ['fetch', strToUint8Array('swiss-number')], + answerPosition: 3n, + resolveMeDesc: { + type: 'desc:import-object', + position: 5n + } + } + } ]; diff --git a/packages/syrup/test/codec.test.js b/packages/syrup/test/codec.test.js index c1408cbd63..254c82fe6d 100644 --- a/packages/syrup/test/codec.test.js +++ b/packages/syrup/test/codec.test.js @@ -3,7 +3,7 @@ import test from 'ava'; import { makeSyrupReader } from '../src/decode.js'; import { makeSyrupWriter } from '../src/encode.js'; -import { RecordUnionCodec, SyrupRecordCodecType, SyrupStringCodec } from '../src/codec.js'; +import { RecordUnionCodec, SyrupStringCodec, SyrupStructuredRecordCodecType } from '../src/codec.js'; const testCodecBidirectionally = (t, codec, value) => { @@ -22,7 +22,7 @@ test('simple string codec', t => { }); test('basic record codec cases', t => { - const codec = new SyrupRecordCodecType('test', [ + const codec = new SyrupStructuredRecordCodecType('test', [ ['field1', 'string'], ['field2', 'integer'], ]); @@ -36,11 +36,11 @@ test('basic record codec cases', t => { test('record union codec', t => { const codec = new RecordUnionCodec({ - testA: new SyrupRecordCodecType('testA', [ + testA: new SyrupStructuredRecordCodecType('testA', [ ['field1', 'string'], ['field2', 'integer'], ]), - testB: new SyrupRecordCodecType('testB', [ + testB: new SyrupStructuredRecordCodecType('testB', [ ['field1', 'string'], ['field2', 'integer'], ]), diff --git a/packages/syrup/test/ocapn.test.js b/packages/syrup/test/ocapn.test.js index 3afd933aec..50df233b40 100644 --- a/packages/syrup/test/ocapn.test.js +++ b/packages/syrup/test/ocapn.test.js @@ -5,12 +5,13 @@ import { makeSyrupReader } from '../src/decode.js'; import { makeSyrupWriter } from '../src/encode.js'; import { OCapNComponentUnionCodec, OCapNDescriptorUnionCodec, OCapNMessageUnionCodec } from '../src/ocapn.js'; import { componentsTable, descriptorsTable, operationsTable } from './_ocapn.js'; +import { OCapNPassableUnionCodec } from '../src/passable.js'; + +const textEncoder = new TextEncoder(); +const sym = (s) => `${s.length}'${s}`; const testBidirectionally = (t, codec, syrup, value, testName) => { - const syrupBytes = new Uint8Array(syrup.length); - for (let i = 0; i < syrup.length; i += 1) { - syrupBytes[i] = syrup.charCodeAt(i); - } + const syrupBytes = textEncoder.encode(syrup); const syrupReader = makeSyrupReader(syrupBytes, { name: testName }); let result; t.notThrows(() => { @@ -22,7 +23,8 @@ const testBidirectionally = (t, codec, syrup, value, testName) => { codec.marshal(value, syrupWriter); }, testName); const bytes2 = syrupWriter.bufferWriter.subarray(0, syrupWriter.bufferWriter.length); - t.deepEqual(bytes2, syrupBytes, testName); + const syrup2 = new TextDecoder().decode(bytes2); + t.deepEqual(syrup2, syrup, testName); } test('affirmative component cases', t => { @@ -32,16 +34,26 @@ test('affirmative component cases', t => { } }); -test('affirmative descriptor read cases', t => { +test('affirmative descriptor cases', t => { const codec = OCapNDescriptorUnionCodec; for (const { syrup, value } of descriptorsTable) { testBidirectionally(t, codec, syrup, value, `for ${JSON.stringify(syrup)}`); } }); -test('affirmative operation read cases', t => { +test('affirmative operation cases', t => { const codec = OCapNMessageUnionCodec; for (const { syrup, value } of operationsTable) { testBidirectionally(t, codec, syrup, value, `for ${JSON.stringify(syrup)}`); } }); + +test('error on unknown record type in passable', t => { + const codec = OCapNPassableUnionCodec; + const syrup = `<${sym('unknown-record-type')}>`; + const syrupBytes = textEncoder.encode(syrup); + const syrupReader = makeSyrupReader(syrupBytes, { name: 'unknown record type' }); + t.throws(() => { + codec.unmarshal(syrupReader); + }, { message: 'Unknown record type: unknown-record-type' }); +}); From 21992d7d423a72b0f0ede94a55766f6419b2ca1f Mon Sep 17 00:00:00 2001 From: kumavis Date: Sat, 5 Apr 2025 00:28:03 -1000 Subject: [PATCH 14/31] refactor(syrup): split ocapn codecs into components, descriptors, passables --- packages/syrup/src/import-export.js | 26 -- packages/syrup/src/ocapn.js | 276 --------------------- packages/syrup/src/ocapn/components.js | 75 ++++++ packages/syrup/src/ocapn/descriptors.js | 81 ++++++ packages/syrup/src/ocapn/operations.js | 144 +++++++++++ packages/syrup/src/{ => ocapn}/passable.js | 8 +- packages/syrup/test/_ocapn.js | 243 ++++++++++++++---- packages/syrup/test/ocapn.test.js | 6 +- 8 files changed, 508 insertions(+), 351 deletions(-) delete mode 100644 packages/syrup/src/import-export.js delete mode 100644 packages/syrup/src/ocapn.js create mode 100644 packages/syrup/src/ocapn/components.js create mode 100644 packages/syrup/src/ocapn/descriptors.js create mode 100644 packages/syrup/src/ocapn/operations.js rename packages/syrup/src/{ => ocapn}/passable.js (95%) diff --git a/packages/syrup/src/import-export.js b/packages/syrup/src/import-export.js deleted file mode 100644 index 6c2490f949..0000000000 --- a/packages/syrup/src/import-export.js +++ /dev/null @@ -1,26 +0,0 @@ -import { SyrupStructuredRecordCodecType } from './codec.js'; - -/* - These are OCapN Descriptors, they are Passables that are used both - directly in OCapN Messages and as part of Passable structures. -*/ - -export const DescImportObject = new SyrupStructuredRecordCodecType( - 'desc:import-object', [ - ['position', 'integer'], -]) - -export const DescImportPromise = new SyrupStructuredRecordCodecType( - 'desc:import-promise', [ - ['position', 'integer'], -]) - -export const DescExport = new SyrupStructuredRecordCodecType( - 'desc:export', [ - ['position', 'integer'], -]) - -export const DescAnswer = new SyrupStructuredRecordCodecType( - 'desc:answer', [ - ['position', 'integer'], -]) \ No newline at end of file diff --git a/packages/syrup/src/ocapn.js b/packages/syrup/src/ocapn.js deleted file mode 100644 index bf341cd838..0000000000 --- a/packages/syrup/src/ocapn.js +++ /dev/null @@ -1,276 +0,0 @@ -import { SyrupCodec, RecordUnionCodec, SyrupStructuredRecordCodecType, SimpleSyrupCodecType } from './codec.js'; -import { OCapNPassableUnionCodec } from './passable.js'; -import { DescImportObject, DescImportPromise, DescExport, DescAnswer } from './import-export.js'; - -// OCapN Components - -export class OCapNSignatureValueCodec extends SyrupCodec { - /** - * @param {string} expectedLabel - */ - constructor(expectedLabel) { - super(); - this.expectedLabel = expectedLabel; - } - unmarshal(syrupReader) { - const label = syrupReader.readSymbolAsString(); - if (label !== this.expectedLabel) { - throw Error(`Expected label ${this.expectedLabel}, got ${label}`); - } - const value = syrupReader.readBytestring(); - return value; - } - marshal(value, syrupWriter) { - syrupWriter.writeSymbol(this.expectedLabel); - syrupWriter.writeBytestring(value); - } -} - -const OCapNSignatureRValue = new OCapNSignatureValueCodec('r'); -const OCapNSignatureSValue = new OCapNSignatureValueCodec('s'); - -const OCapNSignature = new SyrupStructuredRecordCodecType( - 'sig-val', [ - ['scheme', 'symbol'], - ['r', OCapNSignatureRValue], - ['s', OCapNSignatureSValue], -]) - -const OCapNNode = new SyrupStructuredRecordCodecType( - 'ocapn-node', [ - ['transport', 'symbol'], - ['address', 'bytestring'], - ['hints', 'boolean'], -]) - -const OCapNSturdyRef = new SyrupStructuredRecordCodecType( - 'ocapn-sturdyref', [ - ['node', OCapNNode], - ['swissNum', 'string'], -]) - -const OCapNPublicKey = new SyrupStructuredRecordCodecType( - 'public-key', [ - ['scheme', 'symbol'], - ['curve', 'symbol'], - ['flags', 'symbol'], - ['q', 'bytestring'], -]) - - -const OCapNComponentCodecs = { - OCapNNode, - OCapNSturdyRef, - OCapNPublicKey, - OCapNSignature, -} - -// OCapN Descriptors - -const DescHandoffGive = new SyrupStructuredRecordCodecType( - 'desc:handoff-give', [ - ['receiverKey', OCapNPublicKey], - ['exporterLocation', OCapNNode], - ['session', 'bytestring'], - ['gifterSide', OCapNPublicKey], - ['giftId', 'bytestring'], -]) - -const DescSigGiveEnvelope = new SyrupStructuredRecordCodecType( - 'desc:sig-envelope', [ - // TODO: verify union type, can be DescHandoffReceive, DescHandoffGive, ... - ['object', DescHandoffGive], - ['signature', OCapNSignature], -]) - -const DescHandoffReceive = new SyrupStructuredRecordCodecType( - 'desc:handoff-receive', [ - ['receivingSession', 'bytestring'], - ['receivingSide', 'bytestring'], - ['handoffCount', 'integer'], - ['signedGive', DescSigGiveEnvelope], -]) - -const DescSigReceiveEnvelope = new SyrupStructuredRecordCodecType( - 'desc:sig-envelope', [ - // TODO: verify union type, can be DescHandoffReceive, DescHandoffGive, ... - ['object', DescHandoffReceive], - ['signature', OCapNSignature], -]) - - -// Note: this may only be useful for testing -const OCapNDescriptorCodecs = { - OCapNNode, - OCapNSturdyRef, - OCapNPublicKey, - OCapNSignature, - DescSigGiveEnvelope, - // TODO: ambiguous record label for DescSigGiveEnvelope and DescSigReceiveEnvelope - // DescSigReceiveEnvelope, - DescImportObject, - DescImportPromise, - DescExport, - DescAnswer, - DescHandoffGive, - DescHandoffReceive, -} - -// OCapN Operations - -const OpStartSession = new SyrupStructuredRecordCodecType( - 'op:start-session', [ - ['captpVersion', 'string'], - ['sessionPublicKey', OCapNPublicKey], - ['location', OCapNNode], - ['locationSignature', OCapNSignature], -]) - - -const OCapNDeliverResolveMeDescs = { - DescImportObject, - DescImportPromise, -} - -const OCapNResolveMeDescCodec = new RecordUnionCodec(OCapNDeliverResolveMeDescs); - -const OpListen = new SyrupStructuredRecordCodecType( - 'op:listen', [ - ['to', DescExport], - ['resolveMeDesc', OCapNResolveMeDescCodec], - ['wantsPartial', 'boolean'], -]) - -const OCapNDeliverTargets = { - DescExport, - DescAnswer, -} - -const OCapNDeliverTargetCodec = new RecordUnionCodec(OCapNDeliverTargets); - -// Used by the deliver and deliver-only operations -// First arg is method name, rest are Passables -const OpDeliverArgsCodec = new SimpleSyrupCodecType({ - unmarshal: (syrupReader) => { - syrupReader.enterList(); - const result = [ - // method name - syrupReader.readSymbolAsString(), - ]; - while (!syrupReader.peekListEnd()) { - result.push( - OCapNPassableUnionCodec.unmarshal(syrupReader) - ) - } - syrupReader.exitList(); - return result; - }, - marshal: ([methodName, ...args], syrupWriter) => { - syrupWriter.enterList(); - syrupWriter.writeSymbol(methodName); - for (const arg of args) { - OCapNPassableUnionCodec.marshal(arg, syrupWriter); - } - syrupWriter.exitList(); - }, -}) - -const OpDeliverOnly = new SyrupStructuredRecordCodecType( - 'op:deliver-only', [ - ['to', OCapNDeliverTargetCodec], - ['args', OpDeliverArgsCodec], -]) - -const OpDeliverAnswerCodec = new SimpleSyrupCodecType({ - unmarshal: (syrupReader) => { - const typeHint = syrupReader.peekTypeHint(); - if (typeHint === 'number-prefix') { - // should be an integer - return syrupReader.readInteger(); - } - if (typeHint === 'boolean') { - return syrupReader.readBoolean(); - } - throw Error(`Expected integer or boolean, got ${typeHint}`); - }, - marshal: (value, syrupWriter) => { - if (typeof value === 'bigint') { - syrupWriter.writeInteger(value); - } else if (typeof value === 'boolean') { - syrupWriter.writeBoolean(value); - } else { - throw Error(`Expected integer or boolean, got ${typeof value}`); - } - }, -}); - -const OpDeliver = new SyrupStructuredRecordCodecType( - 'op:deliver', [ - ['to', OCapNDeliverTargetCodec], - ['args', OpDeliverArgsCodec], - ['answerPosition', OpDeliverAnswerCodec], - ['resolveMeDesc', OCapNResolveMeDescCodec], -]) - -const OpAbort = new SyrupStructuredRecordCodecType( - 'op:abort', [ - ['reason', 'string'], -]) - -const OpGcExport = new SyrupStructuredRecordCodecType( - 'op:gc-export', [ - ['exportPosition', 'integer'], - ['wireDelta', 'integer'], -]) - -const OpGcAnswer = new SyrupStructuredRecordCodecType( - 'op:gc-answer', [ - ['answerPosition', 'integer'], -]) - -const OpGcSession = new SyrupStructuredRecordCodecType( - 'op:gc-session', [ - ['session', 'bytestring'], -]) - -const OCapNOpCodecs = { - OpStartSession, - OpListen, - OpDeliverOnly, - OpDeliver, - OpAbort, - OpGcExport, - OpGcAnswer, - OpGcSession, -} - -export const OCapNMessageUnionCodec = new RecordUnionCodec(OCapNOpCodecs); -export const OCapNDescriptorUnionCodec = new RecordUnionCodec(OCapNDescriptorCodecs); -export const OCapNComponentUnionCodec = new RecordUnionCodec(OCapNComponentCodecs); - -export const readOCapNMessage = (syrupReader) => { - return OCapNMessageUnionCodec.unmarshal(syrupReader); -} - -export const readOCapDescriptor = (syrupReader) => { - return OCapNDescriptorUnionCodec.unmarshal(syrupReader); -} - -export const readOCapComponent = (syrupReader) => { - return OCapNComponentUnionCodec.unmarshal(syrupReader); -} - -export const writeOCapNMessage = (message, syrupWriter) => { - OCapNMessageUnionCodec.marshal(message, syrupWriter); - return syrupWriter.bufferWriter.subarray(0, syrupWriter.bufferWriter.length); -} - -export const writeOCapDescriptor = (descriptor, syrupWriter) => { - OCapNDescriptorUnionCodec.marshal(descriptor, syrupWriter); - return syrupWriter.bufferWriter.subarray(0, syrupWriter.bufferWriter.length); -} - -export const writeOCapComponent = (component, syrupWriter) => { - OCapNComponentUnionCodec.marshal(component, syrupWriter); - return syrupWriter.bufferWriter.subarray(0, syrupWriter.bufferWriter.length); -} diff --git a/packages/syrup/src/ocapn/components.js b/packages/syrup/src/ocapn/components.js new file mode 100644 index 0000000000..2068a088fd --- /dev/null +++ b/packages/syrup/src/ocapn/components.js @@ -0,0 +1,75 @@ +import { RecordUnionCodec, SyrupCodec, SyrupStructuredRecordCodecType } from "../codec.js"; + +/* + * OCapN Components are used in both OCapN Messages and Descriptors + */ + +export class OCapNSignatureValueCodec extends SyrupCodec { + /** + * @param {string} expectedLabel + */ + constructor(expectedLabel) { + super(); + this.expectedLabel = expectedLabel; + } + unmarshal(syrupReader) { + const label = syrupReader.readSymbolAsString(); + if (label !== this.expectedLabel) { + throw Error(`Expected label ${this.expectedLabel}, got ${label}`); + } + const value = syrupReader.readBytestring(); + return value; + } + marshal(value, syrupWriter) { + syrupWriter.writeSymbol(this.expectedLabel); + syrupWriter.writeBytestring(value); + } +} + +const OCapNSignatureRValue = new OCapNSignatureValueCodec('r'); +const OCapNSignatureSValue = new OCapNSignatureValueCodec('s'); + + +export const OCapNSignature = new SyrupStructuredRecordCodecType( + 'sig-val', [ + ['scheme', 'symbol'], + ['r', OCapNSignatureRValue], + ['s', OCapNSignatureSValue], +]) + +export const OCapNNode = new SyrupStructuredRecordCodecType( + 'ocapn-node', [ + ['transport', 'symbol'], + ['address', 'bytestring'], + ['hints', 'boolean'], +]) + +export const OCapNSturdyRef = new SyrupStructuredRecordCodecType( + 'ocapn-sturdyref', [ + ['node', OCapNNode], + ['swissNum', 'string'], +]) + +export const OCapNPublicKey = new SyrupStructuredRecordCodecType( + 'public-key', [ + ['scheme', 'symbol'], + ['curve', 'symbol'], + ['flags', 'symbol'], + ['q', 'bytestring'], +]) + +export const OCapNComponentUnionCodec = new RecordUnionCodec({ + OCapNNode, + OCapNSturdyRef, + OCapNPublicKey, + OCapNSignature, +}); + +export const readOCapComponent = (syrupReader) => { + return OCapNComponentUnionCodec.unmarshal(syrupReader); +} + +export const writeOCapComponent = (component, syrupWriter) => { + OCapNComponentUnionCodec.marshal(component, syrupWriter); + return syrupWriter.bufferWriter.subarray(0, syrupWriter.bufferWriter.length); +} diff --git a/packages/syrup/src/ocapn/descriptors.js b/packages/syrup/src/ocapn/descriptors.js new file mode 100644 index 0000000000..0c9b7937ea --- /dev/null +++ b/packages/syrup/src/ocapn/descriptors.js @@ -0,0 +1,81 @@ +import { RecordUnionCodec, SyrupStructuredRecordCodecType } from '../codec.js'; +import { OCapNNode, OCapNPublicKey, OCapNSignature } from './components.js'; + +/* + * These are OCapN Descriptors, they are Passables that are used both + * directly in OCapN Messages and as part of Passable structures. + */ + +export const DescImportObject = new SyrupStructuredRecordCodecType( + 'desc:import-object', [ + ['position', 'integer'], +]) + +export const DescImportPromise = new SyrupStructuredRecordCodecType( + 'desc:import-promise', [ + ['position', 'integer'], +]) + +export const DescExport = new SyrupStructuredRecordCodecType( + 'desc:export', [ + ['position', 'integer'], +]) + +export const DescAnswer = new SyrupStructuredRecordCodecType( + 'desc:answer', [ + ['position', 'integer'], +]) + +export const DescHandoffGive = new SyrupStructuredRecordCodecType( + 'desc:handoff-give', [ + ['receiverKey', OCapNPublicKey], + ['exporterLocation', OCapNNode], + ['session', 'bytestring'], + ['gifterSide', OCapNPublicKey], + ['giftId', 'bytestring'], +]) + +export const DescSigGiveEnvelope = new SyrupStructuredRecordCodecType( + 'desc:sig-envelope', [ + ['object', DescHandoffGive], + ['signature', OCapNSignature], +]) + +export const DescHandoffReceive = new SyrupStructuredRecordCodecType( + 'desc:handoff-receive', [ + ['receivingSession', 'bytestring'], + ['receivingSide', 'bytestring'], + ['handoffCount', 'integer'], + ['signedGive', DescSigGiveEnvelope], +]) + +export const DescSigReceiveEnvelope = new SyrupStructuredRecordCodecType( + 'desc:sig-envelope', [ + ['object', DescHandoffReceive], + ['signature', OCapNSignature], +]) + +// Note: this may only be useful for testing +export const OCapNDescriptorUnionCodec = new RecordUnionCodec({ + OCapNNode, + OCapNPublicKey, + OCapNSignature, + DescSigGiveEnvelope, + // TODO: ambiguous record label for DescSigGiveEnvelope and DescSigReceiveEnvelope + // DescSigReceiveEnvelope, + DescImportObject, + DescImportPromise, + DescExport, + DescAnswer, + DescHandoffGive, + DescHandoffReceive, +}); + +export const readOCapDescriptor = (syrupReader) => { + return OCapNDescriptorUnionCodec.unmarshal(syrupReader); +} + +export const writeOCapDescriptor = (descriptor, syrupWriter) => { + OCapNDescriptorUnionCodec.marshal(descriptor, syrupWriter); + return syrupWriter.bufferWriter.subarray(0, syrupWriter.bufferWriter.length); +} diff --git a/packages/syrup/src/ocapn/operations.js b/packages/syrup/src/ocapn/operations.js new file mode 100644 index 0000000000..4d1cd1b75c --- /dev/null +++ b/packages/syrup/src/ocapn/operations.js @@ -0,0 +1,144 @@ +import { RecordUnionCodec, SyrupStructuredRecordCodecType, SimpleSyrupCodecType } from '../codec.js'; +import { OCapNNode, OCapNPublicKey, OCapNSignature } from './components.js'; +import { OCapNPassableUnionCodec } from './passable.js'; +import { DescImportObject, DescImportPromise, DescExport, DescAnswer } from './descriptors.js'; + +/* + * These are OCapN Operations, they are messages that are sent between OCapN Nodes + */ + +const OpStartSession = new SyrupStructuredRecordCodecType( + 'op:start-session', [ + ['captpVersion', 'string'], + ['sessionPublicKey', OCapNPublicKey], + ['location', OCapNNode], + ['locationSignature', OCapNSignature], +]) + + +const OCapNDeliverResolveMeDescs = { + DescImportObject, + DescImportPromise, +} + +const OCapNResolveMeDescCodec = new RecordUnionCodec(OCapNDeliverResolveMeDescs); + +const OpListen = new SyrupStructuredRecordCodecType( + 'op:listen', [ + ['to', DescExport], + ['resolveMeDesc', OCapNResolveMeDescCodec], + ['wantsPartial', 'boolean'], +]) + +const OCapNDeliverTargets = { + DescExport, + DescAnswer, +} + +const OCapNDeliverTargetCodec = new RecordUnionCodec(OCapNDeliverTargets); + +// Used by the deliver and deliver-only operations +// First arg is method name, rest are Passables +const OpDeliverArgsCodec = new SimpleSyrupCodecType({ + unmarshal: (syrupReader) => { + syrupReader.enterList(); + const result = [ + // method name + syrupReader.readSymbolAsString(), + ]; + while (!syrupReader.peekListEnd()) { + result.push( + OCapNPassableUnionCodec.unmarshal(syrupReader) + ) + } + syrupReader.exitList(); + return result; + }, + marshal: ([methodName, ...args], syrupWriter) => { + syrupWriter.enterList(); + syrupWriter.writeSymbol(methodName); + for (const arg of args) { + OCapNPassableUnionCodec.marshal(arg, syrupWriter); + } + syrupWriter.exitList(); + }, +}) + +const OpDeliverOnly = new SyrupStructuredRecordCodecType( + 'op:deliver-only', [ + ['to', OCapNDeliverTargetCodec], + ['args', OpDeliverArgsCodec], +]) + +const OpDeliverAnswerCodec = new SimpleSyrupCodecType({ + unmarshal: (syrupReader) => { + const typeHint = syrupReader.peekTypeHint(); + if (typeHint === 'number-prefix') { + // should be an integer + return syrupReader.readInteger(); + } + if (typeHint === 'boolean') { + return syrupReader.readBoolean(); + } + throw Error(`Expected integer or boolean, got ${typeHint}`); + }, + marshal: (value, syrupWriter) => { + if (typeof value === 'bigint') { + syrupWriter.writeInteger(value); + } else if (typeof value === 'boolean') { + syrupWriter.writeBoolean(value); + } else { + throw Error(`Expected integer or boolean, got ${typeof value}`); + } + }, +}); + +const OpDeliver = new SyrupStructuredRecordCodecType( + 'op:deliver', [ + ['to', OCapNDeliverTargetCodec], + ['args', OpDeliverArgsCodec], + ['answerPosition', OpDeliverAnswerCodec], + ['resolveMeDesc', OCapNResolveMeDescCodec], +]) + +const OpAbort = new SyrupStructuredRecordCodecType( + 'op:abort', [ + ['reason', 'string'], +]) + +const OpGcExport = new SyrupStructuredRecordCodecType( + 'op:gc-export', [ + ['exportPosition', 'integer'], + ['wireDelta', 'integer'], +]) + +const OpGcAnswer = new SyrupStructuredRecordCodecType( + 'op:gc-answer', [ + ['answerPosition', 'integer'], +]) + +const OpGcSession = new SyrupStructuredRecordCodecType( + 'op:gc-session', [ + ['session', 'bytestring'], +]) + +export const OCapNMessageUnionCodec = new RecordUnionCodec({ + OpStartSession, + OpListen, + OpDeliverOnly, + OpDeliver, + OpAbort, + OpGcExport, + OpGcAnswer, + OpGcSession, +}); + +export const readOCapNMessage = (syrupReader) => { + return OCapNMessageUnionCodec.unmarshal(syrupReader); +} + +export const writeOCapNMessage = (message, syrupWriter) => { + OCapNMessageUnionCodec.marshal(message, syrupWriter); + return syrupWriter.bufferWriter.subarray(0, syrupWriter.bufferWriter.length); +} + diff --git a/packages/syrup/src/passable.js b/packages/syrup/src/ocapn/passable.js similarity index 95% rename from packages/syrup/src/passable.js rename to packages/syrup/src/ocapn/passable.js index 10ff4249f6..df89942207 100644 --- a/packages/syrup/src/passable.js +++ b/packages/syrup/src/ocapn/passable.js @@ -1,5 +1,5 @@ -import { RecordUnionCodec, SimpleSyrupCodecType, SyrupBooleanCodec, SyrupIntegerCodec, SyrupDoubleCodec, SyrupSymbolCodec, SyrupStringCodec, SyrupBytestringCodec, SyrupListCodec, CustomRecordCodec, CustomUnionCodecType, SyrupAnyCodec, SyrupStructuredRecordCodecType } from './codec.js'; -import { DescImportObject, DescImportPromise, DescExport, DescAnswer } from './import-export.js'; +import { RecordUnionCodec, SimpleSyrupCodecType, SyrupBooleanCodec, SyrupIntegerCodec, SyrupDoubleCodec, SyrupSymbolCodec, SyrupStringCodec, SyrupBytestringCodec, SyrupListCodec, CustomRecordCodec, CustomUnionCodecType, SyrupAnyCodec, SyrupStructuredRecordCodecType } from '../codec.js'; +import { DescImportObject, DescImportPromise, DescExport, DescAnswer, DescHandoffGive, DescHandoffReceive } from './descriptors.js'; // OCapN Passable Atoms @@ -111,6 +111,10 @@ const OCapNPassableRecordUnionCodec = new RecordUnionCodec({ DescImportObject, DescImportPromise, DescAnswer, + DescHandoffGive, + DescHandoffReceive, + // DescSigGiveEnvelope, + // DescSigReceiveEnvelope, OCapNErrorCodec, }); diff --git a/packages/syrup/test/_ocapn.js b/packages/syrup/test/_ocapn.js index c9b5f975b8..6c064998e4 100644 --- a/packages/syrup/test/_ocapn.js +++ b/packages/syrup/test/_ocapn.js @@ -28,10 +28,26 @@ const makeImport = (position) => { return `<${sym('desc:import-object')}${int(position)}>`; } +const makeDescGive = (receiverKey, exporterLocation, session, gifterSide, giftId) => { + return `<${sym('desc:handoff-give')}${receiverKey}${exporterLocation}${bts(session)}${gifterSide}${bts(giftId)}>`; +} + +const makeSigEnvelope = (object, signature) => { + return `<${sym('desc:sig-envelope')}${object}${signature}>`; +} + +const makeHandoffReceive = (recieverSession, recieverSide, handoffCount, descGive, signature) => { + const signedGiveEnvelope = makeSigEnvelope(descGive, signature); + return `<${sym('desc:handoff-receive')}${bts(recieverSession)}${bts(recieverSide)}${int(handoffCount)}${signedGiveEnvelope}>`; +} + const strToUint8Array = (str) => { return new Uint8Array(str.split('').map(c => c.charCodeAt(0))); } +// I made up these syrup values by hand, they may be wrong, sorry. +// Would like external test data for this. + export const componentsTable = [ { syrup: `${makeSig('eddsa', '1', '2')}`, @@ -42,11 +58,6 @@ export const componentsTable = [ s: new Uint8Array([0x32]) } }, -]; - -// I made up these syrup values by hand, they may be wrong, sorry. -// Would like external test data for this. -export const descriptorsTable = [ { syrup: `<10'ocapn-node3'tcp1:0f>`, value: { @@ -76,42 +87,12 @@ export const descriptorsTable = [ scheme: 'ecc', curve: 'Ed25519', flags: 'eddsa', - q: new Uint8Array([0x31]) + q: strToUint8Array('1') } }, - // any - // { - // syrup: '<17\'desc:sig-envelope123+>', - // value: { - // type: 'desc:sig-envelope', - // object: { - // type: 'desc:handoff-give', - // receiverKey: { - // type: 'public-key', - // scheme: 'ed25519', - // curve: 'ed25519', - // flags: 'ed25519', - // q: new Uint8Array(32) - // }, - // exporterLocation: { - // type: 'ocapn-node', - // transport: 'tcp', - // address: '127.0.0.1', - // hints: false - // }, - // session: new Uint8Array(32), - // gifterSide: { - // type: 'public-key', - // scheme: 'ed25519', - // curve: 'ed25519', - // flags: 'ed25519', - // q: new Uint8Array(32) - // }, - // giftId: new Uint8Array(32) - // }, - // signature: new Uint8Array(32) - // } - // }, +]; + +export const descriptorsTable = [ { syrup: `<18'desc:import-object123+>`, value: { @@ -162,8 +143,102 @@ export const descriptorsTable = [ giftId: new Uint8Array([0x34, 0x35, 0x36]) } }, - // TODO: desc:handoff-receive, needs desc:sig-envelope - // { syrup: `<${sym('desc:handoff-receive')}${bts('123')}${bts('456')}${int(1)}${makeSig()}>`, value: { type: 'desc:handoff-receive', receivingSession: '123', receivingSide: '456' } }, + { + syrup: `<${sym('desc:sig-envelope')}${makeDescGive( + makePubKey('ed25519', 'ed25519', 'ed25519', '123'), + makeNode('tcp', '127.0.0.1', false), + '123', + makePubKey('ed25519', 'ed25519', 'ed25519', '123'), + '123' + )}${makeSig('eddsa', '1', '2')}>`, + value: { + type: 'desc:sig-envelope', + object: { + type: 'desc:handoff-give', + receiverKey: { + type: 'public-key', + scheme: 'ed25519', + curve: 'ed25519', + flags: 'ed25519', + q: strToUint8Array('123') + }, + exporterLocation: { + type: 'ocapn-node', + transport: 'tcp', + address: strToUint8Array('127.0.0.1'), + hints: false + }, + session: strToUint8Array('123'), + gifterSide: { + type: 'public-key', + scheme: 'ed25519', + curve: 'ed25519', + flags: 'ed25519', + q: strToUint8Array('123') + }, + giftId: strToUint8Array('123') + }, + signature: { + type: 'sig-val', + scheme: 'eddsa', + r: strToUint8Array('1'), + s: strToUint8Array('2') + } + } + }, + // handoff receive + { + syrup: `<${sym('desc:handoff-receive')}${bts('123')}${bts('456')}${int(1)}${makeSigEnvelope( + makeDescGive( + makePubKey('ecc', 'Ed25519', 'eddsa', '123'), + makeNode('tcp', '456', false), + '789', + makePubKey('ecc', 'Ed25519', 'eddsa', 'abc'), + 'def' + ), + makeSig('eddsa', '1', '2') + )}>`, + value: { + type: 'desc:handoff-receive', + receivingSession: strToUint8Array('123'), + receivingSide: strToUint8Array('456'), + handoffCount: 1n, + signedGive: { + type: 'desc:sig-envelope', + object: { + type: 'desc:handoff-give', + receiverKey: { + type: 'public-key', + scheme: 'ecc', + curve: 'Ed25519', + flags: 'eddsa', + q: strToUint8Array('123') + }, + exporterLocation: { + type: 'ocapn-node', + transport: 'tcp', + address: strToUint8Array('456'), + hints: false + }, + session: strToUint8Array('789'), + gifterSide: { + type: 'public-key', + scheme: 'ecc', + curve: 'Ed25519', + flags: 'eddsa', + q: strToUint8Array('abc') + }, + giftId: strToUint8Array('def') + }, + signature: { + type: 'sig-val', + scheme: 'eddsa', + r: strToUint8Array('1'), + s: strToUint8Array('2') + } + } + } + } ]; export const operationsTable = [ @@ -188,9 +263,9 @@ export const operationsTable = [ }, { // ; Remote bootstrap object - // ['deposit-gift ; Symbol "deposit-gift" - // 42 ; gift-id, a positive integer - // ]> ; remote object being shared + // ['deposit-gift ; Symbol "deposit-gift" + // 42 ; gift-id, a positive integer + // ]> ; remote object being shared syrup: `<${sym('op:deliver-only')}${makeExport(0)}${list([sym('deposit-gift'), int(42), makeImport(1)])}>`, value: { type: 'op:deliver-only', @@ -245,7 +320,10 @@ export const operationsTable = [ // swiss-number] ; Argument 2: Binary Data // 3 ; Answer position: positive integer // > ; object exported by us at position 5 should provide the answer - syrup: `<${sym('op:deliver')}${makeExport(0)}${list([sym('fetch'), bts('swiss-number')])}${int(3)}${makeImport(5)}>`, + syrup: `<${sym('op:deliver')}${makeExport(0)}${list([ + sym('fetch'), + bts('swiss-number') + ])}${int(3)}${makeImport(5)}>`, value: { type: 'op:deliver', to: { type: 'desc:export', position: 0n }, @@ -256,5 +334,80 @@ export const operationsTable = [ position: 5n } } + }, + { + // ; Remote bootstrap object + // [withdraw-gift ; Argument 1: Symbol "withdraw-gift" + // ] ; Argument 2: desc:handoff-receive + // 1 ; Answer position: Positive integer or false + // > ; The object exported (by us) at position 3, should receive the gift. + syrup: `<${sym('op:deliver')}${makeExport(0)}${list([ + sym('withdraw-gift'), + makeHandoffReceive( + '123', + '456', + 1, + makeDescGive( + makePubKey('ecc', 'Ed25519', 'eddsa', '123'), + makeNode('tcp', '456', false), + '789', + makePubKey('ecc', 'Ed25519', 'eddsa', 'abc'), + 'def' + ), + makeSig('eddsa', '1', '2') + ) + ])}${int(1)}${makeImport(3)}>`, + value: { + type: 'op:deliver', + to: { type: 'desc:export', position: 0n }, + args: [ + 'withdraw-gift', + { + type: 'desc:handoff-receive', + receivingSession: strToUint8Array('123'), + receivingSide: strToUint8Array('456'), + handoffCount: 1n, + signedGive: { + type: 'desc:sig-envelope', + object: { + type: 'desc:handoff-give', + receiverKey: { + type: 'public-key', + scheme: 'ecc', + curve: 'Ed25519', + flags: 'eddsa', + q: strToUint8Array('123') + }, + exporterLocation: { + type: 'ocapn-node', + transport: 'tcp', + address: strToUint8Array('456'), + hints: false + }, + session: strToUint8Array('789'), + gifterSide: { + type: 'public-key', + scheme: 'ecc', + curve: 'Ed25519', + flags: 'eddsa', + q: strToUint8Array('abc') + }, + giftId: strToUint8Array('def') + }, + signature: { + type: 'sig-val', + scheme: 'eddsa', + r: strToUint8Array('1'), + s: strToUint8Array('2') + } + }, + } + ], + answerPosition: 1n, + resolveMeDesc: { + type: 'desc:import-object', + position: 3n + } + } } ]; diff --git a/packages/syrup/test/ocapn.test.js b/packages/syrup/test/ocapn.test.js index 50df233b40..8e7ef5a8f0 100644 --- a/packages/syrup/test/ocapn.test.js +++ b/packages/syrup/test/ocapn.test.js @@ -3,9 +3,11 @@ import test from 'ava'; import { makeSyrupReader } from '../src/decode.js'; import { makeSyrupWriter } from '../src/encode.js'; -import { OCapNComponentUnionCodec, OCapNDescriptorUnionCodec, OCapNMessageUnionCodec } from '../src/ocapn.js'; +import { OCapNComponentUnionCodec } from '../src/ocapn/components.js'; +import { OCapNDescriptorUnionCodec } from '../src/ocapn/descriptors.js'; +import { OCapNMessageUnionCodec } from '../src/ocapn/operations.js'; import { componentsTable, descriptorsTable, operationsTable } from './_ocapn.js'; -import { OCapNPassableUnionCodec } from '../src/passable.js'; +import { OCapNPassableUnionCodec } from '../src/ocapn/passable.js'; const textEncoder = new TextEncoder(); const sym = (s) => `${s.length}'${s}`; From ce79a48a9cc89d159aba08a8ec5771143163270f Mon Sep 17 00:00:00 2001 From: kumavis Date: Sat, 5 Apr 2025 00:46:41 -1000 Subject: [PATCH 15/31] feat(syrup): add and test missing ocapn operations --- packages/syrup/src/codec.js | 5 +- packages/syrup/src/ocapn/operations.js | 22 +++-- packages/syrup/test/_ocapn.js | 110 +++++++++++++++++++++++-- packages/syrup/test/ocapn.test.js | 2 +- 4 files changed, 122 insertions(+), 17 deletions(-) diff --git a/packages/syrup/src/codec.js b/packages/syrup/src/codec.js index 313f022e78..5c3c4691f3 100644 --- a/packages/syrup/src/codec.js +++ b/packages/syrup/src/codec.js @@ -1,3 +1,4 @@ +const quote = JSON.stringify; export class SyrupCodec { /** @@ -217,7 +218,7 @@ export class RecordUnionCodec extends SyrupCodec { const label = syrupReader.readSymbolAsString(); const recordCodec = this.recordTable[label]; if (!recordCodec) { - throw Error(`Unknown record type: ${label}`); + throw Error(`Unexpected record type: ${quote(label)}`); } const result = recordCodec.unmarshalBody(syrupReader); syrupReader.exitRecord(); @@ -227,7 +228,7 @@ export class RecordUnionCodec extends SyrupCodec { const { type } = value; const recordCodec = this.recordTable[type]; if (!recordCodec) { - throw Error(`Unknown record type: ${type}`); + throw Error(`Unexpected record type: ${quote(type)}`); } recordCodec.marshal(value, syrupWriter); } diff --git a/packages/syrup/src/ocapn/operations.js b/packages/syrup/src/ocapn/operations.js index 4d1cd1b75c..a9d4f30839 100644 --- a/packages/syrup/src/ocapn/operations.js +++ b/packages/syrup/src/ocapn/operations.js @@ -15,13 +15,10 @@ const OpStartSession = new SyrupStructuredRecordCodecType( ['locationSignature', OCapNSignature], ]) - -const OCapNDeliverResolveMeDescs = { +const OCapNResolveMeDescCodec = new RecordUnionCodec({ DescImportObject, DescImportPromise, -} - -const OCapNResolveMeDescCodec = new RecordUnionCodec(OCapNDeliverResolveMeDescs); +}); const OpListen = new SyrupStructuredRecordCodecType( 'op:listen', [ @@ -101,6 +98,18 @@ const OpDeliver = new SyrupStructuredRecordCodecType( ['resolveMeDesc', OCapNResolveMeDescCodec], ]) +const OCapNPromiseRefCodec = new RecordUnionCodec({ + DescAnswer, + DescImportPromise, +}); + +const OpPick = new SyrupStructuredRecordCodecType( + 'op:pick', [ + ['promisePosition', OCapNPromiseRefCodec], + ['selectedValuePosition', 'integer'], + ['newAnswerPosition', 'integer'], +]) + const OpAbort = new SyrupStructuredRecordCodecType( 'op:abort', [ ['reason', 'string'], @@ -124,10 +133,11 @@ const OpGcSession = new SyrupStructuredRecordCodecType( export const OCapNMessageUnionCodec = new RecordUnionCodec({ OpStartSession, - OpListen, OpDeliverOnly, OpDeliver, + OpPick, OpAbort, + OpListen, OpGcExport, OpGcAnswer, OpGcSession, diff --git a/packages/syrup/test/_ocapn.js b/packages/syrup/test/_ocapn.js index 6c064998e4..7fe5999d9b 100644 --- a/packages/syrup/test/_ocapn.js +++ b/packages/syrup/test/_ocapn.js @@ -24,10 +24,14 @@ const makeExport = (position) => { return `<${sym('desc:export')}${int(position)}>`; } -const makeImport = (position) => { +const makeImportObj = (position) => { return `<${sym('desc:import-object')}${int(position)}>`; } +const makeImportPromise = (position) => { + return `<${sym('desc:import-promise')}${int(position)}>`; +} + const makeDescGive = (receiverKey, exporterLocation, session, gifterSide, giftId) => { return `<${sym('desc:handoff-give')}${receiverKey}${exporterLocation}${bts(session)}${gifterSide}${bts(giftId)}>`; } @@ -242,10 +246,39 @@ export const descriptorsTable = [ ]; export const operationsTable = [ - { syrup: '<8\'op:abort7"explode>', value: { type: 'op:abort', reason: 'explode' } }, + { + // ; CapTP signature + syrup: `<${sym('op:start-session')}${str('captp-v1')}${makePubKey('ecc', 'Ed25519', 'eddsa', '123')}${makeNode('tcp', '127.0.0.1', false)}${makeSig('eddsa', '1', '2')}>`, + value: { + type: 'op:start-session', + captpVersion: 'captp-v1', + sessionPublicKey: { + type: 'public-key', + scheme: 'ecc', + curve: 'Ed25519', + flags: 'eddsa', + q: strToUint8Array('123') + }, + location: { + type: 'ocapn-node', + transport: 'tcp', + address: strToUint8Array('127.0.0.1'), + hints: false + }, + locationSignature: { + type: 'sig-val', + scheme: 'eddsa', + r: strToUint8Array('1'), + s: strToUint8Array('2') + } + } + }, { // ['fulfill ]> - syrup: `<${sym('op:deliver-only')}${makeExport(1)}${list([sym('fulfill'), makeImport(1)])}>`, + syrup: `<${sym('op:deliver-only')}${makeExport(1)}${list([sym('fulfill'), makeImportObj(1)])}>`, value: { type: 'op:deliver-only', to: { @@ -266,7 +299,7 @@ export const operationsTable = [ // ['deposit-gift ; Symbol "deposit-gift" // 42 ; gift-id, a positive integer // ]> ; remote object being shared - syrup: `<${sym('op:deliver-only')}${makeExport(0)}${list([sym('deposit-gift'), int(42), makeImport(1)])}>`, + syrup: `<${sym('op:deliver-only')}${makeExport(0)}${list([sym('deposit-gift'), int(42), makeImportObj(1)])}>`, value: { type: 'op:deliver-only', to: { @@ -282,7 +315,7 @@ export const operationsTable = [ }, { // ['make-car-factory] 3 > - syrup: `<${sym('op:deliver')}${makeExport(5)}${list([sym('make-car-factory')])}${int(3)}${makeImport(15)}>`, + syrup: `<${sym('op:deliver')}${makeExport(5)}${list([sym('make-car-factory')])}${int(3)}${makeImportObj(15)}>`, value: { type: 'op:deliver', to: { @@ -299,7 +332,7 @@ export const operationsTable = [ }, { // ['beep] false > - syrup: `<${sym('op:deliver')}${makeExport(1)}${list([sym('beep')])}${bool(false)}${makeImport(2)}>`, + syrup: `<${sym('op:deliver')}${makeExport(1)}${list([sym('beep')])}${bool(false)}${makeImportObj(2)}>`, value: { type: 'op:deliver', to: { @@ -323,7 +356,7 @@ export const operationsTable = [ syrup: `<${sym('op:deliver')}${makeExport(0)}${list([ sym('fetch'), bts('swiss-number') - ])}${int(3)}${makeImport(5)}>`, + ])}${int(3)}${makeImportObj(5)}>`, value: { type: 'op:deliver', to: { type: 'desc:export', position: 0n }, @@ -356,7 +389,7 @@ export const operationsTable = [ ), makeSig('eddsa', '1', '2') ) - ])}${int(1)}${makeImport(3)}>`, + ])}${int(1)}${makeImportObj(3)}>`, value: { type: 'op:deliver', to: { type: 'desc:export', position: 0n }, @@ -408,6 +441,67 @@ export const operationsTable = [ type: 'desc:import-object', position: 3n } + }, + }, + { + // ; + // ; Positive Integer + // > ; Positive Integer + syrup: `<${sym('op:pick')}${makeImportPromise(1)}${int(2)}${int(3)}>`, + value: { + type: 'op:pick', + promisePosition: { + type: 'desc:import-promise', + position: 1n + }, + selectedValuePosition: 2n, + newAnswerPosition: 3n + } + }, + { + // ; reason: String + syrup: `<${sym('op:abort')}${str('explode')}>`, + value: { + type: 'op:abort', + reason: 'explode' + } + }, + { + // `, + value: { + type: 'op:listen', + to: { type: 'desc:export', position: 1n }, + resolveMeDesc: { type: 'desc:import-object', position: 2n }, + wantsPartial: false + } + }, + { + // ; positive integer + syrup: `<${sym('op:gc-export')}${int(1)}${int(2)}>`, + value: { + type: 'op:gc-export', + exportPosition: 1n, + wireDelta: 2n + } + }, + { + // ; answer-pos: positive integer + syrup: `<${sym('op:gc-answer')}${int(1)}>`, + value: { + type: 'op:gc-answer', + answerPosition: 1n + } + }, + { + // ; session: bytestring + syrup: `<${sym('op:gc-session')}${bts('session')}>`, + value: { + type: 'op:gc-session', + session: strToUint8Array('session') } } ]; diff --git a/packages/syrup/test/ocapn.test.js b/packages/syrup/test/ocapn.test.js index 8e7ef5a8f0..248215d6a2 100644 --- a/packages/syrup/test/ocapn.test.js +++ b/packages/syrup/test/ocapn.test.js @@ -57,5 +57,5 @@ test('error on unknown record type in passable', t => { const syrupReader = makeSyrupReader(syrupBytes, { name: 'unknown record type' }); t.throws(() => { codec.unmarshal(syrupReader); - }, { message: 'Unknown record type: unknown-record-type' }); + }, { message: 'Unexpected record type: "unknown-record-type"' }); }); From bacae8ac678f5862df9a6b60360ef144a4fbe4c2 Mon Sep 17 00:00:00 2001 From: kumavis Date: Sat, 5 Apr 2025 00:53:45 -1000 Subject: [PATCH 16/31] fix(ocapn): remove deprecated method --- packages/syrup/src/ocapn/operations.js | 6 ------ packages/syrup/test/_ocapn.js | 8 -------- 2 files changed, 14 deletions(-) diff --git a/packages/syrup/src/ocapn/operations.js b/packages/syrup/src/ocapn/operations.js index a9d4f30839..fcb22a8b67 100644 --- a/packages/syrup/src/ocapn/operations.js +++ b/packages/syrup/src/ocapn/operations.js @@ -126,11 +126,6 @@ const OpGcAnswer = new SyrupStructuredRecordCodecType( ['answerPosition', 'integer'], ]) -const OpGcSession = new SyrupStructuredRecordCodecType( - 'op:gc-session', [ - ['session', 'bytestring'], -]) - export const OCapNMessageUnionCodec = new RecordUnionCodec({ OpStartSession, OpDeliverOnly, @@ -140,7 +135,6 @@ export const OCapNMessageUnionCodec = new RecordUnionCodec({ OpListen, OpGcExport, OpGcAnswer, - OpGcSession, }); export const readOCapNMessage = (syrupReader) => { diff --git a/packages/syrup/test/_ocapn.js b/packages/syrup/test/_ocapn.js index 7fe5999d9b..8f34563a63 100644 --- a/packages/syrup/test/_ocapn.js +++ b/packages/syrup/test/_ocapn.js @@ -496,12 +496,4 @@ export const operationsTable = [ answerPosition: 1n } }, - { - // ; session: bytestring - syrup: `<${sym('op:gc-session')}${bts('session')}>`, - value: { - type: 'op:gc-session', - session: strToUint8Array('session') - } - } ]; From 1939cd7b1ad0aa268f6425abfd1776c9680b852b Mon Sep 17 00:00:00 2001 From: kumavis Date: Thu, 10 Apr 2025 11:55:21 -1000 Subject: [PATCH 17/31] chore(syrup): format and partial lint fix --- packages/syrup/src/buffer-reader.js | 11 +- packages/syrup/src/buffer-writer.js | 416 ++++++++++++------------ packages/syrup/src/codec.js | 41 ++- packages/syrup/src/decode.js | 93 ++++-- packages/syrup/src/encode.js | 27 +- packages/syrup/src/ocapn/components.js | 40 ++- packages/syrup/src/ocapn/descriptors.js | 78 +++-- packages/syrup/src/ocapn/operations.js | 72 ++-- packages/syrup/src/ocapn/passable.js | 54 ++- packages/syrup/src/symbol.js | 11 +- packages/syrup/test/_ocapn.js | 248 +++++++------- packages/syrup/test/_table.js | 7 +- packages/syrup/test/codec.test.js | 7 +- packages/syrup/test/decode.test.js | 5 +- packages/syrup/test/ocapn.test.js | 30 +- packages/syrup/test/reader.test.js | Bin 5337 -> 5354 bytes 16 files changed, 637 insertions(+), 503 deletions(-) diff --git a/packages/syrup/src/buffer-reader.js b/packages/syrup/src/buffer-reader.js index 0f6b8029f9..f013e733f4 100644 --- a/packages/syrup/src/buffer-reader.js +++ b/packages/syrup/src/buffer-reader.js @@ -49,7 +49,9 @@ export class BufferReader { fields.offset = bytes.byteOffset; // Temporary check until we can handle non-zero byteOffset if (fields.offset !== 0) { - throw Error('Cannot create BufferReader from Uint8Array with a non-zero byteOffset'); + throw Error( + 'Cannot create BufferReader from Uint8Array with a non-zero byteOffset', + ); } return reader; } @@ -263,7 +265,10 @@ export class BufferReader { bytesAt(index, size) { this.assertCanSeek(index + size); const fields = privateFieldsGet(this); - return fields.bytes.subarray(fields.offset + index, fields.offset + index + size); + return fields.bytes.subarray( + fields.offset + index, + fields.offset + index + size, + ); } /** @@ -331,4 +336,4 @@ export class BufferReader { } return index; } -} \ No newline at end of file +} diff --git a/packages/syrup/src/buffer-writer.js b/packages/syrup/src/buffer-writer.js index f715150352..75efd6e834 100644 --- a/packages/syrup/src/buffer-writer.js +++ b/packages/syrup/src/buffer-writer.js @@ -5,221 +5,221 @@ const textEncoder = new TextEncoder(); /** * @type {WeakMap} -*/ + * length: number, + * index: number, + * bytes: Uint8Array, + * data: DataView, + * capacity: number, + * }>} + */ const privateFields = new WeakMap(); /** -* @param {BufferWriter} self -*/ + * @param {BufferWriter} self + */ const getPrivateFields = self => { - const fields = privateFields.get(self); - if (!fields) { - throw Error('BufferWriter fields are not initialized'); - } - return fields; + const fields = privateFields.get(self); + if (!fields) { + throw Error('BufferWriter fields are not initialized'); + } + return fields; }; const assertNatNumber = n => { - if (Number.isSafeInteger(n) && /** @type {number} */ (n) >= 0) { - return; - } - throw TypeError(`must be a non-negative integer, got ${n}`); + if (Number.isSafeInteger(n) && /** @type {number} */ (n) >= 0) { + return; + } + throw TypeError(`must be a non-negative integer, got ${n}`); }; export class BufferWriter { - /** - * @returns {number} - */ - get length() { - return getPrivateFields(this).length; - } - - /** - * @returns {number} - */ - get index() { - return getPrivateFields(this).index; - } - - /** - * @param {number} index - */ - set index(index) { - this.seek(index); - } - - /** - * @param {number=} capacity - */ - constructor(capacity = 16) { - const bytes = new Uint8Array(capacity); - const data = new DataView(bytes.buffer); - privateFields.set(this, { - bytes, - data, - index: 0, - length: 0, - capacity, - }); - } - - /** - * @param {number} required - */ - ensureCanSeek(required) { - assertNatNumber(required); - const fields = getPrivateFields(this); - let capacity = fields.capacity; - if (capacity >= required) { - return; - } - while (capacity < required) { - capacity *= 2; - } - const bytes = new Uint8Array(capacity); - const data = new DataView(bytes.buffer); - bytes.set(fields.bytes.subarray(0, fields.length)); - fields.bytes = bytes; - fields.data = data; - fields.capacity = capacity; - } - - /** - * @param {number} index - */ - seek(index) { - const fields = getPrivateFields(this); - this.ensureCanSeek(index); - fields.index = index; - fields.length = Math.max(fields.index, fields.length); - } - - /** - * @param {number} size - */ - ensureCanWrite(size) { - assertNatNumber(size); - const fields = getPrivateFields(this); - this.ensureCanSeek(fields.index + size); - } - - /** - * @param {Uint8Array} bytes - */ - write(bytes) { - const fields = getPrivateFields(this); - this.ensureCanWrite(bytes.byteLength); - fields.bytes.set(bytes, fields.index); - fields.index += bytes.byteLength; - fields.length = Math.max(fields.index, fields.length); - } - - /** - * @param {number} byte - */ - writeByte(byte) { - assertNatNumber(byte); - if (byte > 0xFF) { - throw RangeError(`byte must be in range 0..255, got ${byte}`); - } - this.writeUint8(byte); - } - - /** - * @param {number} start - * @param {number} end - */ - writeCopy(start, end) { - assertNatNumber(start); - assertNatNumber(end); - const fields = getPrivateFields(this); - const size = end - start; - this.ensureCanWrite(size); - fields.bytes.copyWithin(fields.index, start, end); - fields.index += size; - fields.length = Math.max(fields.index, fields.length); - } - - /** - * @param {number} value - */ - writeUint8(value) { - const fields = getPrivateFields(this); - this.ensureCanWrite(1); - fields.data.setUint8(fields.index, value); - fields.index += 1; - fields.length = Math.max(fields.index, fields.length); - } - - /** - * @param {number} value - * @param {boolean=} littleEndian - */ - writeUint16(value, littleEndian) { - const fields = getPrivateFields(this); - this.ensureCanWrite(2); - const index = fields.index; - fields.data.setUint16(index, value, littleEndian); - fields.index += 2; - fields.length = Math.max(fields.index, fields.length); - } - - /** - * @param {number} value - * @param {boolean=} littleEndian - */ - writeUint32(value, littleEndian) { - const fields = getPrivateFields(this); - this.ensureCanWrite(4); - const index = fields.index; - fields.data.setUint32(index, value, littleEndian); - fields.index += 4; - fields.length = Math.max(fields.index, fields.length); - } - - /** - * @param {number} value - * @param {boolean=} littleEndian - */ - writeFloat64(value, littleEndian) { - const fields = getPrivateFields(this); - this.ensureCanWrite(8); - const index = fields.index; - fields.data.setFloat64(index, value, littleEndian); - fields.index += 8; - fields.length = Math.max(fields.index, fields.length); - } - - /** - * @param {string} string - */ - writeString(string) { - const bytes = textEncoder.encode(string); - this.write(bytes); - } - - /** - * @param {number=} begin - * @param {number=} end - * @returns {Uint8Array} - */ - subarray(begin, end) { - const fields = getPrivateFields(this); - return fields.bytes.subarray(0, fields.length).subarray(begin, end); - } - - /** - * @param {number=} begin - * @param {number=} end - * @returns {Uint8Array} - */ - slice(begin, end) { - return this.subarray(begin, end).slice(); - } -} \ No newline at end of file + /** + * @returns {number} + */ + get length() { + return getPrivateFields(this).length; + } + + /** + * @returns {number} + */ + get index() { + return getPrivateFields(this).index; + } + + /** + * @param {number} index + */ + set index(index) { + this.seek(index); + } + + /** + * @param {number=} capacity + */ + constructor(capacity = 16) { + const bytes = new Uint8Array(capacity); + const data = new DataView(bytes.buffer); + privateFields.set(this, { + bytes, + data, + index: 0, + length: 0, + capacity, + }); + } + + /** + * @param {number} required + */ + ensureCanSeek(required) { + assertNatNumber(required); + const fields = getPrivateFields(this); + let capacity = fields.capacity; + if (capacity >= required) { + return; + } + while (capacity < required) { + capacity *= 2; + } + const bytes = new Uint8Array(capacity); + const data = new DataView(bytes.buffer); + bytes.set(fields.bytes.subarray(0, fields.length)); + fields.bytes = bytes; + fields.data = data; + fields.capacity = capacity; + } + + /** + * @param {number} index + */ + seek(index) { + const fields = getPrivateFields(this); + this.ensureCanSeek(index); + fields.index = index; + fields.length = Math.max(fields.index, fields.length); + } + + /** + * @param {number} size + */ + ensureCanWrite(size) { + assertNatNumber(size); + const fields = getPrivateFields(this); + this.ensureCanSeek(fields.index + size); + } + + /** + * @param {Uint8Array} bytes + */ + write(bytes) { + const fields = getPrivateFields(this); + this.ensureCanWrite(bytes.byteLength); + fields.bytes.set(bytes, fields.index); + fields.index += bytes.byteLength; + fields.length = Math.max(fields.index, fields.length); + } + + /** + * @param {number} byte + */ + writeByte(byte) { + assertNatNumber(byte); + if (byte > 0xff) { + throw RangeError(`byte must be in range 0..255, got ${byte}`); + } + this.writeUint8(byte); + } + + /** + * @param {number} start + * @param {number} end + */ + writeCopy(start, end) { + assertNatNumber(start); + assertNatNumber(end); + const fields = getPrivateFields(this); + const size = end - start; + this.ensureCanWrite(size); + fields.bytes.copyWithin(fields.index, start, end); + fields.index += size; + fields.length = Math.max(fields.index, fields.length); + } + + /** + * @param {number} value + */ + writeUint8(value) { + const fields = getPrivateFields(this); + this.ensureCanWrite(1); + fields.data.setUint8(fields.index, value); + fields.index += 1; + fields.length = Math.max(fields.index, fields.length); + } + + /** + * @param {number} value + * @param {boolean=} littleEndian + */ + writeUint16(value, littleEndian) { + const fields = getPrivateFields(this); + this.ensureCanWrite(2); + const index = fields.index; + fields.data.setUint16(index, value, littleEndian); + fields.index += 2; + fields.length = Math.max(fields.index, fields.length); + } + + /** + * @param {number} value + * @param {boolean=} littleEndian + */ + writeUint32(value, littleEndian) { + const fields = getPrivateFields(this); + this.ensureCanWrite(4); + const index = fields.index; + fields.data.setUint32(index, value, littleEndian); + fields.index += 4; + fields.length = Math.max(fields.index, fields.length); + } + + /** + * @param {number} value + * @param {boolean=} littleEndian + */ + writeFloat64(value, littleEndian) { + const fields = getPrivateFields(this); + this.ensureCanWrite(8); + const index = fields.index; + fields.data.setFloat64(index, value, littleEndian); + fields.index += 8; + fields.length = Math.max(fields.index, fields.length); + } + + /** + * @param {string} string + */ + writeString(string) { + const bytes = textEncoder.encode(string); + this.write(bytes); + } + + /** + * @param {number=} begin + * @param {number=} end + * @returns {Uint8Array} + */ + subarray(begin, end) { + const fields = getPrivateFields(this); + return fields.bytes.subarray(0, fields.length).subarray(begin, end); + } + + /** + * @param {number=} begin + * @param {number=} end + * @returns {Uint8Array} + */ + slice(begin, end) { + return this.subarray(begin, end).slice(); + } +} diff --git a/packages/syrup/src/codec.js b/packages/syrup/src/codec.js index 5c3c4691f3..f7dc31327f 100644 --- a/packages/syrup/src/codec.js +++ b/packages/syrup/src/codec.js @@ -8,6 +8,7 @@ export class SyrupCodec { unmarshal(syrupReader) { throw new Error('SyrupCodec: unmarshal must be implemented'); } + /** * @param {any} value * @param {import('./encode.js').SyrupWriter} syrupWriter @@ -24,17 +25,19 @@ export class SimpleSyrupCodecType extends SyrupCodec { * @param {function(any, import('./encode.js').SyrupWriter): void} options.marshal * @param {function(import('./decode.js').SyrupReader): any} options.unmarshal */ - constructor ({ marshal, unmarshal }) { + constructor({ marshal, unmarshal }) { super(); this.marshal = marshal; this.unmarshal = unmarshal; } + /** * @param {import('./decode.js').SyrupReader} syrupReader */ unmarshal(syrupReader) { this.unmarshal(syrupReader); } + /** * @param {any} value * @param {import('./encode.js').SyrupWriter} syrupWriter @@ -46,37 +49,37 @@ export class SimpleSyrupCodecType extends SyrupCodec { export const SyrupSymbolCodec = new SimpleSyrupCodecType({ marshal: (value, syrupWriter) => syrupWriter.writeSymbol(value), - unmarshal: (syrupReader) => syrupReader.readSymbolAsString(), + unmarshal: syrupReader => syrupReader.readSymbolAsString(), }); export const SyrupStringCodec = new SimpleSyrupCodecType({ marshal: (value, syrupWriter) => syrupWriter.writeString(value), - unmarshal: (syrupReader) => syrupReader.readString(), + unmarshal: syrupReader => syrupReader.readString(), }); export const SyrupBytestringCodec = new SimpleSyrupCodecType({ marshal: (value, syrupWriter) => syrupWriter.writeBytestring(value), - unmarshal: (syrupReader) => syrupReader.readBytestring(), + unmarshal: syrupReader => syrupReader.readBytestring(), }); export const SyrupBooleanCodec = new SimpleSyrupCodecType({ marshal: (value, syrupWriter) => syrupWriter.writeBoolean(value), - unmarshal: (syrupReader) => syrupReader.readBoolean(), + unmarshal: syrupReader => syrupReader.readBoolean(), }); export const SyrupIntegerCodec = new SimpleSyrupCodecType({ marshal: (value, syrupWriter) => syrupWriter.writeInteger(value), - unmarshal: (syrupReader) => syrupReader.readInteger(), + unmarshal: syrupReader => syrupReader.readInteger(), }); export const SyrupDoubleCodec = new SimpleSyrupCodecType({ marshal: (value, syrupWriter) => syrupWriter.writeDouble(value), - unmarshal: (syrupReader) => syrupReader.readFloat64(), + unmarshal: syrupReader => syrupReader.readFloat64(), }); export const SyrupAnyCodec = new SimpleSyrupCodecType({ marshal: (value, syrupWriter) => syrupWriter.writeAny(value), - unmarshal: (syrupReader) => syrupReader.readAny(), + unmarshal: syrupReader => syrupReader.readAny(), }); export class SyrupRecordCodecType extends SyrupCodec { @@ -87,6 +90,7 @@ export class SyrupRecordCodecType extends SyrupCodec { super(); this.label = label; } + /** * @param {import('./decode.js').SyrupReader} syrupReader */ @@ -100,12 +104,14 @@ export class SyrupRecordCodecType extends SyrupCodec { syrupReader.exitRecord(); return result; } + /** * @param {import('./decode.js').SyrupReader} syrupReader */ unmarshalBody(syrupReader) { throw Error('SyrupRecordCodecType: unmarshalBody must be implemented'); } + /** * @param {any} value * @param {import('./encode.js').SyrupWriter} syrupWriter @@ -116,6 +122,7 @@ export class SyrupRecordCodecType extends SyrupCodec { this.marshalBody(value, syrupWriter); syrupWriter.exitRecord(); } + /** * @param {any} value * @param {import('./encode.js').SyrupWriter} syrupWriter @@ -135,10 +142,13 @@ export class SyrupStructuredRecordCodecType extends SyrupRecordCodecType { this.definition = definition; for (const [fieldName] of definition) { if (fieldName === 'type') { - throw new Error('SyrupRecordCodec: The "type" field is reserved for internal use.'); + throw new Error( + 'SyrupRecordCodec: The "type" field is reserved for internal use.', + ); } } } + /** * @param {import('./decode.js').SyrupReader} syrupReader */ @@ -159,6 +169,7 @@ export class SyrupStructuredRecordCodecType extends SyrupRecordCodecType { result.type = this.label; return result; } + /** * @param {any} value * @param {import('./encode.js').SyrupWriter} syrupWriter @@ -206,13 +217,15 @@ export class RecordUnionCodec extends SyrupCodec { throw Error(`Duplicate record type: ${recordCodec.label}`); } labelSet.add(recordCodec.label); - return [recordCodec.label, recordCodec] - }) + return [recordCodec.label, recordCodec]; + }), ); } + supports(label) { return this.recordTable[label] !== undefined; } + unmarshal(syrupReader) { syrupReader.enterRecord(); const label = syrupReader.readSymbolAsString(); @@ -224,6 +237,7 @@ export class RecordUnionCodec extends SyrupCodec { syrupReader.exitRecord(); return result; } + marshal(value, syrupWriter) { const { type } = value; const recordCodec = this.recordTable[type]; @@ -257,18 +271,19 @@ export class CustomUnionCodecType extends SyrupCodec { * @param {function(import('./decode.js').SyrupReader): SyrupCodec} options.selectCodecForUnmarshal * @param {function(any): SyrupCodec} options.selectCodecForMarshal */ - constructor ({ selectCodecForUnmarshal, selectCodecForMarshal }) { + constructor({ selectCodecForUnmarshal, selectCodecForMarshal }) { super(); this.selectCodecForUnmarshal = selectCodecForUnmarshal; this.selectCodecForMarshal = selectCodecForMarshal; } + unmarshal(syrupReader) { const codec = this.selectCodecForUnmarshal(syrupReader); return codec.unmarshal(syrupReader); } + marshal(value, syrupWriter) { const codec = this.selectCodecForMarshal(value); codec.marshal(value, syrupWriter); } } - diff --git a/packages/syrup/src/decode.js b/packages/syrup/src/decode.js index a8fbc775ff..2864d3070a 100644 --- a/packages/syrup/src/decode.js +++ b/packages/syrup/src/decode.js @@ -29,8 +29,8 @@ const textDecoder = new TextDecoder(); const { defineProperty, freeze } = Object; -const quote = (o) => JSON.stringify(o); -const toChar = (code) => String.fromCharCode(code); +const quote = o => JSON.stringify(o); +const toChar = code => String.fromCharCode(code); const canonicalNaN64 = freeze([0x7f, 0xf8, 0, 0, 0, 0, 0, 0]); const canonicalZero64 = freeze([0, 0, 0, 0, 0, 0, 0, 0]); @@ -48,7 +48,9 @@ function readBoolean(bufferReader, name) { if (cc === FALSE) { return false; } - throw Error(`Unexpected byte ${quote(toChar(cc))}, Syrup booleans must start with ${quote(toChar(TRUE))} or ${quote(toChar(FALSE))} at index ${bufferReader.index} of ${name}`); + throw Error( + `Unexpected byte ${quote(toChar(cc))}, Syrup booleans must start with ${quote(toChar(TRUE))} or ${quote(toChar(FALSE))} at index ${bufferReader.index} of ${name}`, + ); } // Structure types, no value provided @@ -98,7 +100,9 @@ function readTypeAndMaybeValue(bufferReader, name) { } // Number-prefixed types, read value if (cc < ZERO || cc > NINE) { - throw Error(`Unexpected character ${quote(toChar(cc))}, at index ${bufferReader.index} of ${name}`); + throw Error( + `Unexpected character ${quote(toChar(cc))}, at index ${bufferReader.index} of ${name}`, + ); } // Parse number-prefix let end; @@ -136,7 +140,9 @@ function readTypeAndMaybeValue(bufferReader, name) { const valueBytes = bufferReader.read(number); return { type: 'symbol', value: textDecoder.decode(valueBytes) }; } - throw Error(`Unexpected character ${quote(toChar(typeByte))}, at index ${bufferReader.index} of ${name}`); + throw Error( + `Unexpected character ${quote(toChar(typeByte))}, at index ${bufferReader.index} of ${name}`, + ); } /** @@ -208,7 +214,7 @@ function readFloat64Body(bufferReader, name) { // @ts-expect-error canonicalNaN64 is a frozen array, not a Uint8Array if (!bufferReader.matchAt(start, canonicalNaN64)) { throw Error(`Non-canonical NaN at index ${start} of Syrup ${name}`); - } + } } return value; @@ -221,7 +227,8 @@ function readFloat64Body(bufferReader, name) { function readFloat64(bufferReader, name) { const cc = bufferReader.readByte(); if (cc !== DOUBLE) { - throw Error(`Unexpected character ${quote(toChar(cc))}, at index ${bufferReader.index} of ${name}`, + throw Error( + `Unexpected character ${quote(toChar(cc))}, at index ${bufferReader.index} of ${name}`, ); } return readFloat64Body(bufferReader, name); @@ -239,7 +246,7 @@ function readListBody(bufferReader, name) { bufferReader.skip(1); return list; } - + list.push(readAny(bufferReader, name)); } } @@ -250,9 +257,11 @@ function readListBody(bufferReader, name) { * @returns {any[]} */ function readList(bufferReader, name) { - let cc = bufferReader.readByte(); + const cc = bufferReader.readByte(); if (cc !== LIST_START) { - throw Error(`Unexpected byte ${quote(toChar(cc))}, Syrup lists must start with ${quote(toChar(LIST_START))} at index ${bufferReader.index} of ${name}`); + throw Error( + `Unexpected byte ${quote(toChar(cc))}, Syrup lists must start with ${quote(toChar(LIST_START))} at index ${bufferReader.index} of ${name}`, + ); } return readListBody(bufferReader, name); } @@ -273,18 +282,19 @@ function readDictionaryKey(bufferReader, name) { } return { value, type, bytes }; } - throw Error(`Unexpected type ${quote(type)}, Syrup dictionary keys must be strings or symbols at index ${start} of ${name}`); + throw Error( + `Unexpected type ${quote(type)}, Syrup dictionary keys must be strings or symbols at index ${start} of ${name}`, + ); } - /** * @param {BufferReader} bufferReader * @param {string} name */ function readDictionaryBody(bufferReader, name) { const dict = {}; - let priorKey = undefined; - let priorKeyBytes = undefined; + let priorKey; + let priorKeyBytes; for (;;) { // Check for end of dictionary if (bufferReader.peekByte() === DICT_END) { @@ -293,7 +303,10 @@ function readDictionaryBody(bufferReader, name) { } // Read key const start = bufferReader.index; - const { value: newKey, bytes: newKeyBytes } = readDictionaryKey(bufferReader, name); + const { value: newKey, bytes: newKeyBytes } = readDictionaryKey( + bufferReader, + name, + ); // Validate strictly non-descending keys. if (priorKeyBytes !== undefined) { @@ -341,9 +354,11 @@ function readDictionaryBody(bufferReader, name) { */ function readDictionary(bufferReader, name) { const start = bufferReader.index; - let cc = bufferReader.readByte(); + const cc = bufferReader.readByte(); if (cc !== DICT_START) { - throw Error(`Unexpected character ${quote(toChar(cc))}, Syrup dictionaries must start with ${quote(toChar(DICT_START))} at index ${start} of ${name}`); + throw Error( + `Unexpected character ${quote(toChar(cc))}, Syrup dictionaries must start with ${quote(toChar(DICT_START))} at index ${start} of ${name}`, + ); } return readDictionaryBody(bufferReader, name); } @@ -356,7 +371,7 @@ function readDictionary(bufferReader, name) { export function peekTypeHint(bufferReader, name) { const cc = bufferReader.peekByte(); if (cc >= ZERO && cc <= NINE) { - return 'number-prefix' + return 'number-prefix'; } if (cc === TRUE || cc === FALSE) { return 'boolean'; @@ -407,7 +422,7 @@ function readAny(bufferReader, name) { if (type === 'symbol') { return SyrupSymbolFor(value); } - + return value; } @@ -441,15 +456,21 @@ export class SyrupReader { const start = this.bufferReader.index; const cc = this.bufferReader.readByte(); if (cc !== expectedByte) { - throw Error(`Unexpected character ${quote(toChar(cc))}, expected ${quote(toChar(expectedByte))} at index ${start} of ${this.name}`); + throw Error( + `Unexpected character ${quote(toChar(cc))}, expected ${quote(toChar(expectedByte))} at index ${start} of ${this.name}`, + ); } } + /** * @param {string} type */ #_pushStackEntry(type) { - this.state.stack.push(new SyrupReaderStackEntry(type, this.bufferReader.index)); + this.state.stack.push( + new SyrupReaderStackEntry(type, this.bufferReader.index), + ); } + /** * @param {string} expectedType */ @@ -457,10 +478,14 @@ export class SyrupReader { const start = this.bufferReader.index; const stackEntry = this.state.stack.pop(); if (!stackEntry) { - throw Error(`Attempted to exit ${expectedType} without entering it at index ${start} of ${this.name}`); + throw Error( + `Attempted to exit ${expectedType} without entering it at index ${start} of ${this.name}`, + ); } if (stackEntry.type !== expectedType) { - throw Error(`Attempted to exit ${expectedType} while in a ${stackEntry.type} at index ${start} of ${this.name}`); + throw Error( + `Attempted to exit ${expectedType} while in a ${stackEntry.type} at index ${start} of ${this.name}`, + ); } } @@ -468,10 +493,12 @@ export class SyrupReader { this.#_readAndAssertByte(RECORD_START); this.#_pushStackEntry('record'); } + exitRecord() { this.#_readAndAssertByte(RECORD_END); this.#_popStackEntry('record'); } + peekRecordEnd() { const cc = this.bufferReader.peekByte(); return cc === RECORD_END; @@ -481,10 +508,12 @@ export class SyrupReader { this.#_readAndAssertByte(DICT_START); this.#_pushStackEntry('dictionary'); } + exitDictionary() { this.#_readAndAssertByte(DICT_END); this.#_popStackEntry('dictionary'); } + peekDictionaryEnd() { const cc = this.bufferReader.peekByte(); return cc === DICT_END; @@ -494,10 +523,12 @@ export class SyrupReader { this.#_readAndAssertByte(LIST_START); this.#_pushStackEntry('list'); } + exitList() { this.#_readAndAssertByte(LIST_END); this.#_popStackEntry('list'); } + peekListEnd() { const cc = this.bufferReader.peekByte(); return cc === LIST_END; @@ -507,10 +538,12 @@ export class SyrupReader { this.#_readAndAssertByte(SET_START); this.#_pushStackEntry('set'); } + exitSet() { this.#_readAndAssertByte(SET_END); this.#_popStackEntry('set'); } + peekSetEnd() { const cc = this.bufferReader.peekByte(); return cc === SET_END; @@ -519,24 +552,31 @@ export class SyrupReader { readBoolean() { return readBoolean(this.bufferReader, this.name); } + readInteger() { return readInteger(this.bufferReader, this.name); } + readFloat64() { return readFloat64(this.bufferReader, this.name); } + readString() { return readString(this.bufferReader, this.name); } + readBytestring() { return readBytestring(this.bufferReader, this.name); } + readSymbolAsString() { return readSymbolAsString(this.bufferReader, this.name); } + readAny() { return readAny(this.bufferReader, this.name); } + /** * @param {'boolean' | 'integer' | 'float64' | 'string' | 'bytestring' | 'symbol'} type * @returns {any} @@ -559,6 +599,7 @@ export class SyrupReader { throw Error(`Unexpected type ${type}`); } } + peekTypeHint() { return peekTypeHint(this.bufferReader, this.name); } @@ -587,10 +628,12 @@ export function decodeSyrup(bytes, options = {}) { return readAny(bufferReader, name); } catch (err) { if (err.code === 'EOD') { - const err2 = Error(`Unexpected end of Syrup at index ${bufferReader.length} of ${name}`) + const err2 = Error( + `Unexpected end of Syrup at index ${bufferReader.length} of ${name}`, + ); err2.cause = err; throw err2; } throw err; } -} \ No newline at end of file +} diff --git a/packages/syrup/src/encode.js b/packages/syrup/src/encode.js index a721062834..21d91effb5 100644 --- a/packages/syrup/src/encode.js +++ b/packages/syrup/src/encode.js @@ -52,7 +52,7 @@ function writeString(bufferWriter, value) { */ function writeSymbol(bufferWriter, value) { const bytes = textEncoder.encode(value); - writeStringlike(bufferWriter, bytes, '\''); + writeStringlike(bufferWriter, bytes, "'"); } /** @@ -83,7 +83,9 @@ function writeDictionary(bufferWriter, record, path) { writeSymbol(bufferWriter, syrupSymbol); return; } - throw TypeError(`Dictionary keys must be strings or symbols, got ${typeof key} at ${path.join('/')}`); + throw TypeError( + `Dictionary keys must be strings or symbols, got ${typeof key} at ${path.join('/')}`, + ); }; // We need to sort the keys, so we write them to a scratch buffer first @@ -233,48 +235,51 @@ export class SyrupWriter { constructor(bufferWriter) { this.bufferWriter = bufferWriter; } + writeAny(value) { writeAny(this.bufferWriter, value, [], '/'); } + writeSymbol(value) { writeSymbol(this.bufferWriter, value); } + writeString(value) { writeString(this.bufferWriter, value); } + writeBytestring(value) { writeBytestring(this.bufferWriter, value); } + writeBoolean(value) { writeBoolean(this.bufferWriter, value); } + writeInteger(value) { writeInteger(this.bufferWriter, value); } + writeDouble(value) { writeDouble(this.bufferWriter, value); } - // writeList(value) { - // writeList(this.bufferWriter, value, []); - // } - // writeDictionary(value) { - // writeDictionary(this.bufferWriter, value, []); - // } - // writeRecord(value) { - // throw Error('writeRecord is not implemented'); - // } + enterRecord() { this.bufferWriter.writeByte(RECORD_START); } + exitRecord() { this.bufferWriter.writeByte(RECORD_END); } + enterList() { this.bufferWriter.writeByte(LIST_START); } + exitList() { this.bufferWriter.writeByte(LIST_END); } + /** * @param {'boolean' | 'integer' | 'float64' | 'string' | 'bytestring' | 'symbol'} type * @param {any} value diff --git a/packages/syrup/src/ocapn/components.js b/packages/syrup/src/ocapn/components.js index 2068a088fd..43a6ee72cc 100644 --- a/packages/syrup/src/ocapn/components.js +++ b/packages/syrup/src/ocapn/components.js @@ -1,4 +1,8 @@ -import { RecordUnionCodec, SyrupCodec, SyrupStructuredRecordCodecType } from "../codec.js"; +import { + RecordUnionCodec, + SyrupCodec, + SyrupStructuredRecordCodecType, +} from '../codec.js'; /* * OCapN Components are used in both OCapN Messages and Descriptors @@ -12,6 +16,7 @@ export class OCapNSignatureValueCodec extends SyrupCodec { super(); this.expectedLabel = expectedLabel; } + unmarshal(syrupReader) { const label = syrupReader.readSymbolAsString(); if (label !== this.expectedLabel) { @@ -20,6 +25,7 @@ export class OCapNSignatureValueCodec extends SyrupCodec { const value = syrupReader.readBytestring(); return value; } + marshal(value, syrupWriter) { syrupWriter.writeSymbol(this.expectedLabel); syrupWriter.writeBytestring(value); @@ -29,34 +35,32 @@ export class OCapNSignatureValueCodec extends SyrupCodec { const OCapNSignatureRValue = new OCapNSignatureValueCodec('r'); const OCapNSignatureSValue = new OCapNSignatureValueCodec('s'); - -export const OCapNSignature = new SyrupStructuredRecordCodecType( - 'sig-val', [ +export const OCapNSignature = new SyrupStructuredRecordCodecType('sig-val', [ ['scheme', 'symbol'], ['r', OCapNSignatureRValue], ['s', OCapNSignatureSValue], -]) +]); -export const OCapNNode = new SyrupStructuredRecordCodecType( - 'ocapn-node', [ +export const OCapNNode = new SyrupStructuredRecordCodecType('ocapn-node', [ ['transport', 'symbol'], ['address', 'bytestring'], ['hints', 'boolean'], -]) +]); export const OCapNSturdyRef = new SyrupStructuredRecordCodecType( - 'ocapn-sturdyref', [ - ['node', OCapNNode], - ['swissNum', 'string'], -]) + 'ocapn-sturdyref', + [ + ['node', OCapNNode], + ['swissNum', 'string'], + ], +); -export const OCapNPublicKey = new SyrupStructuredRecordCodecType( - 'public-key', [ +export const OCapNPublicKey = new SyrupStructuredRecordCodecType('public-key', [ ['scheme', 'symbol'], ['curve', 'symbol'], ['flags', 'symbol'], ['q', 'bytestring'], -]) +]); export const OCapNComponentUnionCodec = new RecordUnionCodec({ OCapNNode, @@ -65,11 +69,11 @@ export const OCapNComponentUnionCodec = new RecordUnionCodec({ OCapNSignature, }); -export const readOCapComponent = (syrupReader) => { +export const readOCapComponent = syrupReader => { return OCapNComponentUnionCodec.unmarshal(syrupReader); -} +}; export const writeOCapComponent = (component, syrupWriter) => { OCapNComponentUnionCodec.marshal(component, syrupWriter); return syrupWriter.bufferWriter.subarray(0, syrupWriter.bufferWriter.length); -} +}; diff --git a/packages/syrup/src/ocapn/descriptors.js b/packages/syrup/src/ocapn/descriptors.js index 0c9b7937ea..0a4170f100 100644 --- a/packages/syrup/src/ocapn/descriptors.js +++ b/packages/syrup/src/ocapn/descriptors.js @@ -7,53 +7,59 @@ import { OCapNNode, OCapNPublicKey, OCapNSignature } from './components.js'; */ export const DescImportObject = new SyrupStructuredRecordCodecType( - 'desc:import-object', [ - ['position', 'integer'], -]) + 'desc:import-object', + [['position', 'integer']], +); export const DescImportPromise = new SyrupStructuredRecordCodecType( - 'desc:import-promise', [ - ['position', 'integer'], -]) + 'desc:import-promise', + [['position', 'integer']], +); -export const DescExport = new SyrupStructuredRecordCodecType( - 'desc:export', [ +export const DescExport = new SyrupStructuredRecordCodecType('desc:export', [ ['position', 'integer'], -]) +]); -export const DescAnswer = new SyrupStructuredRecordCodecType( - 'desc:answer', [ +export const DescAnswer = new SyrupStructuredRecordCodecType('desc:answer', [ ['position', 'integer'], -]) +]); export const DescHandoffGive = new SyrupStructuredRecordCodecType( - 'desc:handoff-give', [ - ['receiverKey', OCapNPublicKey], - ['exporterLocation', OCapNNode], - ['session', 'bytestring'], - ['gifterSide', OCapNPublicKey], - ['giftId', 'bytestring'], -]) + 'desc:handoff-give', + [ + ['receiverKey', OCapNPublicKey], + ['exporterLocation', OCapNNode], + ['session', 'bytestring'], + ['gifterSide', OCapNPublicKey], + ['giftId', 'bytestring'], + ], +); export const DescSigGiveEnvelope = new SyrupStructuredRecordCodecType( - 'desc:sig-envelope', [ - ['object', DescHandoffGive], - ['signature', OCapNSignature], -]) + 'desc:sig-envelope', + [ + ['object', DescHandoffGive], + ['signature', OCapNSignature], + ], +); export const DescHandoffReceive = new SyrupStructuredRecordCodecType( - 'desc:handoff-receive', [ - ['receivingSession', 'bytestring'], - ['receivingSide', 'bytestring'], - ['handoffCount', 'integer'], - ['signedGive', DescSigGiveEnvelope], -]) + 'desc:handoff-receive', + [ + ['receivingSession', 'bytestring'], + ['receivingSide', 'bytestring'], + ['handoffCount', 'integer'], + ['signedGive', DescSigGiveEnvelope], + ], +); export const DescSigReceiveEnvelope = new SyrupStructuredRecordCodecType( - 'desc:sig-envelope', [ - ['object', DescHandoffReceive], - ['signature', OCapNSignature], -]) + 'desc:sig-envelope', + [ + ['object', DescHandoffReceive], + ['signature', OCapNSignature], + ], +); // Note: this may only be useful for testing export const OCapNDescriptorUnionCodec = new RecordUnionCodec({ @@ -71,11 +77,11 @@ export const OCapNDescriptorUnionCodec = new RecordUnionCodec({ DescHandoffReceive, }); -export const readOCapDescriptor = (syrupReader) => { +export const readOCapDescriptor = syrupReader => { return OCapNDescriptorUnionCodec.unmarshal(syrupReader); -} +}; export const writeOCapDescriptor = (descriptor, syrupWriter) => { OCapNDescriptorUnionCodec.marshal(descriptor, syrupWriter); return syrupWriter.bufferWriter.subarray(0, syrupWriter.bufferWriter.length); -} +}; diff --git a/packages/syrup/src/ocapn/operations.js b/packages/syrup/src/ocapn/operations.js index fcb22a8b67..36106e9e8e 100644 --- a/packages/syrup/src/ocapn/operations.js +++ b/packages/syrup/src/ocapn/operations.js @@ -1,52 +1,57 @@ -import { RecordUnionCodec, SyrupStructuredRecordCodecType, SimpleSyrupCodecType } from '../codec.js'; +import { + RecordUnionCodec, + SyrupStructuredRecordCodecType, + SimpleSyrupCodecType, +} from '../codec.js'; import { OCapNNode, OCapNPublicKey, OCapNSignature } from './components.js'; import { OCapNPassableUnionCodec } from './passable.js'; -import { DescImportObject, DescImportPromise, DescExport, DescAnswer } from './descriptors.js'; +import { + DescImportObject, + DescImportPromise, + DescExport, + DescAnswer, +} from './descriptors.js'; /* * These are OCapN Operations, they are messages that are sent between OCapN Nodes */ -const OpStartSession = new SyrupStructuredRecordCodecType( - 'op:start-session', [ +const OpStartSession = new SyrupStructuredRecordCodecType('op:start-session', [ ['captpVersion', 'string'], ['sessionPublicKey', OCapNPublicKey], ['location', OCapNNode], ['locationSignature', OCapNSignature], -]) +]); const OCapNResolveMeDescCodec = new RecordUnionCodec({ DescImportObject, DescImportPromise, }); -const OpListen = new SyrupStructuredRecordCodecType( - 'op:listen', [ +const OpListen = new SyrupStructuredRecordCodecType('op:listen', [ ['to', DescExport], ['resolveMeDesc', OCapNResolveMeDescCodec], ['wantsPartial', 'boolean'], -]) +]); const OCapNDeliverTargets = { DescExport, DescAnswer, -} +}; const OCapNDeliverTargetCodec = new RecordUnionCodec(OCapNDeliverTargets); // Used by the deliver and deliver-only operations // First arg is method name, rest are Passables const OpDeliverArgsCodec = new SimpleSyrupCodecType({ - unmarshal: (syrupReader) => { + unmarshal: syrupReader => { syrupReader.enterList(); const result = [ // method name syrupReader.readSymbolAsString(), ]; while (!syrupReader.peekListEnd()) { - result.push( - OCapNPassableUnionCodec.unmarshal(syrupReader) - ) + result.push(OCapNPassableUnionCodec.unmarshal(syrupReader)); } syrupReader.exitList(); return result; @@ -59,16 +64,15 @@ const OpDeliverArgsCodec = new SimpleSyrupCodecType({ } syrupWriter.exitList(); }, -}) +}); -const OpDeliverOnly = new SyrupStructuredRecordCodecType( - 'op:deliver-only', [ +const OpDeliverOnly = new SyrupStructuredRecordCodecType('op:deliver-only', [ ['to', OCapNDeliverTargetCodec], ['args', OpDeliverArgsCodec], -]) +]); const OpDeliverAnswerCodec = new SimpleSyrupCodecType({ - unmarshal: (syrupReader) => { + unmarshal: syrupReader => { const typeHint = syrupReader.peekTypeHint(); if (typeHint === 'number-prefix') { // should be an integer @@ -90,41 +94,36 @@ const OpDeliverAnswerCodec = new SimpleSyrupCodecType({ }, }); -const OpDeliver = new SyrupStructuredRecordCodecType( - 'op:deliver', [ +const OpDeliver = new SyrupStructuredRecordCodecType('op:deliver', [ ['to', OCapNDeliverTargetCodec], ['args', OpDeliverArgsCodec], ['answerPosition', OpDeliverAnswerCodec], ['resolveMeDesc', OCapNResolveMeDescCodec], -]) +]); const OCapNPromiseRefCodec = new RecordUnionCodec({ DescAnswer, DescImportPromise, }); -const OpPick = new SyrupStructuredRecordCodecType( - 'op:pick', [ +const OpPick = new SyrupStructuredRecordCodecType('op:pick', [ ['promisePosition', OCapNPromiseRefCodec], ['selectedValuePosition', 'integer'], ['newAnswerPosition', 'integer'], -]) +]); -const OpAbort = new SyrupStructuredRecordCodecType( - 'op:abort', [ +const OpAbort = new SyrupStructuredRecordCodecType('op:abort', [ ['reason', 'string'], -]) +]); -const OpGcExport = new SyrupStructuredRecordCodecType( - 'op:gc-export', [ +const OpGcExport = new SyrupStructuredRecordCodecType('op:gc-export', [ ['exportPosition', 'integer'], ['wireDelta', 'integer'], -]) +]); -const OpGcAnswer = new SyrupStructuredRecordCodecType( - 'op:gc-answer', [ +const OpGcAnswer = new SyrupStructuredRecordCodecType('op:gc-answer', [ ['answerPosition', 'integer'], -]) +]); export const OCapNMessageUnionCodec = new RecordUnionCodec({ OpStartSession, @@ -137,12 +136,11 @@ export const OCapNMessageUnionCodec = new RecordUnionCodec({ OpGcAnswer, }); -export const readOCapNMessage = (syrupReader) => { +export const readOCapNMessage = syrupReader => { return OCapNMessageUnionCodec.unmarshal(syrupReader); -} +}; export const writeOCapNMessage = (message, syrupWriter) => { OCapNMessageUnionCodec.marshal(message, syrupWriter); return syrupWriter.bufferWriter.subarray(0, syrupWriter.bufferWriter.length); -} - +}; diff --git a/packages/syrup/src/ocapn/passable.js b/packages/syrup/src/ocapn/passable.js index df89942207..b4f6e77276 100644 --- a/packages/syrup/src/ocapn/passable.js +++ b/packages/syrup/src/ocapn/passable.js @@ -1,5 +1,26 @@ -import { RecordUnionCodec, SimpleSyrupCodecType, SyrupBooleanCodec, SyrupIntegerCodec, SyrupDoubleCodec, SyrupSymbolCodec, SyrupStringCodec, SyrupBytestringCodec, SyrupListCodec, CustomRecordCodec, CustomUnionCodecType, SyrupAnyCodec, SyrupStructuredRecordCodecType } from '../codec.js'; -import { DescImportObject, DescImportPromise, DescExport, DescAnswer, DescHandoffGive, DescHandoffReceive } from './descriptors.js'; +import { + RecordUnionCodec, + SimpleSyrupCodecType, + SyrupBooleanCodec, + SyrupIntegerCodec, + SyrupDoubleCodec, + SyrupSymbolCodec, + SyrupStringCodec, + SyrupBytestringCodec, + SyrupListCodec, + CustomRecordCodec, + CustomUnionCodecType, + SyrupAnyCodec, + SyrupStructuredRecordCodecType, +} from '../codec.js'; +import { + DescImportObject, + DescImportPromise, + DescExport, + DescAnswer, + DescHandoffGive, + DescHandoffReceive, +} from './descriptors.js'; // OCapN Passable Atoms @@ -21,7 +42,6 @@ const NullCodec = new CustomRecordCodec('null', { }, }); - const AtomCodecs = { undefined: UndefinedCodec, null: NullCodec, @@ -33,7 +53,7 @@ const AtomCodecs = { symbol: SyrupSymbolCodec, // TODO: Pass Invariant Equality byteArray: SyrupBytestringCodec, -} +}; // OCapN Passable Containers @@ -57,50 +77,49 @@ const OCapNTaggedCodec = new CustomRecordCodec('desc:tagged', { [Symbol.for('passStyle')]: 'tagged', [Symbol.toStringTag]: tagName, value, - } + }; }, marshalBody(value, syrupWriter) { syrupWriter.writeSymbol(value.tagName); value.value.marshal(syrupWriter); }, -}) +}); const ContainerCodecs = { list: SyrupListCodec, struct: OCapNStructCodec, tagged: OCapNTaggedCodec, -} +}; // OCapN Reference (Capability) const OCapNTargetCodec = new RecordUnionCodec({ DescExport, DescImportObject, -}) +}); const OCapNPromiseCodec = new RecordUnionCodec({ DescImportPromise, DescAnswer, -}) +}); const OCapNReferenceCodecs = { OCapNTargetCodec, OCapNPromiseCodec, -} +}; // OCapN Error -const OCapNErrorCodec = new SyrupStructuredRecordCodecType( - 'desc:error', [ +const OCapNErrorCodec = new SyrupStructuredRecordCodecType('desc:error', [ ['message', 'string'], -]) +]); const OCapNPassableCodecs = { ...AtomCodecs, ...ContainerCodecs, ...OCapNReferenceCodecs, ...OCapNErrorCodec, -} +}; // all record based passables const OCapNPassableRecordUnionCodec = new RecordUnionCodec({ @@ -129,7 +148,7 @@ export const OCapNPassableUnionCodec = new CustomUnionCodecType({ case 'number-prefix': // can be string, bytestring, symbol, integer // We'll return the any codec in place of those - return SyrupAnyCodec + return SyrupAnyCodec; case 'list': return ContainerCodecs.list; case 'record': @@ -173,7 +192,10 @@ export const OCapNPassableUnionCodec = new CustomUnionCodecType({ if (value[Symbol.for('passStyle')] === 'tagged') { return ContainerCodecs.tagged; } - if (value.type !== undefined && OCapNPassableRecordUnionCodec.supports(value.type)) { + if ( + value.type !== undefined && + OCapNPassableRecordUnionCodec.supports(value.type) + ) { return OCapNPassableRecordUnionCodec; } // TODO: need to distinguish OCapNReferenceCodecs and OCapNErrorCodec diff --git a/packages/syrup/src/symbol.js b/packages/syrup/src/symbol.js index 8265f092db..c385542183 100644 --- a/packages/syrup/src/symbol.js +++ b/packages/syrup/src/symbol.js @@ -2,19 +2,22 @@ export const SYRUP_SYMBOL_PREFIX = 'syrup:'; // To be used as keys, syrup symbols must be javascript symbols. // To avoid an otherwise meaningful symbol name, we prefix it with 'syrup:'. -export const SyrupSymbolFor = (name) => Symbol.for(`${SYRUP_SYMBOL_PREFIX}${name}`); +export const SyrupSymbolFor = name => + Symbol.for(`${SYRUP_SYMBOL_PREFIX}${name}`); /** * @param {symbol} symbol * @returns {string} */ -export const getSyrupSymbolName = (symbol) => { +export const getSyrupSymbolName = symbol => { const description = symbol.description; if (!description) { throw TypeError(`Symbol ${String(symbol)} has no description`); } if (!description.startsWith(SYRUP_SYMBOL_PREFIX)) { - throw TypeError(`Symbol ${String(symbol)} has a description that does not start with "${SYRUP_SYMBOL_PREFIX}", got "${description}"`); + throw TypeError( + `Symbol ${String(symbol)} has a description that does not start with "${SYRUP_SYMBOL_PREFIX}", got "${description}"`, + ); } return description.slice(SYRUP_SYMBOL_PREFIX.length); -} +}; diff --git a/packages/syrup/test/_ocapn.js b/packages/syrup/test/_ocapn.js index 8f34563a63..71efa71f4f 100644 --- a/packages/syrup/test/_ocapn.js +++ b/packages/syrup/test/_ocapn.js @@ -1,53 +1,65 @@ -const sym = (s) => `${s.length}'${s}`; -const str = (s) => `${s.length}"${s}`; -const bts = (s) => `${s.length}:${s}`; -const bool = (b) => b ? 't' : 'f'; -const int = (i) => `${Math.floor(Math.abs(i))}${i < 0 ? '-' : '+'}`; -const list = (items) => `[${items.join('')}]`; +const sym = s => `${s.length}'${s}`; +const str = s => `${s.length}"${s}`; +const bts = s => `${s.length}:${s}`; +const bool = b => (b ? 't' : 'f'); +const int = i => `${Math.floor(Math.abs(i))}${i < 0 ? '-' : '+'}`; +const list = items => `[${items.join('')}]`; const makeNode = (transport, address, hints) => { return `<10'ocapn-node${sym(transport)}${bts(address)}${bool(hints)}>`; -} +}; const makePubKey = (scheme, curve, flags, q) => { return `<${sym('public-key')}${sym(scheme)}${sym(curve)}${sym(flags)}${bts(q)}>`; -} +}; const makeSigComp = (label, value) => { return `${sym(label)}${bts(value)}`; -} +}; const makeSig = (scheme, r, s) => { return `<${sym('sig-val')}${sym(scheme)}${makeSigComp('r', r)}${makeSigComp('s', s)}>`; -} +}; -const makeExport = (position) => { +const makeExport = position => { return `<${sym('desc:export')}${int(position)}>`; -} +}; -const makeImportObj = (position) => { +const makeImportObj = position => { return `<${sym('desc:import-object')}${int(position)}>`; -} +}; -const makeImportPromise = (position) => { +const makeImportPromise = position => { return `<${sym('desc:import-promise')}${int(position)}>`; -} +}; -const makeDescGive = (receiverKey, exporterLocation, session, gifterSide, giftId) => { +const makeDescGive = ( + receiverKey, + exporterLocation, + session, + gifterSide, + giftId, +) => { return `<${sym('desc:handoff-give')}${receiverKey}${exporterLocation}${bts(session)}${gifterSide}${bts(giftId)}>`; -} +}; const makeSigEnvelope = (object, signature) => { return `<${sym('desc:sig-envelope')}${object}${signature}>`; -} +}; -const makeHandoffReceive = (recieverSession, recieverSide, handoffCount, descGive, signature) => { +const makeHandoffReceive = ( + recieverSession, + recieverSide, + handoffCount, + descGive, + signature, +) => { const signedGiveEnvelope = makeSigEnvelope(descGive, signature); return `<${sym('desc:handoff-receive')}${bts(recieverSession)}${bts(recieverSide)}${int(handoffCount)}${signedGiveEnvelope}>`; -} +}; -const strToUint8Array = (str) => { +const strToUint8Array = str => { return new Uint8Array(str.split('').map(c => c.charCodeAt(0))); -} +}; // I made up these syrup values by hand, they may be wrong, sorry. // Would like external test data for this. @@ -59,8 +71,8 @@ export const componentsTable = [ type: 'sig-val', scheme: 'eddsa', r: new Uint8Array([0x31]), - s: new Uint8Array([0x32]) - } + s: new Uint8Array([0x32]), + }, }, { syrup: `<10'ocapn-node3'tcp1:0f>`, @@ -68,8 +80,8 @@ export const componentsTable = [ type: 'ocapn-node', transport: 'tcp', address: new Uint8Array([0x30]), - hints: false - } + hints: false, + }, }, { syrup: `<15'ocapn-sturdyref${makeNode('tcp', '0', false)}${str('1')}>`, @@ -79,10 +91,10 @@ export const componentsTable = [ type: 'ocapn-node', transport: 'tcp', address: new Uint8Array([0x30]), - hints: false + hints: false, }, - swissNum: '1' - } + swissNum: '1', + }, }, { syrup: makePubKey('ecc', 'Ed25519', 'eddsa', '1'), @@ -91,8 +103,8 @@ export const componentsTable = [ scheme: 'ecc', curve: 'Ed25519', flags: 'eddsa', - q: strToUint8Array('1') - } + q: strToUint8Array('1'), + }, }, ]; @@ -101,40 +113,48 @@ export const descriptorsTable = [ syrup: `<18'desc:import-object123+>`, value: { type: 'desc:import-object', - position: 123n - } + position: 123n, + }, }, { syrup: `<19'desc:import-promise456+>`, value: { type: 'desc:import-promise', - position: 456n - } + position: 456n, + }, }, { syrup: `<11'desc:export123+>`, value: { type: 'desc:export', - position: 123n - } + position: 123n, + }, }, { syrup: `<11'desc:answer456+>`, value: { type: 'desc:answer', - position: 456n - } + position: 456n, + }, }, { syrup: `<${sym('desc:handoff-give')}${makePubKey('ecc', 'Ed25519', 'eddsa', '1')}${makeNode('tcp', '127.0.0.1', false)}${bts('123')}${makePubKey('ecc', 'Ed25519', 'eddsa', '2')}${bts('456')}>`, value: { type: 'desc:handoff-give', - receiverKey: { type: 'public-key', scheme: 'ecc', curve: 'Ed25519', flags: 'eddsa', q: new Uint8Array([0x31]) }, + receiverKey: { + type: 'public-key', + scheme: 'ecc', + curve: 'Ed25519', + flags: 'eddsa', + q: new Uint8Array([0x31]), + }, exporterLocation: { type: 'ocapn-node', transport: 'tcp', - address: new Uint8Array([0x31, 0x32, 0x37, 0x2e, 0x30, 0x2e, 0x30, 0x2e, 0x31]), - hints: false + address: new Uint8Array([ + 0x31, 0x32, 0x37, 0x2e, 0x30, 0x2e, 0x30, 0x2e, 0x31, + ]), + hints: false, }, session: new Uint8Array([0x31, 0x32, 0x33]), gifterSide: { @@ -142,10 +162,10 @@ export const descriptorsTable = [ scheme: 'ecc', curve: 'Ed25519', flags: 'eddsa', - q: new Uint8Array([0x32]) + q: new Uint8Array([0x32]), }, - giftId: new Uint8Array([0x34, 0x35, 0x36]) - } + giftId: new Uint8Array([0x34, 0x35, 0x36]), + }, }, { syrup: `<${sym('desc:sig-envelope')}${makeDescGive( @@ -153,7 +173,7 @@ export const descriptorsTable = [ makeNode('tcp', '127.0.0.1', false), '123', makePubKey('ed25519', 'ed25519', 'ed25519', '123'), - '123' + '123', )}${makeSig('eddsa', '1', '2')}>`, value: { type: 'desc:sig-envelope', @@ -164,13 +184,13 @@ export const descriptorsTable = [ scheme: 'ed25519', curve: 'ed25519', flags: 'ed25519', - q: strToUint8Array('123') + q: strToUint8Array('123'), }, exporterLocation: { type: 'ocapn-node', transport: 'tcp', address: strToUint8Array('127.0.0.1'), - hints: false + hints: false, }, session: strToUint8Array('123'), gifterSide: { @@ -178,17 +198,17 @@ export const descriptorsTable = [ scheme: 'ed25519', curve: 'ed25519', flags: 'ed25519', - q: strToUint8Array('123') + q: strToUint8Array('123'), }, - giftId: strToUint8Array('123') + giftId: strToUint8Array('123'), }, signature: { type: 'sig-val', scheme: 'eddsa', r: strToUint8Array('1'), - s: strToUint8Array('2') - } - } + s: strToUint8Array('2'), + }, + }, }, // handoff receive { @@ -198,9 +218,9 @@ export const descriptorsTable = [ makeNode('tcp', '456', false), '789', makePubKey('ecc', 'Ed25519', 'eddsa', 'abc'), - 'def' + 'def', ), - makeSig('eddsa', '1', '2') + makeSig('eddsa', '1', '2'), )}>`, value: { type: 'desc:handoff-receive', @@ -216,13 +236,13 @@ export const descriptorsTable = [ scheme: 'ecc', curve: 'Ed25519', flags: 'eddsa', - q: strToUint8Array('123') + q: strToUint8Array('123'), }, exporterLocation: { type: 'ocapn-node', transport: 'tcp', address: strToUint8Array('456'), - hints: false + hints: false, }, session: strToUint8Array('789'), gifterSide: { @@ -230,19 +250,19 @@ export const descriptorsTable = [ scheme: 'ecc', curve: 'Ed25519', flags: 'eddsa', - q: strToUint8Array('abc') + q: strToUint8Array('abc'), }, - giftId: strToUint8Array('def') + giftId: strToUint8Array('def'), }, signature: { type: 'sig-val', scheme: 'eddsa', r: strToUint8Array('1'), - s: strToUint8Array('2') - } - } - } - } + s: strToUint8Array('2'), + }, + }, + }, + }, ]; export const operationsTable = [ @@ -260,21 +280,21 @@ export const operationsTable = [ scheme: 'ecc', curve: 'Ed25519', flags: 'eddsa', - q: strToUint8Array('123') + q: strToUint8Array('123'), }, location: { type: 'ocapn-node', transport: 'tcp', address: strToUint8Array('127.0.0.1'), - hints: false + hints: false, }, locationSignature: { type: 'sig-val', scheme: 'eddsa', r: strToUint8Array('1'), - s: strToUint8Array('2') - } - } + s: strToUint8Array('2'), + }, + }, }, { // ['fulfill ]> @@ -283,16 +303,16 @@ export const operationsTable = [ type: 'op:deliver-only', to: { type: 'desc:export', - position: 1n + position: 1n, }, args: [ 'fulfill', { type: 'desc:import-object', - position: 1n - } - ] - } + position: 1n, + }, + ], + }, }, { // ; Remote bootstrap object @@ -304,14 +324,10 @@ export const operationsTable = [ type: 'op:deliver-only', to: { type: 'desc:export', - position: 0n + position: 0n, }, - args: [ - 'deposit-gift', - 42n, - { type: 'desc:import-object', position: 1n } - ] - } + args: ['deposit-gift', 42n, { type: 'desc:import-object', position: 1n }], + }, }, { // ['make-car-factory] 3 > @@ -320,15 +336,15 @@ export const operationsTable = [ type: 'op:deliver', to: { type: 'desc:export', - position: 5n + position: 5n, }, args: ['make-car-factory'], answerPosition: 3n, resolveMeDesc: { type: 'desc:import-object', - position: 15n + position: 15n, }, - } + }, }, { // ['beep] false > @@ -337,15 +353,15 @@ export const operationsTable = [ type: 'op:deliver', to: { type: 'desc:export', - position: 1n + position: 1n, }, args: ['beep'], answerPosition: false, resolveMeDesc: { type: 'desc:import-object', - position: 2n - } - } + position: 2n, + }, + }, }, { // ; Remote bootstrap object @@ -355,7 +371,7 @@ export const operationsTable = [ // > ; object exported by us at position 5 should provide the answer syrup: `<${sym('op:deliver')}${makeExport(0)}${list([ sym('fetch'), - bts('swiss-number') + bts('swiss-number'), ])}${int(3)}${makeImportObj(5)}>`, value: { type: 'op:deliver', @@ -364,9 +380,9 @@ export const operationsTable = [ answerPosition: 3n, resolveMeDesc: { type: 'desc:import-object', - position: 5n - } - } + position: 5n, + }, + }, }, { // ; Remote bootstrap object @@ -385,10 +401,10 @@ export const operationsTable = [ makeNode('tcp', '456', false), '789', makePubKey('ecc', 'Ed25519', 'eddsa', 'abc'), - 'def' + 'def', ), - makeSig('eddsa', '1', '2') - ) + makeSig('eddsa', '1', '2'), + ), ])}${int(1)}${makeImportObj(3)}>`, value: { type: 'op:deliver', @@ -409,13 +425,13 @@ export const operationsTable = [ scheme: 'ecc', curve: 'Ed25519', flags: 'eddsa', - q: strToUint8Array('123') + q: strToUint8Array('123'), }, exporterLocation: { type: 'ocapn-node', transport: 'tcp', address: strToUint8Array('456'), - hints: false + hints: false, }, session: strToUint8Array('789'), gifterSide: { @@ -423,24 +439,24 @@ export const operationsTable = [ scheme: 'ecc', curve: 'Ed25519', flags: 'eddsa', - q: strToUint8Array('abc') + q: strToUint8Array('abc'), }, - giftId: strToUint8Array('def') + giftId: strToUint8Array('def'), }, signature: { type: 'sig-val', scheme: 'eddsa', r: strToUint8Array('1'), - s: strToUint8Array('2') - } + s: strToUint8Array('2'), + }, }, - } + }, ], answerPosition: 1n, resolveMeDesc: { type: 'desc:import-object', - position: 3n - } + position: 3n, + }, }, }, { @@ -452,19 +468,19 @@ export const operationsTable = [ type: 'op:pick', promisePosition: { type: 'desc:import-promise', - position: 1n + position: 1n, }, selectedValuePosition: 2n, - newAnswerPosition: 3n - } + newAnswerPosition: 3n, + }, }, { // ; reason: String syrup: `<${sym('op:abort')}${str('explode')}>`, value: { type: 'op:abort', - reason: 'explode' - } + reason: 'explode', + }, }, { // ; answer-pos: positive integer syrup: `<${sym('op:gc-answer')}${int(1)}>`, value: { type: 'op:gc-answer', - answerPosition: 1n - } + answerPosition: 1n, + }, }, ]; diff --git a/packages/syrup/test/_table.js b/packages/syrup/test/_table.js index 3cf5b3b6db..36dd359aa0 100644 --- a/packages/syrup/test/_table.js +++ b/packages/syrup/test/_table.js @@ -9,7 +9,7 @@ export const table = [ { syrup: 't', value: true }, { syrup: 'f', value: false }, { syrup: '5"hello', value: 'hello' }, - { syrup: '5\'hello', value: SyrupSymbolFor('hello') }, + { syrup: "5'hello", value: SyrupSymbolFor('hello') }, { syrup: '5:hello', value: textEncoder.encode('hello') }, { syrup: '[1+2+3+]', value: [1n, 2n, 3n] }, { syrup: '[3"abc3"def]', value: ['abc', 'def'] }, @@ -20,7 +20,10 @@ export const table = [ // order canonicalization { syrup: '{0"10+1"i20+}', value: { i: 20n, '': 10n } }, // dictionary with mixed string and symbol keys - { syrup: '{3"dog20+3\'cat10+}', value: { dog: 20n, [SyrupSymbolFor('cat')]: 10n } }, + { + syrup: '{3"dog20+3\'cat10+}', + value: { dog: 20n, [SyrupSymbolFor('cat')]: 10n }, + }, { syrup: 'D?\xf0\x00\x00\x00\x00\x00\x00', value: 1 }, { syrup: 'D@^\xdd/\x1a\x9f\xbew', value: 123.456 }, { syrup: '[3"foo123+t]', value: ['foo', 123n, true] }, diff --git a/packages/syrup/test/codec.test.js b/packages/syrup/test/codec.test.js index 254c82fe6d..cd8921aeba 100644 --- a/packages/syrup/test/codec.test.js +++ b/packages/syrup/test/codec.test.js @@ -3,8 +3,11 @@ import test from 'ava'; import { makeSyrupReader } from '../src/decode.js'; import { makeSyrupWriter } from '../src/encode.js'; -import { RecordUnionCodec, SyrupStringCodec, SyrupStructuredRecordCodecType } from '../src/codec.js'; - +import { + RecordUnionCodec, + SyrupStringCodec, + SyrupStructuredRecordCodecType, +} from '../src/codec.js'; const testCodecBidirectionally = (t, codec, value) => { const writer = makeSyrupWriter(); diff --git a/packages/syrup/test/decode.test.js b/packages/syrup/test/decode.test.js index bc293f8aec..16c68477ab 100644 --- a/packages/syrup/test/decode.test.js +++ b/packages/syrup/test/decode.test.js @@ -12,7 +12,7 @@ test('affirmative decode cases', t => { for (let i = 0; i < syrup.length; i += 1) { bytes[i] = syrup.charCodeAt(i); } - const desc = `for ${String(syrup)}` + const desc = `for ${String(syrup)}`; let actual; t.notThrows(() => { actual = decodeSyrup(bytes); @@ -27,8 +27,7 @@ test('must not be empty', t => { decodeSyrup(new Uint8Array(0), { name: 'known.sup' }); }, { - message: - 'Unexpected end of Syrup at index 0 of known.sup', + message: 'Unexpected end of Syrup at index 0 of known.sup', }, ); }); diff --git a/packages/syrup/test/ocapn.test.js b/packages/syrup/test/ocapn.test.js index 248215d6a2..bbb3f2596c 100644 --- a/packages/syrup/test/ocapn.test.js +++ b/packages/syrup/test/ocapn.test.js @@ -6,11 +6,15 @@ import { makeSyrupWriter } from '../src/encode.js'; import { OCapNComponentUnionCodec } from '../src/ocapn/components.js'; import { OCapNDescriptorUnionCodec } from '../src/ocapn/descriptors.js'; import { OCapNMessageUnionCodec } from '../src/ocapn/operations.js'; -import { componentsTable, descriptorsTable, operationsTable } from './_ocapn.js'; -import { OCapNPassableUnionCodec } from '../src/ocapn/passable.js'; +import { + componentsTable, + descriptorsTable, + operationsTable, +} from './_ocapn.js'; +import { OCapNPassableUnionCodec } from '../src/ocapn/passable.js'; const textEncoder = new TextEncoder(); -const sym = (s) => `${s.length}'${s}`; +const sym = s => `${s.length}'${s}`; const testBidirectionally = (t, codec, syrup, value, testName) => { const syrupBytes = textEncoder.encode(syrup); @@ -24,10 +28,13 @@ const testBidirectionally = (t, codec, syrup, value, testName) => { t.notThrows(() => { codec.marshal(value, syrupWriter); }, testName); - const bytes2 = syrupWriter.bufferWriter.subarray(0, syrupWriter.bufferWriter.length); + const bytes2 = syrupWriter.bufferWriter.subarray( + 0, + syrupWriter.bufferWriter.length, + ); const syrup2 = new TextDecoder().decode(bytes2); t.deepEqual(syrup2, syrup, testName); -} +}; test('affirmative component cases', t => { const codec = OCapNComponentUnionCodec; @@ -54,8 +61,13 @@ test('error on unknown record type in passable', t => { const codec = OCapNPassableUnionCodec; const syrup = `<${sym('unknown-record-type')}>`; const syrupBytes = textEncoder.encode(syrup); - const syrupReader = makeSyrupReader(syrupBytes, { name: 'unknown record type' }); - t.throws(() => { - codec.unmarshal(syrupReader); - }, { message: 'Unexpected record type: "unknown-record-type"' }); + const syrupReader = makeSyrupReader(syrupBytes, { + name: 'unknown record type', + }); + t.throws( + () => { + codec.unmarshal(syrupReader); + }, + { message: 'Unexpected record type: "unknown-record-type"' }, + ); }); diff --git a/packages/syrup/test/reader.test.js b/packages/syrup/test/reader.test.js index 97e5f154ffd4dc67b5a93eb2a618f9ef67f083e1..b1f1dd3e398c55e605119ac2488328b8b069c263 100644 GIT binary patch delta 112 zcmcbq`AU;VUthtYq*ymOBQ-gDBQqBhYf@!NYVl?fCLYGgO3a4BTna#tn_66)n4W3{ z<|s^#VRqxz0kUf~t+{}B@h($ delta 100 zcmaE*c~g^FUteJ(4;K@=Mp9)-YO&^KX(k>dc0m3c0Dp#fj;uR+F=t-Pjds zHLba7H773=5S@I1`3^|YL2i}J7g(gh8eX#oZ?XP+!9Aig<|*O+ Date: Thu, 10 Apr 2025 12:14:24 -1000 Subject: [PATCH 18/31] chore(syrup): rename codec marshall/unmarshall to write/read --- packages/syrup/src/codec.js | 124 ++++++++++++------------ packages/syrup/src/ocapn/components.js | 8 +- packages/syrup/src/ocapn/descriptors.js | 4 +- packages/syrup/src/ocapn/operations.js | 16 +-- packages/syrup/src/ocapn/passable.js | 26 ++--- packages/syrup/test/codec.test.js | 4 +- packages/syrup/test/ocapn.test.js | 6 +- 7 files changed, 94 insertions(+), 94 deletions(-) diff --git a/packages/syrup/src/codec.js b/packages/syrup/src/codec.js index f7dc31327f..2b3763a892 100644 --- a/packages/syrup/src/codec.js +++ b/packages/syrup/src/codec.js @@ -5,8 +5,8 @@ export class SyrupCodec { * @param {import('./decode.js').SyrupReader} syrupReader * @returns {any} */ - unmarshal(syrupReader) { - throw new Error('SyrupCodec: unmarshal must be implemented'); + read(syrupReader) { + throw new Error('SyrupCodec: read must be implemented'); } /** @@ -14,72 +14,72 @@ export class SyrupCodec { * @param {import('./encode.js').SyrupWriter} syrupWriter * @returns {void} */ - marshal(value, syrupWriter) { - throw new Error('SyrupCodec: marshal must be implemented'); + write(value, syrupWriter) { + throw new Error('SyrupCodec: write must be implemented'); } } export class SimpleSyrupCodecType extends SyrupCodec { /** * @param {object} options - * @param {function(any, import('./encode.js').SyrupWriter): void} options.marshal - * @param {function(import('./decode.js').SyrupReader): any} options.unmarshal + * @param {function(any, import('./encode.js').SyrupWriter): void} options.write + * @param {function(import('./decode.js').SyrupReader): any} options.read */ - constructor({ marshal, unmarshal }) { + constructor({ write, read }) { super(); - this.marshal = marshal; - this.unmarshal = unmarshal; + this.write = write; + this.read = read; } /** * @param {import('./decode.js').SyrupReader} syrupReader */ - unmarshal(syrupReader) { - this.unmarshal(syrupReader); + read(syrupReader) { + this.read(syrupReader); } /** * @param {any} value * @param {import('./encode.js').SyrupWriter} syrupWriter */ - marshal(value, syrupWriter) { - this.marshal(value, syrupWriter); + write(value, syrupWriter) { + this.write(value, syrupWriter); } } export const SyrupSymbolCodec = new SimpleSyrupCodecType({ - marshal: (value, syrupWriter) => syrupWriter.writeSymbol(value), - unmarshal: syrupReader => syrupReader.readSymbolAsString(), + write: (value, syrupWriter) => syrupWriter.writeSymbol(value), + read: syrupReader => syrupReader.readSymbolAsString(), }); export const SyrupStringCodec = new SimpleSyrupCodecType({ - marshal: (value, syrupWriter) => syrupWriter.writeString(value), - unmarshal: syrupReader => syrupReader.readString(), + write: (value, syrupWriter) => syrupWriter.writeString(value), + read: syrupReader => syrupReader.readString(), }); export const SyrupBytestringCodec = new SimpleSyrupCodecType({ - marshal: (value, syrupWriter) => syrupWriter.writeBytestring(value), - unmarshal: syrupReader => syrupReader.readBytestring(), + write: (value, syrupWriter) => syrupWriter.writeBytestring(value), + read: syrupReader => syrupReader.readBytestring(), }); export const SyrupBooleanCodec = new SimpleSyrupCodecType({ - marshal: (value, syrupWriter) => syrupWriter.writeBoolean(value), - unmarshal: syrupReader => syrupReader.readBoolean(), + write: (value, syrupWriter) => syrupWriter.writeBoolean(value), + read: syrupReader => syrupReader.readBoolean(), }); export const SyrupIntegerCodec = new SimpleSyrupCodecType({ - marshal: (value, syrupWriter) => syrupWriter.writeInteger(value), - unmarshal: syrupReader => syrupReader.readInteger(), + write: (value, syrupWriter) => syrupWriter.writeInteger(value), + read: syrupReader => syrupReader.readInteger(), }); export const SyrupDoubleCodec = new SimpleSyrupCodecType({ - marshal: (value, syrupWriter) => syrupWriter.writeDouble(value), - unmarshal: syrupReader => syrupReader.readFloat64(), + write: (value, syrupWriter) => syrupWriter.writeDouble(value), + read: syrupReader => syrupReader.readFloat64(), }); export const SyrupAnyCodec = new SimpleSyrupCodecType({ - marshal: (value, syrupWriter) => syrupWriter.writeAny(value), - unmarshal: syrupReader => syrupReader.readAny(), + write: (value, syrupWriter) => syrupWriter.writeAny(value), + read: syrupReader => syrupReader.readAny(), }); export class SyrupRecordCodecType extends SyrupCodec { @@ -94,13 +94,13 @@ export class SyrupRecordCodecType extends SyrupCodec { /** * @param {import('./decode.js').SyrupReader} syrupReader */ - unmarshal(syrupReader) { + read(syrupReader) { syrupReader.enterRecord(); const label = syrupReader.readSymbolAsString(); if (label !== this.label) { throw Error(`Expected label ${this.label}, got ${label}`); } - const result = this.unmarshalBody(syrupReader); + const result = this.readBody(syrupReader); syrupReader.exitRecord(); return result; } @@ -108,18 +108,18 @@ export class SyrupRecordCodecType extends SyrupCodec { /** * @param {import('./decode.js').SyrupReader} syrupReader */ - unmarshalBody(syrupReader) { - throw Error('SyrupRecordCodecType: unmarshalBody must be implemented'); + readBody(syrupReader) { + throw Error('SyrupRecordCodecType: readBody must be implemented'); } /** * @param {any} value * @param {import('./encode.js').SyrupWriter} syrupWriter */ - marshal(value, syrupWriter) { + write(value, syrupWriter) { syrupWriter.enterRecord(); syrupWriter.writeSymbol(value.type); - this.marshalBody(value, syrupWriter); + this.writeBody(value, syrupWriter); syrupWriter.exitRecord(); } @@ -127,8 +127,8 @@ export class SyrupRecordCodecType extends SyrupCodec { * @param {any} value * @param {import('./encode.js').SyrupWriter} syrupWriter */ - marshalBody(value, syrupWriter) { - throw Error('SyrupRecordCodecType: marshalBody must be implemented'); + writeBody(value, syrupWriter) { + throw Error('SyrupRecordCodecType: writeBody must be implemented'); } } export class SyrupStructuredRecordCodecType extends SyrupRecordCodecType { @@ -152,7 +152,7 @@ export class SyrupStructuredRecordCodecType extends SyrupRecordCodecType { /** * @param {import('./decode.js').SyrupReader} syrupReader */ - unmarshalBody(syrupReader) { + readBody(syrupReader) { const result = {}; for (const field of this.definition) { const [fieldName, fieldType] = field; @@ -162,7 +162,7 @@ export class SyrupStructuredRecordCodecType extends SyrupRecordCodecType { fieldValue = syrupReader.readOfType(fieldType); } else { const fieldDefinition = fieldType; - fieldValue = fieldDefinition.unmarshal(syrupReader); + fieldValue = fieldDefinition.read(syrupReader); } result[fieldName] = fieldValue; } @@ -174,7 +174,7 @@ export class SyrupStructuredRecordCodecType extends SyrupRecordCodecType { * @param {any} value * @param {import('./encode.js').SyrupWriter} syrupWriter */ - marshalBody(value, syrupWriter) { + writeBody(value, syrupWriter) { for (const field of this.definition) { const [fieldName, fieldType] = field; const fieldValue = value[fieldName]; @@ -182,7 +182,7 @@ export class SyrupStructuredRecordCodecType extends SyrupRecordCodecType { // @ts-expect-error fieldType is any string syrupWriter.writeOfType(fieldType, fieldValue); } else { - fieldType.marshal(fieldValue, syrupWriter); + fieldType.write(fieldValue, syrupWriter); } } } @@ -193,13 +193,13 @@ export class CustomRecordCodec extends SyrupRecordCodecType { /** * @param {string} label * @param {object} options - * @param {function(any, import('./encode.js').SyrupWriter): void} options.marshalBody - * @param {function(import('./decode.js').SyrupReader): any} options.unmarshalBody + * @param {function(any, import('./encode.js').SyrupWriter): void} options.writeBody + * @param {function(import('./decode.js').SyrupReader): any} options.readBody */ - constructor(label, { marshalBody, unmarshalBody }) { + constructor(label, { writeBody, readBody }) { super(label); - this.marshalBody = marshalBody; - this.unmarshalBody = unmarshalBody; + this.writeBody = writeBody; + this.readBody = readBody; } } @@ -226,30 +226,30 @@ export class RecordUnionCodec extends SyrupCodec { return this.recordTable[label] !== undefined; } - unmarshal(syrupReader) { + read(syrupReader) { syrupReader.enterRecord(); const label = syrupReader.readSymbolAsString(); const recordCodec = this.recordTable[label]; if (!recordCodec) { throw Error(`Unexpected record type: ${quote(label)}`); } - const result = recordCodec.unmarshalBody(syrupReader); + const result = recordCodec.readBody(syrupReader); syrupReader.exitRecord(); return result; } - marshal(value, syrupWriter) { + write(value, syrupWriter) { const { type } = value; const recordCodec = this.recordTable[type]; if (!recordCodec) { throw Error(`Unexpected record type: ${quote(type)}`); } - recordCodec.marshal(value, syrupWriter); + recordCodec.write(value, syrupWriter); } } export const SyrupListCodec = new SimpleSyrupCodecType({ - unmarshal(syrupReader) { + read(syrupReader) { syrupReader.enterList(); const result = []; while (!syrupReader.peekListEnd()) { @@ -260,30 +260,30 @@ export const SyrupListCodec = new SimpleSyrupCodecType({ syrupReader.exitList(); return result; }, - marshal(value, syrupWriter) { - throw Error('SyrupListCodec: marshal must be implemented'); + write(value, syrupWriter) { + throw Error('SyrupListCodec: write must be implemented'); }, }); export class CustomUnionCodecType extends SyrupCodec { /** * @param {object} options - * @param {function(import('./decode.js').SyrupReader): SyrupCodec} options.selectCodecForUnmarshal - * @param {function(any): SyrupCodec} options.selectCodecForMarshal + * @param {function(import('./decode.js').SyrupReader): SyrupCodec} options.selectCodecForRead + * @param {function(any): SyrupCodec} options.selectCodecForWrite */ - constructor({ selectCodecForUnmarshal, selectCodecForMarshal }) { + constructor({ selectCodecForRead, selectCodecForWrite }) { super(); - this.selectCodecForUnmarshal = selectCodecForUnmarshal; - this.selectCodecForMarshal = selectCodecForMarshal; + this.selectCodecForRead = selectCodecForRead; + this.selectCodecForWrite = selectCodecForWrite; } - unmarshal(syrupReader) { - const codec = this.selectCodecForUnmarshal(syrupReader); - return codec.unmarshal(syrupReader); + read(syrupReader) { + const codec = this.selectCodecForRead(syrupReader); + return codec.read(syrupReader); } - marshal(value, syrupWriter) { - const codec = this.selectCodecForMarshal(value); - codec.marshal(value, syrupWriter); + write(value, syrupWriter) { + const codec = this.selectCodecForWrite(value); + codec.write(value, syrupWriter); } } diff --git a/packages/syrup/src/ocapn/components.js b/packages/syrup/src/ocapn/components.js index 43a6ee72cc..745ffab8a5 100644 --- a/packages/syrup/src/ocapn/components.js +++ b/packages/syrup/src/ocapn/components.js @@ -17,7 +17,7 @@ export class OCapNSignatureValueCodec extends SyrupCodec { this.expectedLabel = expectedLabel; } - unmarshal(syrupReader) { + read(syrupReader) { const label = syrupReader.readSymbolAsString(); if (label !== this.expectedLabel) { throw Error(`Expected label ${this.expectedLabel}, got ${label}`); @@ -26,7 +26,7 @@ export class OCapNSignatureValueCodec extends SyrupCodec { return value; } - marshal(value, syrupWriter) { + write(value, syrupWriter) { syrupWriter.writeSymbol(this.expectedLabel); syrupWriter.writeBytestring(value); } @@ -70,10 +70,10 @@ export const OCapNComponentUnionCodec = new RecordUnionCodec({ }); export const readOCapComponent = syrupReader => { - return OCapNComponentUnionCodec.unmarshal(syrupReader); + return OCapNComponentUnionCodec.read(syrupReader); }; export const writeOCapComponent = (component, syrupWriter) => { - OCapNComponentUnionCodec.marshal(component, syrupWriter); + OCapNComponentUnionCodec.write(component, syrupWriter); return syrupWriter.bufferWriter.subarray(0, syrupWriter.bufferWriter.length); }; diff --git a/packages/syrup/src/ocapn/descriptors.js b/packages/syrup/src/ocapn/descriptors.js index 0a4170f100..4b4bc4fabc 100644 --- a/packages/syrup/src/ocapn/descriptors.js +++ b/packages/syrup/src/ocapn/descriptors.js @@ -78,10 +78,10 @@ export const OCapNDescriptorUnionCodec = new RecordUnionCodec({ }); export const readOCapDescriptor = syrupReader => { - return OCapNDescriptorUnionCodec.unmarshal(syrupReader); + return OCapNDescriptorUnionCodec.read(syrupReader); }; export const writeOCapDescriptor = (descriptor, syrupWriter) => { - OCapNDescriptorUnionCodec.marshal(descriptor, syrupWriter); + OCapNDescriptorUnionCodec.write(descriptor, syrupWriter); return syrupWriter.bufferWriter.subarray(0, syrupWriter.bufferWriter.length); }; diff --git a/packages/syrup/src/ocapn/operations.js b/packages/syrup/src/ocapn/operations.js index 36106e9e8e..e9b6f72c1f 100644 --- a/packages/syrup/src/ocapn/operations.js +++ b/packages/syrup/src/ocapn/operations.js @@ -44,23 +44,23 @@ const OCapNDeliverTargetCodec = new RecordUnionCodec(OCapNDeliverTargets); // Used by the deliver and deliver-only operations // First arg is method name, rest are Passables const OpDeliverArgsCodec = new SimpleSyrupCodecType({ - unmarshal: syrupReader => { + read: syrupReader => { syrupReader.enterList(); const result = [ // method name syrupReader.readSymbolAsString(), ]; while (!syrupReader.peekListEnd()) { - result.push(OCapNPassableUnionCodec.unmarshal(syrupReader)); + result.push(OCapNPassableUnionCodec.read(syrupReader)); } syrupReader.exitList(); return result; }, - marshal: ([methodName, ...args], syrupWriter) => { + write: ([methodName, ...args], syrupWriter) => { syrupWriter.enterList(); syrupWriter.writeSymbol(methodName); for (const arg of args) { - OCapNPassableUnionCodec.marshal(arg, syrupWriter); + OCapNPassableUnionCodec.write(arg, syrupWriter); } syrupWriter.exitList(); }, @@ -72,7 +72,7 @@ const OpDeliverOnly = new SyrupStructuredRecordCodecType('op:deliver-only', [ ]); const OpDeliverAnswerCodec = new SimpleSyrupCodecType({ - unmarshal: syrupReader => { + read: syrupReader => { const typeHint = syrupReader.peekTypeHint(); if (typeHint === 'number-prefix') { // should be an integer @@ -83,7 +83,7 @@ const OpDeliverAnswerCodec = new SimpleSyrupCodecType({ } throw Error(`Expected integer or boolean, got ${typeHint}`); }, - marshal: (value, syrupWriter) => { + write: (value, syrupWriter) => { if (typeof value === 'bigint') { syrupWriter.writeInteger(value); } else if (typeof value === 'boolean') { @@ -137,10 +137,10 @@ export const OCapNMessageUnionCodec = new RecordUnionCodec({ }); export const readOCapNMessage = syrupReader => { - return OCapNMessageUnionCodec.unmarshal(syrupReader); + return OCapNMessageUnionCodec.read(syrupReader); }; export const writeOCapNMessage = (message, syrupWriter) => { - OCapNMessageUnionCodec.marshal(message, syrupWriter); + OCapNMessageUnionCodec.write(message, syrupWriter); return syrupWriter.bufferWriter.subarray(0, syrupWriter.bufferWriter.length); }; diff --git a/packages/syrup/src/ocapn/passable.js b/packages/syrup/src/ocapn/passable.js index b4f6e77276..1a9cc080c1 100644 --- a/packages/syrup/src/ocapn/passable.js +++ b/packages/syrup/src/ocapn/passable.js @@ -25,19 +25,19 @@ import { // OCapN Passable Atoms const UndefinedCodec = new CustomRecordCodec('void', { - unmarshalBody(syrupReader) { + readBody(syrupReader) { return undefined; }, - marshalBody(value, syrupWriter) { + writeBody(value, syrupWriter) { // body is empty }, }); const NullCodec = new CustomRecordCodec('null', { - unmarshalBody(syrupReader) { + readBody(syrupReader) { return null; }, - marshalBody(value, syrupWriter) { + writeBody(value, syrupWriter) { // body is empty }, }); @@ -59,16 +59,16 @@ const AtomCodecs = { // TODO: dictionary but with only string keys export const OCapNStructCodec = new SimpleSyrupCodecType({ - unmarshal(syrupReader) { - throw Error('OCapNStructCodec: unmarshal must be implemented'); + read(syrupReader) { + throw Error('OCapNStructCodec: read must be implemented'); }, - marshal(value, syrupWriter) { - throw Error('OCapNStructCodec: marshal must be implemented'); + write(value, syrupWriter) { + throw Error('OCapNStructCodec: write must be implemented'); }, }); const OCapNTaggedCodec = new CustomRecordCodec('desc:tagged', { - unmarshalBody(syrupReader) { + readBody(syrupReader) { const tagName = syrupReader.readSymbolAsString(); // @ts-expect-error any type const value = syrupReader.readOfType('any'); @@ -79,9 +79,9 @@ const OCapNTaggedCodec = new CustomRecordCodec('desc:tagged', { value, }; }, - marshalBody(value, syrupWriter) { + writeBody(value, syrupWriter) { syrupWriter.writeSymbol(value.tagName); - value.value.marshal(syrupWriter); + value.value.write(syrupWriter); }, }); @@ -138,7 +138,7 @@ const OCapNPassableRecordUnionCodec = new RecordUnionCodec({ }); export const OCapNPassableUnionCodec = new CustomUnionCodecType({ - selectCodecForUnmarshal(syrupReader) { + selectCodecForRead(syrupReader) { const typeHint = syrupReader.peekTypeHint(); switch (typeHint) { case 'boolean': @@ -160,7 +160,7 @@ export const OCapNPassableUnionCodec = new CustomUnionCodecType({ throw Error(`Unknown type hint: ${typeHint}`); } }, - selectCodecForMarshal(value) { + selectCodecForWrite(value) { if (value === undefined) { return AtomCodecs.undefined; } diff --git a/packages/syrup/test/codec.test.js b/packages/syrup/test/codec.test.js index cd8921aeba..8fa0c5708e 100644 --- a/packages/syrup/test/codec.test.js +++ b/packages/syrup/test/codec.test.js @@ -11,10 +11,10 @@ import { const testCodecBidirectionally = (t, codec, value) => { const writer = makeSyrupWriter(); - codec.marshal(value, writer); + codec.write(value, writer); const bytes = writer.bufferWriter.subarray(0, writer.bufferWriter.length); const reader = makeSyrupReader(bytes); - const result = codec.unmarshal(reader); + const result = codec.read(reader); t.deepEqual(result, value); }; diff --git a/packages/syrup/test/ocapn.test.js b/packages/syrup/test/ocapn.test.js index bbb3f2596c..e5776a1935 100644 --- a/packages/syrup/test/ocapn.test.js +++ b/packages/syrup/test/ocapn.test.js @@ -21,12 +21,12 @@ const testBidirectionally = (t, codec, syrup, value, testName) => { const syrupReader = makeSyrupReader(syrupBytes, { name: testName }); let result; t.notThrows(() => { - result = codec.unmarshal(syrupReader); + result = codec.read(syrupReader); }, testName); t.deepEqual(result, value, testName); const syrupWriter = makeSyrupWriter(); t.notThrows(() => { - codec.marshal(value, syrupWriter); + codec.write(value, syrupWriter); }, testName); const bytes2 = syrupWriter.bufferWriter.subarray( 0, @@ -66,7 +66,7 @@ test('error on unknown record type in passable', t => { }); t.throws( () => { - codec.unmarshal(syrupReader); + codec.read(syrupReader); }, { message: 'Unexpected record type: "unknown-record-type"' }, ); From 2c4a47e9e35749ef382f8cc537904985f4ff7042 Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 11 Apr 2025 09:59:56 -1000 Subject: [PATCH 19/31] chore(syrup): remove underscore prefix from private fields --- packages/syrup/src/decode.js | 38 ++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/syrup/src/decode.js b/packages/syrup/src/decode.js index 2864d3070a..152366b62d 100644 --- a/packages/syrup/src/decode.js +++ b/packages/syrup/src/decode.js @@ -452,7 +452,7 @@ export class SyrupReader { /** * @param {number} expectedByte */ - #_readAndAssertByte(expectedByte) { + #readAndAssertByte(expectedByte) { const start = this.bufferReader.index; const cc = this.bufferReader.readByte(); if (cc !== expectedByte) { @@ -465,7 +465,7 @@ export class SyrupReader { /** * @param {string} type */ - #_pushStackEntry(type) { + #pushStackEntry(type) { this.state.stack.push( new SyrupReaderStackEntry(type, this.bufferReader.index), ); @@ -474,7 +474,7 @@ export class SyrupReader { /** * @param {string} expectedType */ - #_popStackEntry(expectedType) { + #popStackEntry(expectedType) { const start = this.bufferReader.index; const stackEntry = this.state.stack.pop(); if (!stackEntry) { @@ -490,13 +490,13 @@ export class SyrupReader { } enterRecord() { - this.#_readAndAssertByte(RECORD_START); - this.#_pushStackEntry('record'); + this.#readAndAssertByte(RECORD_START); + this.#pushStackEntry('record'); } exitRecord() { - this.#_readAndAssertByte(RECORD_END); - this.#_popStackEntry('record'); + this.#readAndAssertByte(RECORD_END); + this.#popStackEntry('record'); } peekRecordEnd() { @@ -505,13 +505,13 @@ export class SyrupReader { } enterDictionary() { - this.#_readAndAssertByte(DICT_START); - this.#_pushStackEntry('dictionary'); + this.#readAndAssertByte(DICT_START); + this.#pushStackEntry('dictionary'); } exitDictionary() { - this.#_readAndAssertByte(DICT_END); - this.#_popStackEntry('dictionary'); + this.#readAndAssertByte(DICT_END); + this.#popStackEntry('dictionary'); } peekDictionaryEnd() { @@ -520,13 +520,13 @@ export class SyrupReader { } enterList() { - this.#_readAndAssertByte(LIST_START); - this.#_pushStackEntry('list'); + this.#readAndAssertByte(LIST_START); + this.#pushStackEntry('list'); } exitList() { - this.#_readAndAssertByte(LIST_END); - this.#_popStackEntry('list'); + this.#readAndAssertByte(LIST_END); + this.#popStackEntry('list'); } peekListEnd() { @@ -535,13 +535,13 @@ export class SyrupReader { } enterSet() { - this.#_readAndAssertByte(SET_START); - this.#_pushStackEntry('set'); + this.#readAndAssertByte(SET_START); + this.#pushStackEntry('set'); } exitSet() { - this.#_readAndAssertByte(SET_END); - this.#_popStackEntry('set'); + this.#readAndAssertByte(SET_END); + this.#popStackEntry('set'); } peekSetEnd() { From e27047f584449513c407130f25797b4151368fcb Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 11 Apr 2025 11:02:48 -1000 Subject: [PATCH 20/31] refactor(syrup): codecs as types instead of classes --- packages/syrup/src/codec.js | 355 ++++++++++-------------- packages/syrup/src/ocapn/components.js | 63 ++--- packages/syrup/src/ocapn/descriptors.js | 23 +- packages/syrup/src/ocapn/operations.js | 35 +-- packages/syrup/src/ocapn/passable.js | 103 ++++--- packages/syrup/test/codec.test.js | 16 +- 6 files changed, 281 insertions(+), 314 deletions(-) diff --git a/packages/syrup/src/codec.js b/packages/syrup/src/codec.js index 2b3763a892..ccd54a2b2d 100644 --- a/packages/syrup/src/codec.js +++ b/packages/syrup/src/codec.js @@ -1,160 +1,147 @@ +const { freeze } = Object; const quote = JSON.stringify; -export class SyrupCodec { - /** - * @param {import('./decode.js').SyrupReader} syrupReader - * @returns {any} - */ - read(syrupReader) { - throw new Error('SyrupCodec: read must be implemented'); - } - - /** - * @param {any} value - * @param {import('./encode.js').SyrupWriter} syrupWriter - * @returns {void} - */ - write(value, syrupWriter) { - throw new Error('SyrupCodec: write must be implemented'); - } -} +/** @typedef {import('./decode.js').SyrupReader} SyrupReader */ +/** @typedef {import('./encode.js').SyrupWriter} SyrupWriter */ -export class SimpleSyrupCodecType extends SyrupCodec { - /** - * @param {object} options - * @param {function(any, import('./encode.js').SyrupWriter): void} options.write - * @param {function(import('./decode.js').SyrupReader): any} options.read - */ - constructor({ write, read }) { - super(); - this.write = write; - this.read = read; - } +/** + * @typedef {object} SyrupCodec + * @property {function(SyrupReader): any} read + * @property {function(any, SyrupWriter): void} write + */ - /** - * @param {import('./decode.js').SyrupReader} syrupReader - */ - read(syrupReader) { - this.read(syrupReader); - } - - /** - * @param {any} value - * @param {import('./encode.js').SyrupWriter} syrupWriter - */ - write(value, syrupWriter) { - this.write(value, syrupWriter); - } -} - -export const SyrupSymbolCodec = new SimpleSyrupCodecType({ +/** @type {SyrupCodec} */ +export const SymbolCodec = freeze({ write: (value, syrupWriter) => syrupWriter.writeSymbol(value), read: syrupReader => syrupReader.readSymbolAsString(), }); -export const SyrupStringCodec = new SimpleSyrupCodecType({ +/** @type {SyrupCodec} */ +export const StringCodec = freeze({ write: (value, syrupWriter) => syrupWriter.writeString(value), read: syrupReader => syrupReader.readString(), }); -export const SyrupBytestringCodec = new SimpleSyrupCodecType({ +/** @type {SyrupCodec} */ +export const BytestringCodec = freeze({ write: (value, syrupWriter) => syrupWriter.writeBytestring(value), read: syrupReader => syrupReader.readBytestring(), }); -export const SyrupBooleanCodec = new SimpleSyrupCodecType({ +/** @type {SyrupCodec} */ +export const BooleanCodec = freeze({ write: (value, syrupWriter) => syrupWriter.writeBoolean(value), read: syrupReader => syrupReader.readBoolean(), }); -export const SyrupIntegerCodec = new SimpleSyrupCodecType({ +/** @type {SyrupCodec} */ +export const IntegerCodec = freeze({ write: (value, syrupWriter) => syrupWriter.writeInteger(value), read: syrupReader => syrupReader.readInteger(), }); -export const SyrupDoubleCodec = new SimpleSyrupCodecType({ +/** @type {SyrupCodec} */ +export const DoubleCodec = freeze({ write: (value, syrupWriter) => syrupWriter.writeDouble(value), read: syrupReader => syrupReader.readFloat64(), }); -export const SyrupAnyCodec = new SimpleSyrupCodecType({ +/** @type {SyrupCodec} */ +export const AnyCodec = freeze({ write: (value, syrupWriter) => syrupWriter.writeAny(value), read: syrupReader => syrupReader.readAny(), }); -export class SyrupRecordCodecType extends SyrupCodec { - /** - * @param {string} label - */ - constructor(label) { - super(); - this.label = label; - } - +/** @type {SyrupCodec} */ +export const ListCodec = freeze({ /** - * @param {import('./decode.js').SyrupReader} syrupReader + * @param {SyrupReader} syrupReader + * @returns {any[]} */ read(syrupReader) { - syrupReader.enterRecord(); - const label = syrupReader.readSymbolAsString(); - if (label !== this.label) { - throw Error(`Expected label ${this.label}, got ${label}`); + syrupReader.enterList(); + const result = []; + while (!syrupReader.peekListEnd()) { + const value = syrupReader.readAny(); + result.push(value); } - const result = this.readBody(syrupReader); - syrupReader.exitRecord(); + syrupReader.exitList(); return result; - } - + }, /** - * @param {import('./decode.js').SyrupReader} syrupReader + * @param {any} value + * @param {SyrupWriter} syrupWriter */ - readBody(syrupReader) { - throw Error('SyrupRecordCodecType: readBody must be implemented'); - } + write(value, syrupWriter) { + throw Error('SyrupListCodec: write must be implemented'); + }, +}); +/** + * @typedef {SyrupCodec & { + * label: string; + * readBody: (SyrupReader) => any; + * writeBody: (any, SyrupWriter) => void; + * }} SyrupRecordCodec + */ + +/** + * @param {string} label + * @param {function(SyrupReader): any} readBody + * @param {function(any, SyrupWriter): void} writeBody + * @returns {SyrupRecordCodec} + */ +export const makeRecordCodec = (label, readBody, writeBody) => { + /** + * @param {SyrupReader} syrupReader + * @returns {any} + */ + const read = syrupReader => { + syrupReader.enterRecord(); + const actualLabel = syrupReader.readSymbolAsString(); + if (actualLabel !== label) { + throw Error( + `RecordCodec: Expected label ${quote(label)}, got ${quote(actualLabel)}`, + ); + } + const result = readBody(syrupReader); + syrupReader.exitRecord(); + return result; + }; /** * @param {any} value - * @param {import('./encode.js').SyrupWriter} syrupWriter + * @param {SyrupWriter} syrupWriter */ - write(value, syrupWriter) { + const write = (value, syrupWriter) => { syrupWriter.enterRecord(); syrupWriter.writeSymbol(value.type); - this.writeBody(value, syrupWriter); + writeBody(value, syrupWriter); syrupWriter.exitRecord(); - } - - /** - * @param {any} value - * @param {import('./encode.js').SyrupWriter} syrupWriter - */ - writeBody(value, syrupWriter) { - throw Error('SyrupRecordCodecType: writeBody must be implemented'); - } -} -export class SyrupStructuredRecordCodecType extends SyrupRecordCodecType { - /** - * @param {string} label - * @param {Array<[string, string | SyrupCodec]>} definition - */ - // TODO: improve definition type to restricted strings - constructor(label, definition) { - super(label); - this.definition = definition; - for (const [fieldName] of definition) { - if (fieldName === 'type') { - throw new Error( - 'SyrupRecordCodec: The "type" field is reserved for internal use.', - ); - } - } - } - - /** - * @param {import('./decode.js').SyrupReader} syrupReader + }; + return freeze({ + label, + read, + readBody, + write, + writeBody, + }); +}; + +/** @typedef {Array<[string, string | SyrupCodec]>} SyrupRecordDefinition */ + +/** + * @param {string} label + * @param {SyrupRecordDefinition} definition + * @returns {SyrupRecordCodec} + */ +export const makeRecordCodecFromDefinition = (label, definition) => { + /** + * @param {SyrupReader} syrupReader + * @returns {any} */ - readBody(syrupReader) { + const readBody = syrupReader => { const result = {}; - for (const field of this.definition) { + for (const field of definition) { const [fieldName, fieldType] = field; let fieldValue; if (typeof fieldType === 'string') { @@ -166,16 +153,15 @@ export class SyrupStructuredRecordCodecType extends SyrupRecordCodecType { } result[fieldName] = fieldValue; } - result.type = this.label; + result.type = label; return result; - } - + }; /** * @param {any} value - * @param {import('./encode.js').SyrupWriter} syrupWriter + * @param {SyrupWriter} syrupWriter */ - writeBody(value, syrupWriter) { - for (const field of this.definition) { + const writeBody = (value, syrupWriter) => { + for (const field of definition) { const [fieldName, fieldType] = field; const fieldValue = value[fieldName]; if (typeof fieldType === 'string') { @@ -185,105 +171,72 @@ export class SyrupStructuredRecordCodecType extends SyrupRecordCodecType { fieldType.write(fieldValue, syrupWriter); } } - } -} + }; -// TODO: vestigial "definition" argument -export class CustomRecordCodec extends SyrupRecordCodecType { + return makeRecordCodec(label, readBody, writeBody); +}; + +/** + * @param {function(SyrupReader): SyrupCodec} selectCodecForRead + * @param {function(any): SyrupCodec} selectCodecForWrite + * @returns {SyrupCodec} + */ +export const makeUnionCodec = (selectCodecForRead, selectCodecForWrite) => { /** - * @param {string} label - * @param {object} options - * @param {function(any, import('./encode.js').SyrupWriter): void} options.writeBody - * @param {function(import('./decode.js').SyrupReader): any} options.readBody + * @param {SyrupReader} syrupReader + * @returns {SyrupCodec} */ - constructor(label, { writeBody, readBody }) { - super(label); - this.writeBody = writeBody; - this.readBody = readBody; - } -} - -export class RecordUnionCodec extends SyrupCodec { + const read = syrupReader => { + const codec = selectCodecForRead(syrupReader); + return codec.read(syrupReader); + }; /** - * @param {Record} recordTypes + * @param {any} value + * @param {SyrupWriter} syrupWriter */ - constructor(recordTypes) { - super(); - this.recordTypes = recordTypes; - const labelSet = new Set(); - this.recordTable = Object.fromEntries( - Object.values(recordTypes).map(recordCodec => { - if (labelSet.has(recordCodec.label)) { - throw Error(`Duplicate record type: ${recordCodec.label}`); - } - labelSet.add(recordCodec.label); - return [recordCodec.label, recordCodec]; - }), - ); - } - - supports(label) { - return this.recordTable[label] !== undefined; - } - - read(syrupReader) { + const write = (value, syrupWriter) => { + const codec = selectCodecForWrite(value); + codec.write(value, syrupWriter); + }; + return freeze({ read, write }); +}; + +/** + * @typedef {SyrupCodec & { + * supports: (label: string) => boolean; + * }} SyrupRecordUnionCodec + */ + +/** + * @param {Record} recordTypes + * @returns {SyrupRecordUnionCodec} + */ +export const makeRecordUnionCodec = recordTypes => { + const recordTable = Object.fromEntries( + Object.values(recordTypes).map(recordCodec => { + return [recordCodec.label, recordCodec]; + }), + ); + const supports = label => { + return recordTable[label] !== undefined; + }; + const read = syrupReader => { syrupReader.enterRecord(); const label = syrupReader.readSymbolAsString(); - const recordCodec = this.recordTable[label]; + const recordCodec = recordTable[label]; if (!recordCodec) { throw Error(`Unexpected record type: ${quote(label)}`); } const result = recordCodec.readBody(syrupReader); syrupReader.exitRecord(); return result; - } - - write(value, syrupWriter) { - const { type } = value; - const recordCodec = this.recordTable[type]; + }; + const write = (value, syrupWriter) => { + const recordCodec = recordTable[value.type]; if (!recordCodec) { - throw Error(`Unexpected record type: ${quote(type)}`); + throw Error(`Unexpected record type: ${quote(value.type)}`); } recordCodec.write(value, syrupWriter); - } -} - -export const SyrupListCodec = new SimpleSyrupCodecType({ - read(syrupReader) { - syrupReader.enterList(); - const result = []; - while (!syrupReader.peekListEnd()) { - const value = syrupReader.readAny(); - console.log('readAny', value); - result.push(value); - } - syrupReader.exitList(); - return result; - }, - write(value, syrupWriter) { - throw Error('SyrupListCodec: write must be implemented'); - }, -}); - -export class CustomUnionCodecType extends SyrupCodec { - /** - * @param {object} options - * @param {function(import('./decode.js').SyrupReader): SyrupCodec} options.selectCodecForRead - * @param {function(any): SyrupCodec} options.selectCodecForWrite - */ - constructor({ selectCodecForRead, selectCodecForWrite }) { - super(); - this.selectCodecForRead = selectCodecForRead; - this.selectCodecForWrite = selectCodecForWrite; - } - - read(syrupReader) { - const codec = this.selectCodecForRead(syrupReader); - return codec.read(syrupReader); - } - - write(value, syrupWriter) { - const codec = this.selectCodecForWrite(value); - codec.write(value, syrupWriter); - } -} + }; + return freeze({ read, write, supports }); +}; diff --git a/packages/syrup/src/ocapn/components.js b/packages/syrup/src/ocapn/components.js index 745ffab8a5..75b1276128 100644 --- a/packages/syrup/src/ocapn/components.js +++ b/packages/syrup/src/ocapn/components.js @@ -1,68 +1,63 @@ import { - RecordUnionCodec, - SyrupCodec, - SyrupStructuredRecordCodecType, + makeRecordCodecFromDefinition, + makeRecordUnionCodec, } from '../codec.js'; +/** @typedef {import('../codec.js').SyrupCodec} SyrupCodec */ + +const { freeze } = Object; /* * OCapN Components are used in both OCapN Messages and Descriptors */ -export class OCapNSignatureValueCodec extends SyrupCodec { - /** - * @param {string} expectedLabel - */ - constructor(expectedLabel) { - super(); - this.expectedLabel = expectedLabel; - } - - read(syrupReader) { +/** + * @param {string} expectedLabel + * @returns {SyrupCodec} + */ +export const makeOCapNSignatureValueComponentCodec = expectedLabel => { + const read = syrupReader => { const label = syrupReader.readSymbolAsString(); - if (label !== this.expectedLabel) { - throw Error(`Expected label ${this.expectedLabel}, got ${label}`); + if (label !== expectedLabel) { + throw Error(`Expected label ${expectedLabel}, got ${label}`); } const value = syrupReader.readBytestring(); return value; - } - - write(value, syrupWriter) { - syrupWriter.writeSymbol(this.expectedLabel); + }; + const write = (value, syrupWriter) => { + syrupWriter.writeSymbol(expectedLabel); syrupWriter.writeBytestring(value); - } -} + }; + return freeze({ read, write }); +}; -const OCapNSignatureRValue = new OCapNSignatureValueCodec('r'); -const OCapNSignatureSValue = new OCapNSignatureValueCodec('s'); +const OCapNSignatureRValue = makeOCapNSignatureValueComponentCodec('r'); +const OCapNSignatureSValue = makeOCapNSignatureValueComponentCodec('s'); -export const OCapNSignature = new SyrupStructuredRecordCodecType('sig-val', [ +export const OCapNSignature = makeRecordCodecFromDefinition('sig-val', [ ['scheme', 'symbol'], ['r', OCapNSignatureRValue], ['s', OCapNSignatureSValue], ]); -export const OCapNNode = new SyrupStructuredRecordCodecType('ocapn-node', [ +export const OCapNNode = makeRecordCodecFromDefinition('ocapn-node', [ ['transport', 'symbol'], ['address', 'bytestring'], ['hints', 'boolean'], ]); -export const OCapNSturdyRef = new SyrupStructuredRecordCodecType( - 'ocapn-sturdyref', - [ - ['node', OCapNNode], - ['swissNum', 'string'], - ], -); +export const OCapNSturdyRef = makeRecordCodecFromDefinition('ocapn-sturdyref', [ + ['node', OCapNNode], + ['swissNum', 'string'], +]); -export const OCapNPublicKey = new SyrupStructuredRecordCodecType('public-key', [ +export const OCapNPublicKey = makeRecordCodecFromDefinition('public-key', [ ['scheme', 'symbol'], ['curve', 'symbol'], ['flags', 'symbol'], ['q', 'bytestring'], ]); -export const OCapNComponentUnionCodec = new RecordUnionCodec({ +export const OCapNComponentUnionCodec = makeRecordUnionCodec({ OCapNNode, OCapNSturdyRef, OCapNPublicKey, diff --git a/packages/syrup/src/ocapn/descriptors.js b/packages/syrup/src/ocapn/descriptors.js index 4b4bc4fabc..da58c66d9a 100644 --- a/packages/syrup/src/ocapn/descriptors.js +++ b/packages/syrup/src/ocapn/descriptors.js @@ -1,4 +1,7 @@ -import { RecordUnionCodec, SyrupStructuredRecordCodecType } from '../codec.js'; +import { + makeRecordCodecFromDefinition, + makeRecordUnionCodec, +} from '../codec.js'; import { OCapNNode, OCapNPublicKey, OCapNSignature } from './components.js'; /* @@ -6,25 +9,25 @@ import { OCapNNode, OCapNPublicKey, OCapNSignature } from './components.js'; * directly in OCapN Messages and as part of Passable structures. */ -export const DescImportObject = new SyrupStructuredRecordCodecType( +export const DescImportObject = makeRecordCodecFromDefinition( 'desc:import-object', [['position', 'integer']], ); -export const DescImportPromise = new SyrupStructuredRecordCodecType( +export const DescImportPromise = makeRecordCodecFromDefinition( 'desc:import-promise', [['position', 'integer']], ); -export const DescExport = new SyrupStructuredRecordCodecType('desc:export', [ +export const DescExport = makeRecordCodecFromDefinition('desc:export', [ ['position', 'integer'], ]); -export const DescAnswer = new SyrupStructuredRecordCodecType('desc:answer', [ +export const DescAnswer = makeRecordCodecFromDefinition('desc:answer', [ ['position', 'integer'], ]); -export const DescHandoffGive = new SyrupStructuredRecordCodecType( +export const DescHandoffGive = makeRecordCodecFromDefinition( 'desc:handoff-give', [ ['receiverKey', OCapNPublicKey], @@ -35,7 +38,7 @@ export const DescHandoffGive = new SyrupStructuredRecordCodecType( ], ); -export const DescSigGiveEnvelope = new SyrupStructuredRecordCodecType( +export const DescSigGiveEnvelope = makeRecordCodecFromDefinition( 'desc:sig-envelope', [ ['object', DescHandoffGive], @@ -43,7 +46,7 @@ export const DescSigGiveEnvelope = new SyrupStructuredRecordCodecType( ], ); -export const DescHandoffReceive = new SyrupStructuredRecordCodecType( +export const DescHandoffReceive = makeRecordCodecFromDefinition( 'desc:handoff-receive', [ ['receivingSession', 'bytestring'], @@ -53,7 +56,7 @@ export const DescHandoffReceive = new SyrupStructuredRecordCodecType( ], ); -export const DescSigReceiveEnvelope = new SyrupStructuredRecordCodecType( +export const DescSigReceiveEnvelope = makeRecordCodecFromDefinition( 'desc:sig-envelope', [ ['object', DescHandoffReceive], @@ -62,7 +65,7 @@ export const DescSigReceiveEnvelope = new SyrupStructuredRecordCodecType( ); // Note: this may only be useful for testing -export const OCapNDescriptorUnionCodec = new RecordUnionCodec({ +export const OCapNDescriptorUnionCodec = makeRecordUnionCodec({ OCapNNode, OCapNPublicKey, OCapNSignature, diff --git a/packages/syrup/src/ocapn/operations.js b/packages/syrup/src/ocapn/operations.js index e9b6f72c1f..b05863bcff 100644 --- a/packages/syrup/src/ocapn/operations.js +++ b/packages/syrup/src/ocapn/operations.js @@ -1,7 +1,6 @@ import { - RecordUnionCodec, - SyrupStructuredRecordCodecType, - SimpleSyrupCodecType, + makeRecordUnionCodec, + makeRecordCodecFromDefinition, } from '../codec.js'; import { OCapNNode, OCapNPublicKey, OCapNSignature } from './components.js'; import { OCapNPassableUnionCodec } from './passable.js'; @@ -12,23 +11,25 @@ import { DescAnswer, } from './descriptors.js'; +const { freeze } = Object; + /* * These are OCapN Operations, they are messages that are sent between OCapN Nodes */ -const OpStartSession = new SyrupStructuredRecordCodecType('op:start-session', [ +const OpStartSession = makeRecordCodecFromDefinition('op:start-session', [ ['captpVersion', 'string'], ['sessionPublicKey', OCapNPublicKey], ['location', OCapNNode], ['locationSignature', OCapNSignature], ]); -const OCapNResolveMeDescCodec = new RecordUnionCodec({ +const OCapNResolveMeDescCodec = makeRecordUnionCodec({ DescImportObject, DescImportPromise, }); -const OpListen = new SyrupStructuredRecordCodecType('op:listen', [ +const OpListen = makeRecordCodecFromDefinition('op:listen', [ ['to', DescExport], ['resolveMeDesc', OCapNResolveMeDescCodec], ['wantsPartial', 'boolean'], @@ -39,11 +40,11 @@ const OCapNDeliverTargets = { DescAnswer, }; -const OCapNDeliverTargetCodec = new RecordUnionCodec(OCapNDeliverTargets); +const OCapNDeliverTargetCodec = makeRecordUnionCodec(OCapNDeliverTargets); // Used by the deliver and deliver-only operations // First arg is method name, rest are Passables -const OpDeliverArgsCodec = new SimpleSyrupCodecType({ +const OpDeliverArgsCodec = freeze({ read: syrupReader => { syrupReader.enterList(); const result = [ @@ -66,12 +67,12 @@ const OpDeliverArgsCodec = new SimpleSyrupCodecType({ }, }); -const OpDeliverOnly = new SyrupStructuredRecordCodecType('op:deliver-only', [ +const OpDeliverOnly = makeRecordCodecFromDefinition('op:deliver-only', [ ['to', OCapNDeliverTargetCodec], ['args', OpDeliverArgsCodec], ]); -const OpDeliverAnswerCodec = new SimpleSyrupCodecType({ +const OpDeliverAnswerCodec = freeze({ read: syrupReader => { const typeHint = syrupReader.peekTypeHint(); if (typeHint === 'number-prefix') { @@ -94,38 +95,38 @@ const OpDeliverAnswerCodec = new SimpleSyrupCodecType({ }, }); -const OpDeliver = new SyrupStructuredRecordCodecType('op:deliver', [ +const OpDeliver = makeRecordCodecFromDefinition('op:deliver', [ ['to', OCapNDeliverTargetCodec], ['args', OpDeliverArgsCodec], ['answerPosition', OpDeliverAnswerCodec], ['resolveMeDesc', OCapNResolveMeDescCodec], ]); -const OCapNPromiseRefCodec = new RecordUnionCodec({ +const OCapNPromiseRefCodec = makeRecordUnionCodec({ DescAnswer, DescImportPromise, }); -const OpPick = new SyrupStructuredRecordCodecType('op:pick', [ +const OpPick = makeRecordCodecFromDefinition('op:pick', [ ['promisePosition', OCapNPromiseRefCodec], ['selectedValuePosition', 'integer'], ['newAnswerPosition', 'integer'], ]); -const OpAbort = new SyrupStructuredRecordCodecType('op:abort', [ +const OpAbort = makeRecordCodecFromDefinition('op:abort', [ ['reason', 'string'], ]); -const OpGcExport = new SyrupStructuredRecordCodecType('op:gc-export', [ +const OpGcExport = makeRecordCodecFromDefinition('op:gc-export', [ ['exportPosition', 'integer'], ['wireDelta', 'integer'], ]); -const OpGcAnswer = new SyrupStructuredRecordCodecType('op:gc-answer', [ +const OpGcAnswer = makeRecordCodecFromDefinition('op:gc-answer', [ ['answerPosition', 'integer'], ]); -export const OCapNMessageUnionCodec = new RecordUnionCodec({ +export const OCapNMessageUnionCodec = makeRecordUnionCodec({ OpStartSession, OpDeliverOnly, OpDeliver, diff --git a/packages/syrup/src/ocapn/passable.js b/packages/syrup/src/ocapn/passable.js index 1a9cc080c1..bfeea13409 100644 --- a/packages/syrup/src/ocapn/passable.js +++ b/packages/syrup/src/ocapn/passable.js @@ -1,17 +1,16 @@ import { - RecordUnionCodec, - SimpleSyrupCodecType, - SyrupBooleanCodec, - SyrupIntegerCodec, - SyrupDoubleCodec, - SyrupSymbolCodec, - SyrupStringCodec, - SyrupBytestringCodec, - SyrupListCodec, - CustomRecordCodec, - CustomUnionCodecType, - SyrupAnyCodec, - SyrupStructuredRecordCodecType, + BooleanCodec, + IntegerCodec, + DoubleCodec, + SymbolCodec, + StringCodec, + BytestringCodec, + ListCodec, + AnyCodec, + makeRecordCodecFromDefinition, + makeRecordCodec, + makeRecordUnionCodec, + makeUnionCodec, } from '../codec.js'; import { DescImportObject, @@ -22,53 +21,65 @@ import { DescHandoffReceive, } from './descriptors.js'; +/** @typedef {import('../codec.js').SyrupCodec} SyrupCodec */ +/** @typedef {import('../codec.js').SyrupRecordCodec} SyrupRecordCodec */ + // OCapN Passable Atoms -const UndefinedCodec = new CustomRecordCodec('void', { - readBody(syrupReader) { +const UndefinedCodec = makeRecordCodec( + 'void', + // readBody + syrupReader => { return undefined; }, - writeBody(value, syrupWriter) { + // writeBody + (value, syrupWriter) => { // body is empty }, -}); +); -const NullCodec = new CustomRecordCodec('null', { - readBody(syrupReader) { +const NullCodec = makeRecordCodec( + 'null', + // readBody + syrupReader => { return null; }, - writeBody(value, syrupWriter) { + // writeBody + (value, syrupWriter) => { // body is empty }, -}); +); const AtomCodecs = { undefined: UndefinedCodec, null: NullCodec, - boolean: SyrupBooleanCodec, - integer: SyrupIntegerCodec, - float64: SyrupDoubleCodec, - string: SyrupStringCodec, + boolean: BooleanCodec, + integer: IntegerCodec, + float64: DoubleCodec, + string: StringCodec, // TODO: Pass Invariant Equality - symbol: SyrupSymbolCodec, + symbol: SymbolCodec, // TODO: Pass Invariant Equality - byteArray: SyrupBytestringCodec, + byteArray: BytestringCodec, }; // OCapN Passable Containers // TODO: dictionary but with only string keys -export const OCapNStructCodec = new SimpleSyrupCodecType({ +/** @type {SyrupCodec} */ +export const OCapNStructCodec = { read(syrupReader) { throw Error('OCapNStructCodec: read must be implemented'); }, write(value, syrupWriter) { throw Error('OCapNStructCodec: write must be implemented'); }, -}); +}; -const OCapNTaggedCodec = new CustomRecordCodec('desc:tagged', { - readBody(syrupReader) { +const OCapNTaggedCodec = makeRecordCodec( + 'desc:tagged', + // readBody + syrupReader => { const tagName = syrupReader.readSymbolAsString(); // @ts-expect-error any type const value = syrupReader.readOfType('any'); @@ -79,26 +90,27 @@ const OCapNTaggedCodec = new CustomRecordCodec('desc:tagged', { value, }; }, - writeBody(value, syrupWriter) { + // writeBody + (value, syrupWriter) => { syrupWriter.writeSymbol(value.tagName); value.value.write(syrupWriter); }, -}); +); const ContainerCodecs = { - list: SyrupListCodec, + list: ListCodec, struct: OCapNStructCodec, tagged: OCapNTaggedCodec, }; // OCapN Reference (Capability) -const OCapNTargetCodec = new RecordUnionCodec({ +const OCapNTargetCodec = makeRecordUnionCodec({ DescExport, DescImportObject, }); -const OCapNPromiseCodec = new RecordUnionCodec({ +const OCapNPromiseCodec = makeRecordUnionCodec({ DescImportPromise, DescAnswer, }); @@ -110,11 +122,12 @@ const OCapNReferenceCodecs = { // OCapN Error -const OCapNErrorCodec = new SyrupStructuredRecordCodecType('desc:error', [ +const OCapNErrorCodec = makeRecordCodecFromDefinition('desc:error', [ ['message', 'string'], ]); -const OCapNPassableCodecs = { +// provided for completeness +const _OCapNPassableCodecs = { ...AtomCodecs, ...ContainerCodecs, ...OCapNReferenceCodecs, @@ -122,7 +135,7 @@ const OCapNPassableCodecs = { }; // all record based passables -const OCapNPassableRecordUnionCodec = new RecordUnionCodec({ +const OCapNPassableRecordUnionCodec = makeRecordUnionCodec({ UndefinedCodec, NullCodec, OCapNTaggedCodec, @@ -137,8 +150,9 @@ const OCapNPassableRecordUnionCodec = new RecordUnionCodec({ OCapNErrorCodec, }); -export const OCapNPassableUnionCodec = new CustomUnionCodecType({ - selectCodecForRead(syrupReader) { +export const OCapNPassableUnionCodec = makeUnionCodec( + // selectCodecForRead + syrupReader => { const typeHint = syrupReader.peekTypeHint(); switch (typeHint) { case 'boolean': @@ -148,7 +162,7 @@ export const OCapNPassableUnionCodec = new CustomUnionCodecType({ case 'number-prefix': // can be string, bytestring, symbol, integer // We'll return the any codec in place of those - return SyrupAnyCodec; + return AnyCodec; case 'list': return ContainerCodecs.list; case 'record': @@ -160,7 +174,8 @@ export const OCapNPassableUnionCodec = new CustomUnionCodecType({ throw Error(`Unknown type hint: ${typeHint}`); } }, - selectCodecForWrite(value) { + // selectCodecForWrite + value => { if (value === undefined) { return AtomCodecs.undefined; } @@ -203,4 +218,4 @@ export const OCapNPassableUnionCodec = new CustomUnionCodecType({ } throw Error(`Unknown value: ${value}`); }, -}); +); diff --git a/packages/syrup/test/codec.test.js b/packages/syrup/test/codec.test.js index 8fa0c5708e..7f1ece8569 100644 --- a/packages/syrup/test/codec.test.js +++ b/packages/syrup/test/codec.test.js @@ -4,9 +4,9 @@ import test from 'ava'; import { makeSyrupReader } from '../src/decode.js'; import { makeSyrupWriter } from '../src/encode.js'; import { - RecordUnionCodec, - SyrupStringCodec, - SyrupStructuredRecordCodecType, + makeRecordUnionCodec, + makeRecordCodecFromDefinition, + StringCodec, } from '../src/codec.js'; const testCodecBidirectionally = (t, codec, value) => { @@ -19,13 +19,13 @@ const testCodecBidirectionally = (t, codec, value) => { }; test('simple string codec', t => { - const codec = SyrupStringCodec; + const codec = StringCodec; const value = 'hello'; testCodecBidirectionally(t, codec, value); }); test('basic record codec cases', t => { - const codec = new SyrupStructuredRecordCodecType('test', [ + const codec = makeRecordCodecFromDefinition('test', [ ['field1', 'string'], ['field2', 'integer'], ]); @@ -38,12 +38,12 @@ test('basic record codec cases', t => { }); test('record union codec', t => { - const codec = new RecordUnionCodec({ - testA: new SyrupStructuredRecordCodecType('testA', [ + const codec = makeRecordUnionCodec({ + testA: makeRecordCodecFromDefinition('testA', [ ['field1', 'string'], ['field2', 'integer'], ]), - testB: new SyrupStructuredRecordCodecType('testB', [ + testB: makeRecordCodecFromDefinition('testB', [ ['field1', 'string'], ['field2', 'integer'], ]), From 6ef4b6d3264e59aa97ba1f6dcd4cb49ea357ecb1 Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 11 Apr 2025 11:31:40 -1000 Subject: [PATCH 21/31] refactor(syrup): define passable with makeTypeHintUnionCodec --- packages/syrup/src/codec.js | 40 ++++++++++++++ packages/syrup/src/decode.js | 4 +- packages/syrup/src/ocapn/passable.js | 81 ++++++++++------------------ 3 files changed, 71 insertions(+), 54 deletions(-) diff --git a/packages/syrup/src/codec.js b/packages/syrup/src/codec.js index ccd54a2b2d..90fb89708a 100644 --- a/packages/syrup/src/codec.js +++ b/packages/syrup/src/codec.js @@ -3,6 +3,7 @@ const quote = JSON.stringify; /** @typedef {import('./decode.js').SyrupReader} SyrupReader */ /** @typedef {import('./encode.js').SyrupWriter} SyrupWriter */ +/** @typedef {import('./decode.js').TypeHintTypes} TypeHintTypes */ /** * @typedef {object} SyrupCodec @@ -201,6 +202,45 @@ export const makeUnionCodec = (selectCodecForRead, selectCodecForWrite) => { return freeze({ read, write }); }; +/** @typedef {'undefined'|'object'|'boolean'|'number'|'string'|'symbol'|'bigint'} JavascriptTypeofValueTypes */ +/** @typedef {Partial>} TypeHintUnionReadTable */ +/** @typedef {Partial SyrupCodec)>>} TypeHintUnionWriteTable */ + +/** + * @param {TypeHintUnionReadTable} readTable + * @param {TypeHintUnionWriteTable} writeTable + * @returns {SyrupCodec} + */ +export const makeTypeHintUnionCodec = (readTable, writeTable) => { + return makeUnionCodec( + syrupReader => { + const typeHint = syrupReader.peekTypeHint(); + const codec = readTable[typeHint]; + if (!codec) { + const expected = Object.keys(readTable).join(', '); + throw Error( + `Unexpected type hint ${quote(typeHint)}, expected one of ${expected}`, + ); + } + return codec; + }, + value => { + const codecOrGetter = writeTable[typeof value]; + const codec = + typeof codecOrGetter === 'function' + ? codecOrGetter(value) + : codecOrGetter; + if (!codec) { + const expected = Object.keys(writeTable).join(', '); + throw Error( + `Unexpected value type ${quote(typeof value)}, expected one of ${expected}`, + ); + } + return codec; + }, + ); +}; + /** * @typedef {SyrupCodec & { * supports: (label: string) => boolean; diff --git a/packages/syrup/src/decode.js b/packages/syrup/src/decode.js index 152366b62d..46b99b54f3 100644 --- a/packages/syrup/src/decode.js +++ b/packages/syrup/src/decode.js @@ -363,10 +363,12 @@ function readDictionary(bufferReader, name) { return readDictionaryBody(bufferReader, name); } +/** @typedef {'float64' | 'number-prefix' | 'list' | 'set' | 'dictionary' | 'record' | 'boolean'} TypeHintTypes */ + /** * @param {BufferReader} bufferReader * @param {string} name - * @returns {'float64' | 'number-prefix' | 'list' | 'set' | 'dictionary' | 'record' | 'boolean'} + * @returns {TypeHintTypes} */ export function peekTypeHint(bufferReader, name) { const cc = bufferReader.peekByte(); diff --git a/packages/syrup/src/ocapn/passable.js b/packages/syrup/src/ocapn/passable.js index bfeea13409..a6cc8988ff 100644 --- a/packages/syrup/src/ocapn/passable.js +++ b/packages/syrup/src/ocapn/passable.js @@ -10,7 +10,7 @@ import { makeRecordCodecFromDefinition, makeRecordCodec, makeRecordUnionCodec, - makeUnionCodec, + makeTypeHintUnionCodec, } from '../codec.js'; import { DescImportObject, @@ -150,57 +150,33 @@ const OCapNPassableRecordUnionCodec = makeRecordUnionCodec({ OCapNErrorCodec, }); -export const OCapNPassableUnionCodec = makeUnionCodec( - // selectCodecForRead - syrupReader => { - const typeHint = syrupReader.peekTypeHint(); - switch (typeHint) { - case 'boolean': - return AtomCodecs.boolean; - case 'float64': - return AtomCodecs.float64; - case 'number-prefix': - // can be string, bytestring, symbol, integer - // We'll return the any codec in place of those - return AnyCodec; - case 'list': - return ContainerCodecs.list; - case 'record': - // many possible matches, the union codec will select the correct one - return OCapNPassableRecordUnionCodec; - case 'dictionary': - return ContainerCodecs.struct; - default: - throw Error(`Unknown type hint: ${typeHint}`); - } +export const OCapNPassableUnionCodec = makeTypeHintUnionCodec( + // syrup type hint -> codec + { + boolean: AtomCodecs.boolean, + float64: AtomCodecs.float64, + // "number-prefix" can be string, bytestring, symbol, integer + // TODO: should restrict further to only the types that can be passed + 'number-prefix': AnyCodec, + list: ContainerCodecs.list, + record: OCapNPassableRecordUnionCodec, + dictionary: ContainerCodecs.struct, }, - // selectCodecForWrite - value => { - if (value === undefined) { - return AtomCodecs.undefined; - } - if (value === null) { - return AtomCodecs.null; - } - if (typeof value === 'boolean') { - return AtomCodecs.boolean; - } - if (typeof value === 'number') { - return AtomCodecs.float64; - } - if (typeof value === 'string') { - return AtomCodecs.string; - } - if (typeof value === 'symbol') { - return AtomCodecs.symbol; - } - if (typeof value === 'bigint') { - return AtomCodecs.integer; - } - if (value instanceof Uint8Array) { - return AtomCodecs.byteArray; - } - if (typeof value === 'object') { + // javascript typeof value -> codec + { + undefined: AtomCodecs.undefined, + boolean: AtomCodecs.boolean, + number: AtomCodecs.float64, + string: AtomCodecs.string, + symbol: AtomCodecs.symbol, + bigint: AtomCodecs.integer, + object: value => { + if (value === null) { + return AtomCodecs.null; + } + if (value instanceof Uint8Array) { + return AtomCodecs.byteArray; + } if (Array.isArray(value)) { return ContainerCodecs.list; } @@ -215,7 +191,6 @@ export const OCapNPassableUnionCodec = makeUnionCodec( } // TODO: need to distinguish OCapNReferenceCodecs and OCapNErrorCodec return ContainerCodecs.struct; - } - throw Error(`Unknown value: ${value}`); + }, }, ); From 610b9eeeb4b7f8aefd7f69b8fbd0745f3c564f5b Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 11 Apr 2025 12:13:46 -1000 Subject: [PATCH 22/31] chore(syrup): lint fix --- packages/syrup/src/decode.js | 23 ++++++------ packages/syrup/src/encode.js | 50 ++++++++++++++++++--------- packages/syrup/src/ocapn/passable.js | 1 + packages/syrup/test/_ocapn.js | 5 +-- packages/syrup/test/reader.test.js | Bin 5354 -> 5560 bytes 5 files changed, 47 insertions(+), 32 deletions(-) diff --git a/packages/syrup/src/decode.js b/packages/syrup/src/decode.js index 46b99b54f3..11624d87e2 100644 --- a/packages/syrup/src/decode.js +++ b/packages/syrup/src/decode.js @@ -7,7 +7,7 @@ import { SyrupSymbolFor } from './symbol.js'; const MINUS = '-'.charCodeAt(0); const PLUS = '+'.charCodeAt(0); const ZERO = '0'.charCodeAt(0); -const ONE = '1'.charCodeAt(0); +// const ONE = '1'.charCodeAt(0); const NINE = '9'.charCodeAt(0); const LIST_START = '['.charCodeAt(0); const LIST_END = ']'.charCodeAt(0); @@ -95,6 +95,7 @@ function readTypeAndMaybeValue(bufferReader, name) { return { type: 'boolean', value: false }; } if (cc === DOUBLE) { + // eslint-disable-next-line no-use-before-define const value = readFloat64Body(bufferReader, name); return { type: 'float64', value }; } @@ -246,7 +247,7 @@ function readListBody(bufferReader, name) { bufferReader.skip(1); return list; } - + // eslint-disable-next-line no-use-before-define list.push(readAny(bufferReader, name)); } } @@ -256,7 +257,8 @@ function readListBody(bufferReader, name) { * @param {string} name * @returns {any[]} */ -function readList(bufferReader, name) { +// eslint-disable-next-line no-underscore-dangle +function _readList(bufferReader, name) { const cc = bufferReader.readByte(); if (cc !== LIST_START) { throw Error( @@ -338,6 +340,7 @@ function readDictionaryBody(bufferReader, name) { priorKeyBytes = newKeyBytes; // Read value and add to dictionary + // eslint-disable-next-line no-use-before-define const value = readAny(bufferReader, name); defineProperty(dict, newKey, { value, @@ -352,7 +355,8 @@ function readDictionaryBody(bufferReader, name) { * @param {BufferReader} bufferReader * @param {string} name */ -function readDictionary(bufferReader, name) { +// eslint-disable-next-line no-underscore-dangle +function _readDictionary(bufferReader, name) { const start = bufferReader.index; const cc = bufferReader.readByte(); if (cc !== DICT_START) { @@ -428,12 +432,7 @@ function readAny(bufferReader, name) { return value; } -class SyrupReaderStackEntry { - constructor(type, start) { - this.type = type; - this.start = start; - } -} +/** @typedef {{type: string, start: number}} SyrupReaderStackEntry */ export class SyrupReader { /** @@ -468,9 +467,7 @@ export class SyrupReader { * @param {string} type */ #pushStackEntry(type) { - this.state.stack.push( - new SyrupReaderStackEntry(type, this.bufferReader.index), - ); + this.state.stack.push({ type, start: this.bufferReader.index }); } /** diff --git a/packages/syrup/src/encode.js b/packages/syrup/src/encode.js index 21d91effb5..f98f7566bb 100644 --- a/packages/syrup/src/encode.js +++ b/packages/syrup/src/encode.js @@ -9,15 +9,26 @@ const { ownKeys } = Reflect; const defaultCapacity = 256; +// const MINUS = '-'.charCodeAt(0); +// const PLUS = '+'.charCodeAt(0); +// const ZERO = '0'.charCodeAt(0); +// const ONE = '1'.charCodeAt(0); +// const NINE = '9'.charCodeAt(0); const LIST_START = '['.charCodeAt(0); const LIST_END = ']'.charCodeAt(0); const DICT_START = '{'.charCodeAt(0); const DICT_END = '}'.charCodeAt(0); +// const SET_START = '#'.charCodeAt(0); +// const SET_END = '$'.charCodeAt(0); +// const BYTES_START = ':'.charCodeAt(0); +// const STRING_START = '"'.charCodeAt(0); +// const SYMBOL_START = "'".charCodeAt(0); const RECORD_START = '<'.charCodeAt(0); const RECORD_END = '>'.charCodeAt(0); -const DOUBLE = 'D'.charCodeAt(0); const TRUE = 't'.charCodeAt(0); const FALSE = 'f'.charCodeAt(0); +// const SINGLE = 'F'.charCodeAt(0); +const DOUBLE = 'D'.charCodeAt(0); const NAN64 = freeze([0x7f, 0xf8, 0, 0, 0, 0, 0, 0]); @@ -63,6 +74,26 @@ function writeBytestring(bufferWriter, value) { writeStringlike(bufferWriter, value, ':'); } +/** + * @param {import('./buffer-writer.js').BufferWriter} bufferWriter + * @param {string | symbol} key + * @param {Array} path + */ +function writeDictionaryKey(bufferWriter, key, path) { + if (typeof key === 'string') { + writeString(bufferWriter, key); + return; + } + if (typeof key === 'symbol') { + const syrupSymbol = getSyrupSymbolName(key); + writeSymbol(bufferWriter, syrupSymbol); + return; + } + throw TypeError( + `Dictionary keys must be strings or symbols, got ${typeof key} at ${path.join('/')}`, + ); +} + /** * @param {import('./buffer-writer.js').BufferWriter} bufferWriter * @param {Record} record @@ -73,26 +104,11 @@ function writeDictionary(bufferWriter, record, path) { const keys = []; const keyBytes = []; - const writeKey = (bufferWriter, key) => { - if (typeof key === 'string') { - writeString(bufferWriter, key); - return; - } - if (typeof key === 'symbol') { - const syrupSymbol = getSyrupSymbolName(key); - writeSymbol(bufferWriter, syrupSymbol); - return; - } - throw TypeError( - `Dictionary keys must be strings or symbols, got ${typeof key} at ${path.join('/')}`, - ); - }; - // We need to sort the keys, so we write them to a scratch buffer first const scratchWriter = new BufferWriter(); for (const key of ownKeys(record)) { const start = scratchWriter.length; - writeKey(scratchWriter, key); + writeDictionaryKey(scratchWriter, key, path); const end = scratchWriter.length; keys.push(key); diff --git a/packages/syrup/src/ocapn/passable.js b/packages/syrup/src/ocapn/passable.js index a6cc8988ff..2fc7a36081 100644 --- a/packages/syrup/src/ocapn/passable.js +++ b/packages/syrup/src/ocapn/passable.js @@ -127,6 +127,7 @@ const OCapNErrorCodec = makeRecordCodecFromDefinition('desc:error', [ ]); // provided for completeness +// eslint-disable-next-line no-underscore-dangle const _OCapNPassableCodecs = { ...AtomCodecs, ...ContainerCodecs, diff --git a/packages/syrup/test/_ocapn.js b/packages/syrup/test/_ocapn.js index 71efa71f4f..5fb3d7ce82 100644 --- a/packages/syrup/test/_ocapn.js +++ b/packages/syrup/test/_ocapn.js @@ -2,6 +2,7 @@ const sym = s => `${s.length}'${s}`; const str = s => `${s.length}"${s}`; const bts = s => `${s.length}:${s}`; const bool = b => (b ? 't' : 'f'); +// eslint-disable-next-line @endo/restrict-comparison-operands const int = i => `${Math.floor(Math.abs(i))}${i < 0 ? '-' : '+'}`; const list = items => `[${items.join('')}]`; const makeNode = (transport, address, hints) => { @@ -57,8 +58,8 @@ const makeHandoffReceive = ( return `<${sym('desc:handoff-receive')}${bts(recieverSession)}${bts(recieverSide)}${int(handoffCount)}${signedGiveEnvelope}>`; }; -const strToUint8Array = str => { - return new Uint8Array(str.split('').map(c => c.charCodeAt(0))); +const strToUint8Array = string => { + return new Uint8Array(string.split('').map(c => c.charCodeAt(0))); }; // I made up these syrup values by hand, they may be wrong, sorry. diff --git a/packages/syrup/test/reader.test.js b/packages/syrup/test/reader.test.js index b1f1dd3e398c55e605119ac2488328b8b069c263..6f69cf0c7b53264efb9211ecb066d4658ac96a54 100644 GIT binary patch delta 236 zcmaE*xkFn)UthtYq*ymOBQ-gjOJ7SNJtsdYF-O6vG%YQ)NI^?~BF_s0pqkX;oXoru z-IUDY#H5^5-MrL_5?!E3szP4AZfRahYEf}=eo?A!N@8AmPU^ Date: Fri, 11 Apr 2025 13:36:47 -1000 Subject: [PATCH 23/31] fix(syrup): improve ocapn subtype handling --- packages/syrup/src/ocapn/descriptors.js | 11 +++---- packages/syrup/src/ocapn/operations.js | 31 +++++++------------ packages/syrup/src/ocapn/subtypes.js | 40 +++++++++++++++++++++++++ packages/syrup/test/ocapn.test.js | 17 +++++++++++ 4 files changed, 74 insertions(+), 25 deletions(-) create mode 100644 packages/syrup/src/ocapn/subtypes.js diff --git a/packages/syrup/src/ocapn/descriptors.js b/packages/syrup/src/ocapn/descriptors.js index da58c66d9a..c93f153973 100644 --- a/packages/syrup/src/ocapn/descriptors.js +++ b/packages/syrup/src/ocapn/descriptors.js @@ -2,6 +2,7 @@ import { makeRecordCodecFromDefinition, makeRecordUnionCodec, } from '../codec.js'; +import { PositiveIntegerCodec } from './subtypes.js'; import { OCapNNode, OCapNPublicKey, OCapNSignature } from './components.js'; /* @@ -11,20 +12,20 @@ import { OCapNNode, OCapNPublicKey, OCapNSignature } from './components.js'; export const DescImportObject = makeRecordCodecFromDefinition( 'desc:import-object', - [['position', 'integer']], + [['position', PositiveIntegerCodec]], ); export const DescImportPromise = makeRecordCodecFromDefinition( 'desc:import-promise', - [['position', 'integer']], + [['position', PositiveIntegerCodec]], ); export const DescExport = makeRecordCodecFromDefinition('desc:export', [ - ['position', 'integer'], + ['position', PositiveIntegerCodec], ]); export const DescAnswer = makeRecordCodecFromDefinition('desc:answer', [ - ['position', 'integer'], + ['position', PositiveIntegerCodec], ]); export const DescHandoffGive = makeRecordCodecFromDefinition( @@ -51,7 +52,7 @@ export const DescHandoffReceive = makeRecordCodecFromDefinition( [ ['receivingSession', 'bytestring'], ['receivingSide', 'bytestring'], - ['handoffCount', 'integer'], + ['handoffCount', PositiveIntegerCodec], ['signedGive', DescSigGiveEnvelope], ], ); diff --git a/packages/syrup/src/ocapn/operations.js b/packages/syrup/src/ocapn/operations.js index b05863bcff..8e4bd8641e 100644 --- a/packages/syrup/src/ocapn/operations.js +++ b/packages/syrup/src/ocapn/operations.js @@ -1,7 +1,9 @@ import { makeRecordUnionCodec, makeRecordCodecFromDefinition, + makeTypeHintUnionCodec, } from '../codec.js'; +import { PositiveIntegerCodec, FalseCodec } from './subtypes.js'; import { OCapNNode, OCapNPublicKey, OCapNSignature } from './components.js'; import { OCapNPassableUnionCodec } from './passable.js'; import { @@ -72,28 +74,17 @@ const OpDeliverOnly = makeRecordCodecFromDefinition('op:deliver-only', [ ['args', OpDeliverArgsCodec], ]); -const OpDeliverAnswerCodec = freeze({ - read: syrupReader => { - const typeHint = syrupReader.peekTypeHint(); - if (typeHint === 'number-prefix') { - // should be an integer - return syrupReader.readInteger(); - } - if (typeHint === 'boolean') { - return syrupReader.readBoolean(); - } - throw Error(`Expected integer or boolean, got ${typeHint}`); +// The OpDeliver answer is either a positive integer or false +const OpDeliverAnswerCodec = makeTypeHintUnionCodec( + { + 'number-prefix': PositiveIntegerCodec, + boolean: FalseCodec, }, - write: (value, syrupWriter) => { - if (typeof value === 'bigint') { - syrupWriter.writeInteger(value); - } else if (typeof value === 'boolean') { - syrupWriter.writeBoolean(value); - } else { - throw Error(`Expected integer or boolean, got ${typeof value}`); - } + { + bigint: PositiveIntegerCodec, + boolean: FalseCodec, }, -}); +); const OpDeliver = makeRecordCodecFromDefinition('op:deliver', [ ['to', OCapNDeliverTargetCodec], diff --git a/packages/syrup/src/ocapn/subtypes.js b/packages/syrup/src/ocapn/subtypes.js new file mode 100644 index 0000000000..88df853559 --- /dev/null +++ b/packages/syrup/src/ocapn/subtypes.js @@ -0,0 +1,40 @@ +const { freeze } = Object; + +/** @typedef {import('../codec.js').SyrupCodec} SyrupCodec */ + +/** @type {SyrupCodec} */ +export const PositiveIntegerCodec = freeze({ + write: (value, syrupWriter) => { + if (typeof value !== 'bigint') { + throw Error('PositiveIntegerCodec: value must be a bigint'); + } + if (value < 0n) { + throw Error('PositiveIntegerCodec: value must be positive'); + } + syrupWriter.writeInteger(value); + }, + read: syrupReader => { + const value = syrupReader.readInteger(); + if (value < 0n) { + throw Error('PositiveIntegerCodec: value must be positive'); + } + return value; + }, +}); + +/** @type {SyrupCodec} */ +export const FalseCodec = freeze({ + write: (value, syrupWriter) => { + if (value) { + throw Error('FalseCodec: value must be false'); + } + syrupWriter.writeBoolean(value); + }, + read: syrupReader => { + const value = syrupReader.readBoolean(); + if (value) { + throw Error('FalseCodec: value must be false'); + } + return value; + }, +}); diff --git a/packages/syrup/test/ocapn.test.js b/packages/syrup/test/ocapn.test.js index e5776a1935..7818757583 100644 --- a/packages/syrup/test/ocapn.test.js +++ b/packages/syrup/test/ocapn.test.js @@ -71,3 +71,20 @@ test('error on unknown record type in passable', t => { { message: 'Unexpected record type: "unknown-record-type"' }, ); }); + +test('descriptor fails with negative integer', t => { + const codec = OCapNDescriptorUnionCodec; + const syrup = `<${sym('desc:import-object')}1-}>`; + const syrupBytes = textEncoder.encode(syrup); + const syrupReader = makeSyrupReader(syrupBytes, { + name: 'import-object with negative integer', + }); + t.throws( + () => { + codec.read(syrupReader); + }, + { + message: 'PositiveIntegerCodec: value must be positive', + }, + ); +}); From a408cb1c4ece438d7b852f3deac0f945ad5763b0 Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 11 Apr 2025 12:42:57 -1000 Subject: [PATCH 24/31] fix(syrup): rename symbol to selector --- packages/syrup/src/codec.js | 15 ++++---- packages/syrup/src/decode.js | 51 +++++++++++++------------ packages/syrup/src/encode.js | 22 +++++------ packages/syrup/src/ocapn/components.js | 14 +++---- packages/syrup/src/ocapn/operations.js | 15 +++++++- packages/syrup/src/ocapn/passable.js | 12 +++--- packages/syrup/src/selector.js | 23 +++++++++++ packages/syrup/src/symbol.js | 23 ----------- packages/syrup/test/_table.js | 8 ++-- packages/syrup/test/decode.test.js | 4 +- packages/syrup/test/reader.test.js | Bin 5560 -> 5572 bytes 11 files changed, 101 insertions(+), 86 deletions(-) create mode 100644 packages/syrup/src/selector.js delete mode 100644 packages/syrup/src/symbol.js diff --git a/packages/syrup/src/codec.js b/packages/syrup/src/codec.js index 90fb89708a..98637daced 100644 --- a/packages/syrup/src/codec.js +++ b/packages/syrup/src/codec.js @@ -3,6 +3,7 @@ const quote = JSON.stringify; /** @typedef {import('./decode.js').SyrupReader} SyrupReader */ /** @typedef {import('./encode.js').SyrupWriter} SyrupWriter */ +/** @typedef {import('./decode.js').SyrupType} SyrupType */ /** @typedef {import('./decode.js').TypeHintTypes} TypeHintTypes */ /** @@ -12,9 +13,9 @@ const quote = JSON.stringify; */ /** @type {SyrupCodec} */ -export const SymbolCodec = freeze({ - write: (value, syrupWriter) => syrupWriter.writeSymbol(value), - read: syrupReader => syrupReader.readSymbolAsString(), +export const SelectorCodec = freeze({ + write: (value, syrupWriter) => syrupWriter.writeSelector(value), + read: syrupReader => syrupReader.readSelectorAsString(), }); /** @type {SyrupCodec} */ @@ -99,7 +100,7 @@ export const makeRecordCodec = (label, readBody, writeBody) => { */ const read = syrupReader => { syrupReader.enterRecord(); - const actualLabel = syrupReader.readSymbolAsString(); + const actualLabel = syrupReader.readSelectorAsString(); if (actualLabel !== label) { throw Error( `RecordCodec: Expected label ${quote(label)}, got ${quote(actualLabel)}`, @@ -115,7 +116,7 @@ export const makeRecordCodec = (label, readBody, writeBody) => { */ const write = (value, syrupWriter) => { syrupWriter.enterRecord(); - syrupWriter.writeSymbol(value.type); + syrupWriter.writeSelector(value.type); writeBody(value, syrupWriter); syrupWriter.exitRecord(); }; @@ -128,7 +129,7 @@ export const makeRecordCodec = (label, readBody, writeBody) => { }); }; -/** @typedef {Array<[string, string | SyrupCodec]>} SyrupRecordDefinition */ +/** @typedef {Array<[string, SyrupType | SyrupCodec]>} SyrupRecordDefinition */ /** * @param {string} label @@ -262,7 +263,7 @@ export const makeRecordUnionCodec = recordTypes => { }; const read = syrupReader => { syrupReader.enterRecord(); - const label = syrupReader.readSymbolAsString(); + const label = syrupReader.readSelectorAsString(); const recordCodec = recordTable[label]; if (!recordCodec) { throw Error(`Unexpected record type: ${quote(label)}`); diff --git a/packages/syrup/src/decode.js b/packages/syrup/src/decode.js index 11624d87e2..27393734bd 100644 --- a/packages/syrup/src/decode.js +++ b/packages/syrup/src/decode.js @@ -2,7 +2,7 @@ import { BufferReader } from './buffer-reader.js'; import { compareByteArrays } from './compare.js'; -import { SyrupSymbolFor } from './symbol.js'; +import { SyrupSelectorFor } from './selector.js'; const MINUS = '-'.charCodeAt(0); const PLUS = '+'.charCodeAt(0); @@ -17,7 +17,7 @@ const SET_START = '#'.charCodeAt(0); const SET_END = '$'.charCodeAt(0); const BYTES_START = ':'.charCodeAt(0); const STRING_START = '"'.charCodeAt(0); -const SYMBOL_START = "'".charCodeAt(0); +const SELECTOR_START = "'".charCodeAt(0); const RECORD_START = '<'.charCodeAt(0); const RECORD_END = '>'.charCodeAt(0); const TRUE = 't'.charCodeAt(0); @@ -53,9 +53,12 @@ function readBoolean(bufferReader, name) { ); } +/** @typedef {'boolean' | 'float64' | 'integer' | 'bytestring' | 'string' | 'selector'} SyrupAtomType */ +/** @typedef {'list' | 'set' | 'dictionary' | 'record'} SyrupStructuredType */ +/** @typedef {SyrupAtomType | SyrupStructuredType} SyrupType */ + // Structure types, no value provided -/** @typedef {'list' | 'set' | 'dictionary' | 'record'} StructuredType */ -/** @typedef {{type: StructuredType, value: null}} ReadTypeStructuredResult */ +/** @typedef {{type: SyrupStructuredType, value: null}} ReadTypeStructuredResult */ // Simple Atom types, value is read /** @typedef {{type: 'boolean', value: boolean}} ReadTypeBooleanResult */ /** @typedef {{type: 'float64', value: number}} ReadTypeFloat64Result */ @@ -63,8 +66,8 @@ function readBoolean(bufferReader, name) { /** @typedef {{type: 'integer', value: bigint}} ReadTypeIntegerResult */ /** @typedef {{type: 'bytestring', value: Uint8Array}} ReadTypeBytestringResult */ /** @typedef {{type: 'string', value: string}} ReadTypeStringResult */ -/** @typedef {{type: 'symbol', value: string}} ReadTypeSymbolResult */ -/** @typedef {ReadTypeBooleanResult | ReadTypeFloat64Result | ReadTypeIntegerResult | ReadTypeBytestringResult | ReadTypeStringResult | ReadTypeSymbolResult} ReadTypeAtomResult */ +/** @typedef {{type: 'selector', value: string}} ReadTypeSelectorResult */ +/** @typedef {ReadTypeBooleanResult | ReadTypeFloat64Result | ReadTypeIntegerResult | ReadTypeBytestringResult | ReadTypeStringResult | ReadTypeSelectorResult} ReadTypeAtomResult */ /** * @param {BufferReader} bufferReader * @param {string} name @@ -136,10 +139,10 @@ function readTypeAndMaybeValue(bufferReader, name) { const valueBytes = bufferReader.read(number); return { type: 'string', value: textDecoder.decode(valueBytes) }; } - if (typeByte === SYMBOL_START) { + if (typeByte === SELECTOR_START) { const number = Number.parseInt(numberString, 10); const valueBytes = bufferReader.read(number); - return { type: 'symbol', value: textDecoder.decode(valueBytes) }; + return { type: 'selector', value: textDecoder.decode(valueBytes) }; } throw Error( `Unexpected character ${quote(toChar(typeByte))}, at index ${bufferReader.index} of ${name}`, @@ -148,7 +151,7 @@ function readTypeAndMaybeValue(bufferReader, name) { /** * @param {BufferReader} bufferReader - * @param {'boolean' | 'integer' | 'float64' | 'string' | 'symbol' | 'bytestring'} expectedType + * @param {'boolean' | 'integer' | 'float64' | 'string' | 'selector' | 'bytestring'} expectedType * @param {string} name * @returns {any} */ @@ -184,8 +187,8 @@ function readString(bufferReader, name) { * @param {string} name * @returns {string} */ -function readSymbolAsString(bufferReader, name) { - return readAndAssertType(bufferReader, 'symbol', name); +function readSelectorAsString(bufferReader, name) { + return readAndAssertType(bufferReader, 'selector', name); } /** @@ -271,21 +274,21 @@ function _readList(bufferReader, name) { /** * @param {BufferReader} bufferReader * @param {string} name - * @returns {{value: any, type: 'string' | 'symbol', bytes: Uint8Array}} + * @returns {{value: any, type: 'string' | 'selector', bytes: Uint8Array}} */ function readDictionaryKey(bufferReader, name) { const start = bufferReader.index; const { value, type } = readTypeAndMaybeValue(bufferReader, name); - if (type === 'string' || type === 'symbol') { + if (type === 'string' || type === 'selector') { const end = bufferReader.index; const bytes = bufferReader.bytesAt(start, end - start); - if (type === 'symbol') { - return { value: SyrupSymbolFor(value), type, bytes }; + if (type === 'selector') { + return { value: SyrupSelectorFor(value), type, bytes }; } return { value, type, bytes }; } throw Error( - `Unexpected type ${quote(type)}, Syrup dictionary keys must be strings or symbols at index ${start} of ${name}`, + `Unexpected type ${quote(type)}, Syrup dictionary keys must be strings or selectors at index ${start} of ${name}`, ); } @@ -424,9 +427,9 @@ function readAny(bufferReader, name) { throw Error(`readAny for Records is not yet supported.`); } // Atom types, value is already read - // For symbols, we need to convert the string to a symbol - if (type === 'symbol') { - return SyrupSymbolFor(value); + // For selectors, we need to convert the string to a selector + if (type === 'selector') { + return SyrupSelectorFor(value); } return value; @@ -568,8 +571,8 @@ export class SyrupReader { return readBytestring(this.bufferReader, this.name); } - readSymbolAsString() { - return readSymbolAsString(this.bufferReader, this.name); + readSelectorAsString() { + return readSelectorAsString(this.bufferReader, this.name); } readAny() { @@ -577,7 +580,7 @@ export class SyrupReader { } /** - * @param {'boolean' | 'integer' | 'float64' | 'string' | 'bytestring' | 'symbol'} type + * @param {'boolean' | 'integer' | 'float64' | 'string' | 'bytestring' | 'selector'} type * @returns {any} */ readOfType(type) { @@ -592,8 +595,8 @@ export class SyrupReader { return this.readString(); case 'bytestring': return this.readBytestring(); - case 'symbol': - return this.readSymbolAsString(); + case 'selector': + return this.readSelectorAsString(); default: throw Error(`Unexpected type ${type}`); } diff --git a/packages/syrup/src/encode.js b/packages/syrup/src/encode.js index f98f7566bb..2d7a287960 100644 --- a/packages/syrup/src/encode.js +++ b/packages/syrup/src/encode.js @@ -2,7 +2,7 @@ import { BufferWriter } from './buffer-writer.js'; import { compareByteArrays } from './compare.js'; -import { getSyrupSymbolName } from './symbol.js'; +import { getSyrupSelectorName } from './selector.js'; const { freeze } = Object; const { ownKeys } = Reflect; @@ -22,7 +22,7 @@ const DICT_END = '}'.charCodeAt(0); // const SET_END = '$'.charCodeAt(0); // const BYTES_START = ':'.charCodeAt(0); // const STRING_START = '"'.charCodeAt(0); -// const SYMBOL_START = "'".charCodeAt(0); +// const SELECTOR_START = "'".charCodeAt(0); const RECORD_START = '<'.charCodeAt(0); const RECORD_END = '>'.charCodeAt(0); const TRUE = 't'.charCodeAt(0); @@ -61,7 +61,7 @@ function writeString(bufferWriter, value) { * @param {import('./buffer-writer.js').BufferWriter} bufferWriter * @param {string} value */ -function writeSymbol(bufferWriter, value) { +function writeSelector(bufferWriter, value) { const bytes = textEncoder.encode(value); writeStringlike(bufferWriter, bytes, "'"); } @@ -85,8 +85,8 @@ function writeDictionaryKey(bufferWriter, key, path) { return; } if (typeof key === 'symbol') { - const syrupSymbol = getSyrupSymbolName(key); - writeSymbol(bufferWriter, syrupSymbol); + const syrupSelector = getSyrupSelectorName(key); + writeSelector(bufferWriter, syrupSelector); return; } throw TypeError( @@ -202,7 +202,7 @@ function writeBoolean(bufferWriter, value) { */ function writeAny(bufferWriter, value, path, pathSuffix) { if (typeof value === 'symbol') { - writeSymbol(bufferWriter, getSyrupSymbolName(value)); + writeSelector(bufferWriter, getSyrupSelectorName(value)); return; } @@ -256,8 +256,8 @@ export class SyrupWriter { writeAny(this.bufferWriter, value, [], '/'); } - writeSymbol(value) { - writeSymbol(this.bufferWriter, value); + writeSelector(value) { + writeSelector(this.bufferWriter, value); } writeString(value) { @@ -297,13 +297,13 @@ export class SyrupWriter { } /** - * @param {'boolean' | 'integer' | 'float64' | 'string' | 'bytestring' | 'symbol'} type + * @param {'boolean' | 'integer' | 'float64' | 'string' | 'bytestring' | 'selector'} type * @param {any} value */ writeOfType(type, value) { switch (type) { - case 'symbol': - this.writeSymbol(value); + case 'selector': + this.writeSelector(value); break; case 'bytestring': this.writeBytestring(value); diff --git a/packages/syrup/src/ocapn/components.js b/packages/syrup/src/ocapn/components.js index 75b1276128..10feb8e4fc 100644 --- a/packages/syrup/src/ocapn/components.js +++ b/packages/syrup/src/ocapn/components.js @@ -16,7 +16,7 @@ const { freeze } = Object; */ export const makeOCapNSignatureValueComponentCodec = expectedLabel => { const read = syrupReader => { - const label = syrupReader.readSymbolAsString(); + const label = syrupReader.readSelectorAsString(); if (label !== expectedLabel) { throw Error(`Expected label ${expectedLabel}, got ${label}`); } @@ -24,7 +24,7 @@ export const makeOCapNSignatureValueComponentCodec = expectedLabel => { return value; }; const write = (value, syrupWriter) => { - syrupWriter.writeSymbol(expectedLabel); + syrupWriter.writeSelector(expectedLabel); syrupWriter.writeBytestring(value); }; return freeze({ read, write }); @@ -34,13 +34,13 @@ const OCapNSignatureRValue = makeOCapNSignatureValueComponentCodec('r'); const OCapNSignatureSValue = makeOCapNSignatureValueComponentCodec('s'); export const OCapNSignature = makeRecordCodecFromDefinition('sig-val', [ - ['scheme', 'symbol'], + ['scheme', 'selector'], ['r', OCapNSignatureRValue], ['s', OCapNSignatureSValue], ]); export const OCapNNode = makeRecordCodecFromDefinition('ocapn-node', [ - ['transport', 'symbol'], + ['transport', 'selector'], ['address', 'bytestring'], ['hints', 'boolean'], ]); @@ -51,9 +51,9 @@ export const OCapNSturdyRef = makeRecordCodecFromDefinition('ocapn-sturdyref', [ ]); export const OCapNPublicKey = makeRecordCodecFromDefinition('public-key', [ - ['scheme', 'symbol'], - ['curve', 'symbol'], - ['flags', 'symbol'], + ['scheme', 'selector'], + ['curve', 'selector'], + ['flags', 'selector'], ['q', 'bytestring'], ]); diff --git a/packages/syrup/src/ocapn/operations.js b/packages/syrup/src/ocapn/operations.js index 8e4bd8641e..a32992002c 100644 --- a/packages/syrup/src/ocapn/operations.js +++ b/packages/syrup/src/ocapn/operations.js @@ -44,14 +44,21 @@ const OCapNDeliverTargets = { const OCapNDeliverTargetCodec = makeRecordUnionCodec(OCapNDeliverTargets); +/** @typedef {[string, ...any[]]} OpDeliverArgs */ + // Used by the deliver and deliver-only operations // First arg is method name, rest are Passables const OpDeliverArgsCodec = freeze({ + /** + * @param {import('../decode.js').SyrupReader} syrupReader + * @returns {OpDeliverArgs} + */ read: syrupReader => { syrupReader.enterList(); + /** @type {OpDeliverArgs} */ const result = [ // method name - syrupReader.readSymbolAsString(), + syrupReader.readSelectorAsString(), ]; while (!syrupReader.peekListEnd()) { result.push(OCapNPassableUnionCodec.read(syrupReader)); @@ -59,9 +66,13 @@ const OpDeliverArgsCodec = freeze({ syrupReader.exitList(); return result; }, + /** + * @param {OpDeliverArgs} args + * @param {import('../encode.js').SyrupWriter} syrupWriter + */ write: ([methodName, ...args], syrupWriter) => { syrupWriter.enterList(); - syrupWriter.writeSymbol(methodName); + syrupWriter.writeSelector(methodName); for (const arg of args) { OCapNPassableUnionCodec.write(arg, syrupWriter); } diff --git a/packages/syrup/src/ocapn/passable.js b/packages/syrup/src/ocapn/passable.js index 2fc7a36081..68f14e1616 100644 --- a/packages/syrup/src/ocapn/passable.js +++ b/packages/syrup/src/ocapn/passable.js @@ -2,7 +2,7 @@ import { BooleanCodec, IntegerCodec, DoubleCodec, - SymbolCodec, + SelectorCodec, StringCodec, BytestringCodec, ListCodec, @@ -58,7 +58,7 @@ const AtomCodecs = { float64: DoubleCodec, string: StringCodec, // TODO: Pass Invariant Equality - symbol: SymbolCodec, + selector: SelectorCodec, // TODO: Pass Invariant Equality byteArray: BytestringCodec, }; @@ -80,7 +80,7 @@ const OCapNTaggedCodec = makeRecordCodec( 'desc:tagged', // readBody syrupReader => { - const tagName = syrupReader.readSymbolAsString(); + const tagName = syrupReader.readSelectorAsString(); // @ts-expect-error any type const value = syrupReader.readOfType('any'); // TODO: Pass Invariant Equality @@ -92,7 +92,7 @@ const OCapNTaggedCodec = makeRecordCodec( }, // writeBody (value, syrupWriter) => { - syrupWriter.writeSymbol(value.tagName); + syrupWriter.writeSelector(value.tagName); value.value.write(syrupWriter); }, ); @@ -156,7 +156,7 @@ export const OCapNPassableUnionCodec = makeTypeHintUnionCodec( { boolean: AtomCodecs.boolean, float64: AtomCodecs.float64, - // "number-prefix" can be string, bytestring, symbol, integer + // "number-prefix" can be string, bytestring, selector, integer // TODO: should restrict further to only the types that can be passed 'number-prefix': AnyCodec, list: ContainerCodecs.list, @@ -169,7 +169,7 @@ export const OCapNPassableUnionCodec = makeTypeHintUnionCodec( boolean: AtomCodecs.boolean, number: AtomCodecs.float64, string: AtomCodecs.string, - symbol: AtomCodecs.symbol, + symbol: AtomCodecs.selector, bigint: AtomCodecs.integer, object: value => { if (value === null) { diff --git a/packages/syrup/src/selector.js b/packages/syrup/src/selector.js new file mode 100644 index 0000000000..37bce6b74a --- /dev/null +++ b/packages/syrup/src/selector.js @@ -0,0 +1,23 @@ +export const SYRUP_SELECTOR_PREFIX = 'syrup:'; + +// To be used as keys, syrup selectors must be javascript symbols. +// To avoid an otherwise meaningful symbol name, we prefix it with 'syrup:'. +export const SyrupSelectorFor = name => + Symbol.for(`${SYRUP_SELECTOR_PREFIX}${name}`); + +/** + * @param {symbol} selectorSymbol + * @returns {string} + */ +export const getSyrupSelectorName = selectorSymbol => { + const description = selectorSymbol.description; + if (!description) { + throw TypeError(`Symbol ${String(selectorSymbol)} has no description`); + } + if (!description.startsWith(SYRUP_SELECTOR_PREFIX)) { + throw TypeError( + `Symbol ${String(selectorSymbol)} has a description that does not start with "${SYRUP_SELECTOR_PREFIX}", got "${description}"`, + ); + } + return description.slice(SYRUP_SELECTOR_PREFIX.length); +}; diff --git a/packages/syrup/src/symbol.js b/packages/syrup/src/symbol.js deleted file mode 100644 index c385542183..0000000000 --- a/packages/syrup/src/symbol.js +++ /dev/null @@ -1,23 +0,0 @@ -export const SYRUP_SYMBOL_PREFIX = 'syrup:'; - -// To be used as keys, syrup symbols must be javascript symbols. -// To avoid an otherwise meaningful symbol name, we prefix it with 'syrup:'. -export const SyrupSymbolFor = name => - Symbol.for(`${SYRUP_SYMBOL_PREFIX}${name}`); - -/** - * @param {symbol} symbol - * @returns {string} - */ -export const getSyrupSymbolName = symbol => { - const description = symbol.description; - if (!description) { - throw TypeError(`Symbol ${String(symbol)} has no description`); - } - if (!description.startsWith(SYRUP_SYMBOL_PREFIX)) { - throw TypeError( - `Symbol ${String(symbol)} has a description that does not start with "${SYRUP_SYMBOL_PREFIX}", got "${description}"`, - ); - } - return description.slice(SYRUP_SYMBOL_PREFIX.length); -}; diff --git a/packages/syrup/test/_table.js b/packages/syrup/test/_table.js index 36dd359aa0..ddda3e2a1a 100644 --- a/packages/syrup/test/_table.js +++ b/packages/syrup/test/_table.js @@ -1,4 +1,4 @@ -import { SyrupSymbolFor } from '../src/symbol.js'; +import { SyrupSelectorFor } from '../src/selector.js'; const textEncoder = new TextEncoder(); @@ -9,7 +9,7 @@ export const table = [ { syrup: 't', value: true }, { syrup: 'f', value: false }, { syrup: '5"hello', value: 'hello' }, - { syrup: "5'hello", value: SyrupSymbolFor('hello') }, + { syrup: "5'hello", value: SyrupSelectorFor('hello') }, { syrup: '5:hello', value: textEncoder.encode('hello') }, { syrup: '[1+2+3+]', value: [1n, 2n, 3n] }, { syrup: '[3"abc3"def]', value: ['abc', 'def'] }, @@ -19,10 +19,10 @@ export const table = [ { syrup: '{0"10+1"i20+}', value: { '': 10n, i: 20n } }, // order canonicalization { syrup: '{0"10+1"i20+}', value: { i: 20n, '': 10n } }, - // dictionary with mixed string and symbol keys + // dictionary with mixed string and selector keys { syrup: '{3"dog20+3\'cat10+}', - value: { dog: 20n, [SyrupSymbolFor('cat')]: 10n }, + value: { dog: 20n, [SyrupSelectorFor('cat')]: 10n }, }, { syrup: 'D?\xf0\x00\x00\x00\x00\x00\x00', value: 1 }, { syrup: 'D@^\xdd/\x1a\x9f\xbew', value: 123.456 }, diff --git a/packages/syrup/test/decode.test.js b/packages/syrup/test/decode.test.js index 16c68477ab..b2b130b531 100644 --- a/packages/syrup/test/decode.test.js +++ b/packages/syrup/test/decode.test.js @@ -68,14 +68,14 @@ test('must reject out-of-order prefix key', t => { ); }); -test('dictionary keys must be strings or symbols', t => { +test('dictionary keys must be strings or selectors', t => { t.throws( () => { decodeSyrup(textEncoder.encode('{1+')); }, { message: - 'Unexpected type "integer", Syrup dictionary keys must be strings or symbols at index 1 of ', + 'Unexpected type "integer", Syrup dictionary keys must be strings or selectors at index 1 of ', }, ); }); diff --git a/packages/syrup/test/reader.test.js b/packages/syrup/test/reader.test.js index 6f69cf0c7b53264efb9211ecb066d4658ac96a54..82289bb6edd201fba3575717346ffec3aba88445 100644 GIT binary patch delta 81 zcmdm?eMEc16dv}}oYdr!{G!P-d1k`co6UKh7-5{rb$qkoY&rh4y*WdeSz0GfUo A=l}o! From fc36f6cb0ceefb99b7fd9b19d7d4cda3a3995da3 Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 14 Apr 2025 18:24:06 -1000 Subject: [PATCH 25/31] fix(syrup): Records can have bytestring labels --- packages/syrup/src/codec.js | 75 +++++++++++++--- packages/syrup/src/decode.js | 28 +++++- packages/syrup/src/encode.js | 20 ++++- packages/syrup/src/ocapn/components.js | 24 ++--- packages/syrup/src/ocapn/descriptors.js | 22 +++-- packages/syrup/src/ocapn/operations.js | 23 +++-- packages/syrup/src/ocapn/passable.js | 17 ++-- packages/syrup/src/ocapn/util.js | 28 ++++++ packages/syrup/test/codec.test.js | 115 +++++++++++++++++++++++- 9 files changed, 291 insertions(+), 61 deletions(-) create mode 100644 packages/syrup/src/ocapn/util.js diff --git a/packages/syrup/src/codec.js b/packages/syrup/src/codec.js index 98637daced..4d80ddfadf 100644 --- a/packages/syrup/src/codec.js +++ b/packages/syrup/src/codec.js @@ -12,6 +12,9 @@ const quote = JSON.stringify; * @property {function(any, SyrupWriter): void} write */ +const textDecoder = new TextDecoder(); +const textEncoder = new TextEncoder(); + /** @type {SyrupCodec} */ export const SelectorCodec = freeze({ write: (value, syrupWriter) => syrupWriter.writeSelector(value), @@ -79,6 +82,32 @@ export const ListCodec = freeze({ }, }); +/** + * @param {SyrupCodec} childCodec + * @returns {SyrupCodec} + */ +export const makeListCodecFromEntryCodec = childCodec => { + return freeze({ + read: syrupReader => { + syrupReader.enterList(); + const result = []; + while (!syrupReader.peekListEnd()) { + const value = childCodec.read(syrupReader); + result.push(value); + } + syrupReader.exitList(); + return result; + }, + write: (value, syrupWriter) => { + syrupWriter.enterList(); + for (const child of value) { + childCodec.write(child, syrupWriter); + } + syrupWriter.exitList(); + }, + }); +}; + /** * @typedef {SyrupCodec & { * label: string; @@ -87,23 +116,38 @@ export const ListCodec = freeze({ * }} SyrupRecordCodec */ +/** + * @typedef {'selector' | 'string' | 'bytestring'} SyrupRecordLabelType + * see https://github.com/ocapn/syrup/issues/22 + */ + /** * @param {string} label + * @param {SyrupRecordLabelType} labelType * @param {function(SyrupReader): any} readBody * @param {function(any, SyrupWriter): void} writeBody * @returns {SyrupRecordCodec} */ -export const makeRecordCodec = (label, readBody, writeBody) => { +export const makeRecordCodec = (label, labelType, readBody, writeBody) => { /** * @param {SyrupReader} syrupReader * @returns {any} */ const read = syrupReader => { syrupReader.enterRecord(); - const actualLabel = syrupReader.readSelectorAsString(); - if (actualLabel !== label) { + const labelInfo = syrupReader.readRecordLabel(); + if (labelInfo.type !== labelType) { throw Error( - `RecordCodec: Expected label ${quote(label)}, got ${quote(actualLabel)}`, + `RecordCodec: Expected label type ${quote(labelType)} for ${quote(label)}, got ${quote(labelInfo.type)}`, + ); + } + const labelString = + labelInfo.type === 'bytestring' + ? textDecoder.decode(labelInfo.value) + : labelInfo.value; + if (labelString !== label) { + throw Error( + `RecordCodec: Expected label ${quote(label)}, got ${quote(labelString)}`, ); } const result = readBody(syrupReader); @@ -116,7 +160,13 @@ export const makeRecordCodec = (label, readBody, writeBody) => { */ const write = (value, syrupWriter) => { syrupWriter.enterRecord(); - syrupWriter.writeSelector(value.type); + if (labelType === 'selector') { + syrupWriter.writeSelector(value.type); + } else if (labelType === 'string') { + syrupWriter.writeString(value.type); + } else if (labelType === 'bytestring') { + syrupWriter.writeBytestring(textEncoder.encode(value.type)); + } writeBody(value, syrupWriter); syrupWriter.exitRecord(); }; @@ -133,10 +183,11 @@ export const makeRecordCodec = (label, readBody, writeBody) => { /** * @param {string} label + * @param {SyrupRecordLabelType} labelType * @param {SyrupRecordDefinition} definition * @returns {SyrupRecordCodec} */ -export const makeRecordCodecFromDefinition = (label, definition) => { +export const makeRecordCodecFromDefinition = (label, labelType, definition) => { /** * @param {SyrupReader} syrupReader * @returns {any} @@ -175,7 +226,7 @@ export const makeRecordCodecFromDefinition = (label, definition) => { } }; - return makeRecordCodec(label, readBody, writeBody); + return makeRecordCodec(label, labelType, readBody, writeBody); }; /** @@ -263,10 +314,14 @@ export const makeRecordUnionCodec = recordTypes => { }; const read = syrupReader => { syrupReader.enterRecord(); - const label = syrupReader.readSelectorAsString(); - const recordCodec = recordTable[label]; + const labelInfo = syrupReader.readRecordLabel(); + const labelString = + labelInfo.type === 'bytestring' + ? textDecoder.decode(labelInfo.value) + : labelInfo.value; + const recordCodec = recordTable[labelString]; if (!recordCodec) { - throw Error(`Unexpected record type: ${quote(label)}`); + throw Error(`Unexpected record type: ${quote(labelString)}`); } const result = recordCodec.readBody(syrupReader); syrupReader.exitRecord(); diff --git a/packages/syrup/src/decode.js b/packages/syrup/src/decode.js index 27393734bd..ca2c09be77 100644 --- a/packages/syrup/src/decode.js +++ b/packages/syrup/src/decode.js @@ -194,12 +194,30 @@ function readSelectorAsString(bufferReader, name) { /** * @param {BufferReader} bufferReader * @param {string} name - * @returns {string} + * @returns {Uint8Array} */ function readBytestring(bufferReader, name) { return readAndAssertType(bufferReader, 'bytestring', name); } +/** + * @param {BufferReader} bufferReader + * @param {string} name + * @returns {{value: string, type: 'selector'} | {value: Uint8Array, type: 'bytestring'} | {value: string, type: 'string'}} + * see https://github.com/ocapn/syrup/issues/22 + */ +function readRecordLabel(bufferReader, name) { + const start = bufferReader.index; + const { value, type } = readTypeAndMaybeValue(bufferReader, name); + if (type === 'selector' || type === 'string' || type === 'bytestring') { + // @ts-expect-error type system is not smart enough + return { value, type }; + } + throw Error( + `Unexpected type ${quote(type)}, Syrup record labels must be strings, selectors, or bytestrings at index ${start} of ${name}`, + ); +} + /** * @param {BufferReader} bufferReader * @param {string} name @@ -506,6 +524,10 @@ export class SyrupReader { return cc === RECORD_END; } + readRecordLabel() { + return readRecordLabel(this.bufferReader, this.name); + } + enterDictionary() { this.#readAndAssertByte(DICT_START); this.#pushStackEntry('dictionary'); @@ -521,6 +543,10 @@ export class SyrupReader { return cc === DICT_END; } + readDictionaryKey() { + return readDictionaryKey(this.bufferReader, this.name); + } + enterList() { this.#readAndAssertByte(LIST_START); this.#pushStackEntry('list'); diff --git a/packages/syrup/src/encode.js b/packages/syrup/src/encode.js index 2d7a287960..97ecd8a790 100644 --- a/packages/syrup/src/encode.js +++ b/packages/syrup/src/encode.js @@ -18,8 +18,8 @@ const LIST_START = '['.charCodeAt(0); const LIST_END = ']'.charCodeAt(0); const DICT_START = '{'.charCodeAt(0); const DICT_END = '}'.charCodeAt(0); -// const SET_START = '#'.charCodeAt(0); -// const SET_END = '$'.charCodeAt(0); +const SET_START = '#'.charCodeAt(0); +const SET_END = '$'.charCodeAt(0); // const BYTES_START = ':'.charCodeAt(0); // const STRING_START = '"'.charCodeAt(0); // const SELECTOR_START = "'".charCodeAt(0); @@ -296,6 +296,22 @@ export class SyrupWriter { this.bufferWriter.writeByte(LIST_END); } + enterDictionary() { + this.bufferWriter.writeByte(DICT_START); + } + + exitDictionary() { + this.bufferWriter.writeByte(DICT_END); + } + + enterSet() { + this.bufferWriter.writeByte(SET_START); + } + + exitSet() { + this.bufferWriter.writeByte(SET_END); + } + /** * @param {'boolean' | 'integer' | 'float64' | 'string' | 'bytestring' | 'selector'} type * @param {any} value diff --git a/packages/syrup/src/ocapn/components.js b/packages/syrup/src/ocapn/components.js index 10feb8e4fc..86d718d796 100644 --- a/packages/syrup/src/ocapn/components.js +++ b/packages/syrup/src/ocapn/components.js @@ -1,7 +1,6 @@ -import { - makeRecordCodecFromDefinition, - makeRecordUnionCodec, -} from '../codec.js'; +import { makeRecordUnionCodec } from '../codec.js'; +import { makeOCapNRecordCodecFromDefinition } from './util.js'; + /** @typedef {import('../codec.js').SyrupCodec} SyrupCodec */ const { freeze } = Object; @@ -33,24 +32,27 @@ export const makeOCapNSignatureValueComponentCodec = expectedLabel => { const OCapNSignatureRValue = makeOCapNSignatureValueComponentCodec('r'); const OCapNSignatureSValue = makeOCapNSignatureValueComponentCodec('s'); -export const OCapNSignature = makeRecordCodecFromDefinition('sig-val', [ +export const OCapNSignature = makeOCapNRecordCodecFromDefinition('sig-val', [ ['scheme', 'selector'], ['r', OCapNSignatureRValue], ['s', OCapNSignatureSValue], ]); -export const OCapNNode = makeRecordCodecFromDefinition('ocapn-node', [ +export const OCapNNode = makeOCapNRecordCodecFromDefinition('ocapn-node', [ ['transport', 'selector'], ['address', 'bytestring'], ['hints', 'boolean'], ]); -export const OCapNSturdyRef = makeRecordCodecFromDefinition('ocapn-sturdyref', [ - ['node', OCapNNode], - ['swissNum', 'string'], -]); +export const OCapNSturdyRef = makeOCapNRecordCodecFromDefinition( + 'ocapn-sturdyref', + [ + ['node', OCapNNode], + ['swissNum', 'string'], + ], +); -export const OCapNPublicKey = makeRecordCodecFromDefinition('public-key', [ +export const OCapNPublicKey = makeOCapNRecordCodecFromDefinition('public-key', [ ['scheme', 'selector'], ['curve', 'selector'], ['flags', 'selector'], diff --git a/packages/syrup/src/ocapn/descriptors.js b/packages/syrup/src/ocapn/descriptors.js index c93f153973..2d608cd6f9 100644 --- a/packages/syrup/src/ocapn/descriptors.js +++ b/packages/syrup/src/ocapn/descriptors.js @@ -1,7 +1,5 @@ -import { - makeRecordCodecFromDefinition, - makeRecordUnionCodec, -} from '../codec.js'; +import { makeRecordUnionCodec } from '../codec.js'; +import { makeOCapNRecordCodecFromDefinition } from './util.js'; import { PositiveIntegerCodec } from './subtypes.js'; import { OCapNNode, OCapNPublicKey, OCapNSignature } from './components.js'; @@ -10,25 +8,25 @@ import { OCapNNode, OCapNPublicKey, OCapNSignature } from './components.js'; * directly in OCapN Messages and as part of Passable structures. */ -export const DescImportObject = makeRecordCodecFromDefinition( +export const DescImportObject = makeOCapNRecordCodecFromDefinition( 'desc:import-object', [['position', PositiveIntegerCodec]], ); -export const DescImportPromise = makeRecordCodecFromDefinition( +export const DescImportPromise = makeOCapNRecordCodecFromDefinition( 'desc:import-promise', [['position', PositiveIntegerCodec]], ); -export const DescExport = makeRecordCodecFromDefinition('desc:export', [ +export const DescExport = makeOCapNRecordCodecFromDefinition('desc:export', [ ['position', PositiveIntegerCodec], ]); -export const DescAnswer = makeRecordCodecFromDefinition('desc:answer', [ +export const DescAnswer = makeOCapNRecordCodecFromDefinition('desc:answer', [ ['position', PositiveIntegerCodec], ]); -export const DescHandoffGive = makeRecordCodecFromDefinition( +export const DescHandoffGive = makeOCapNRecordCodecFromDefinition( 'desc:handoff-give', [ ['receiverKey', OCapNPublicKey], @@ -39,7 +37,7 @@ export const DescHandoffGive = makeRecordCodecFromDefinition( ], ); -export const DescSigGiveEnvelope = makeRecordCodecFromDefinition( +export const DescSigGiveEnvelope = makeOCapNRecordCodecFromDefinition( 'desc:sig-envelope', [ ['object', DescHandoffGive], @@ -47,7 +45,7 @@ export const DescSigGiveEnvelope = makeRecordCodecFromDefinition( ], ); -export const DescHandoffReceive = makeRecordCodecFromDefinition( +export const DescHandoffReceive = makeOCapNRecordCodecFromDefinition( 'desc:handoff-receive', [ ['receivingSession', 'bytestring'], @@ -57,7 +55,7 @@ export const DescHandoffReceive = makeRecordCodecFromDefinition( ], ); -export const DescSigReceiveEnvelope = makeRecordCodecFromDefinition( +export const DescSigReceiveEnvelope = makeOCapNRecordCodecFromDefinition( 'desc:sig-envelope', [ ['object', DescHandoffReceive], diff --git a/packages/syrup/src/ocapn/operations.js b/packages/syrup/src/ocapn/operations.js index a32992002c..edf8397c13 100644 --- a/packages/syrup/src/ocapn/operations.js +++ b/packages/syrup/src/ocapn/operations.js @@ -1,8 +1,5 @@ -import { - makeRecordUnionCodec, - makeRecordCodecFromDefinition, - makeTypeHintUnionCodec, -} from '../codec.js'; +import { makeRecordUnionCodec, makeTypeHintUnionCodec } from '../codec.js'; +import { makeOCapNRecordCodecFromDefinition } from './util.js'; import { PositiveIntegerCodec, FalseCodec } from './subtypes.js'; import { OCapNNode, OCapNPublicKey, OCapNSignature } from './components.js'; import { OCapNPassableUnionCodec } from './passable.js'; @@ -19,7 +16,7 @@ const { freeze } = Object; * These are OCapN Operations, they are messages that are sent between OCapN Nodes */ -const OpStartSession = makeRecordCodecFromDefinition('op:start-session', [ +const OpStartSession = makeOCapNRecordCodecFromDefinition('op:start-session', [ ['captpVersion', 'string'], ['sessionPublicKey', OCapNPublicKey], ['location', OCapNNode], @@ -31,7 +28,7 @@ const OCapNResolveMeDescCodec = makeRecordUnionCodec({ DescImportPromise, }); -const OpListen = makeRecordCodecFromDefinition('op:listen', [ +const OpListen = makeOCapNRecordCodecFromDefinition('op:listen', [ ['to', DescExport], ['resolveMeDesc', OCapNResolveMeDescCodec], ['wantsPartial', 'boolean'], @@ -80,7 +77,7 @@ const OpDeliverArgsCodec = freeze({ }, }); -const OpDeliverOnly = makeRecordCodecFromDefinition('op:deliver-only', [ +const OpDeliverOnly = makeOCapNRecordCodecFromDefinition('op:deliver-only', [ ['to', OCapNDeliverTargetCodec], ['args', OpDeliverArgsCodec], ]); @@ -97,7 +94,7 @@ const OpDeliverAnswerCodec = makeTypeHintUnionCodec( }, ); -const OpDeliver = makeRecordCodecFromDefinition('op:deliver', [ +const OpDeliver = makeOCapNRecordCodecFromDefinition('op:deliver', [ ['to', OCapNDeliverTargetCodec], ['args', OpDeliverArgsCodec], ['answerPosition', OpDeliverAnswerCodec], @@ -109,22 +106,22 @@ const OCapNPromiseRefCodec = makeRecordUnionCodec({ DescImportPromise, }); -const OpPick = makeRecordCodecFromDefinition('op:pick', [ +const OpPick = makeOCapNRecordCodecFromDefinition('op:pick', [ ['promisePosition', OCapNPromiseRefCodec], ['selectedValuePosition', 'integer'], ['newAnswerPosition', 'integer'], ]); -const OpAbort = makeRecordCodecFromDefinition('op:abort', [ +const OpAbort = makeOCapNRecordCodecFromDefinition('op:abort', [ ['reason', 'string'], ]); -const OpGcExport = makeRecordCodecFromDefinition('op:gc-export', [ +const OpGcExport = makeOCapNRecordCodecFromDefinition('op:gc-export', [ ['exportPosition', 'integer'], ['wireDelta', 'integer'], ]); -const OpGcAnswer = makeRecordCodecFromDefinition('op:gc-answer', [ +const OpGcAnswer = makeOCapNRecordCodecFromDefinition('op:gc-answer', [ ['answerPosition', 'integer'], ]); diff --git a/packages/syrup/src/ocapn/passable.js b/packages/syrup/src/ocapn/passable.js index 68f14e1616..947d7cb8e3 100644 --- a/packages/syrup/src/ocapn/passable.js +++ b/packages/syrup/src/ocapn/passable.js @@ -7,11 +7,13 @@ import { BytestringCodec, ListCodec, AnyCodec, - makeRecordCodecFromDefinition, - makeRecordCodec, makeRecordUnionCodec, makeTypeHintUnionCodec, } from '../codec.js'; +import { + makeOCapNRecordCodec, + makeOCapNRecordCodecFromDefinition, +} from './util.js'; import { DescImportObject, DescImportPromise, @@ -26,7 +28,7 @@ import { // OCapN Passable Atoms -const UndefinedCodec = makeRecordCodec( +const UndefinedCodec = makeOCapNRecordCodec( 'void', // readBody syrupReader => { @@ -38,7 +40,7 @@ const UndefinedCodec = makeRecordCodec( }, ); -const NullCodec = makeRecordCodec( +const NullCodec = makeOCapNRecordCodec( 'null', // readBody syrupReader => { @@ -57,9 +59,7 @@ const AtomCodecs = { integer: IntegerCodec, float64: DoubleCodec, string: StringCodec, - // TODO: Pass Invariant Equality selector: SelectorCodec, - // TODO: Pass Invariant Equality byteArray: BytestringCodec, }; @@ -76,14 +76,13 @@ export const OCapNStructCodec = { }, }; -const OCapNTaggedCodec = makeRecordCodec( +const OCapNTaggedCodec = makeOCapNRecordCodec( 'desc:tagged', // readBody syrupReader => { const tagName = syrupReader.readSelectorAsString(); // @ts-expect-error any type const value = syrupReader.readOfType('any'); - // TODO: Pass Invariant Equality return { [Symbol.for('passStyle')]: 'tagged', [Symbol.toStringTag]: tagName, @@ -122,7 +121,7 @@ const OCapNReferenceCodecs = { // OCapN Error -const OCapNErrorCodec = makeRecordCodecFromDefinition('desc:error', [ +const OCapNErrorCodec = makeOCapNRecordCodecFromDefinition('desc:error', [ ['message', 'string'], ]); diff --git a/packages/syrup/src/ocapn/util.js b/packages/syrup/src/ocapn/util.js new file mode 100644 index 0000000000..294ba52d16 --- /dev/null +++ b/packages/syrup/src/ocapn/util.js @@ -0,0 +1,28 @@ +import { makeRecordCodec, makeRecordCodecFromDefinition } from '../codec.js'; + +/** @typedef {import('../decode.js').SyrupReader} SyrupReader */ +/** @typedef {import('../encode.js').SyrupWriter} SyrupWriter */ +/** @typedef {import('../codec.js').SyrupRecordCodec} SyrupRecordCodec */ +/** @typedef {import('../codec.js').SyrupType} SyrupType */ +/** @typedef {import('../codec.js').SyrupCodec} SyrupCodec */ + +/** + * @param {string} label + * @param {Array<[string, SyrupType | SyrupCodec]>} definition + * @returns {SyrupRecordCodec} + */ +export const makeOCapNRecordCodecFromDefinition = (label, definition) => { + // Syrup Records as used in OCapN are always labeled with selectors + return makeRecordCodecFromDefinition(label, 'selector', definition); +}; + +/** + * @param {string} label + * @param {function(SyrupReader): any} readBody + * @param {function(any, SyrupWriter): void} writeBody + * @returns {SyrupRecordCodec} + */ +export const makeOCapNRecordCodec = (label, readBody, writeBody) => { + // Syrup Records as used in OCapN are always labeled with selectors + return makeRecordCodec(label, 'selector', readBody, writeBody); +}; diff --git a/packages/syrup/test/codec.test.js b/packages/syrup/test/codec.test.js index 7f1ece8569..af418debcd 100644 --- a/packages/syrup/test/codec.test.js +++ b/packages/syrup/test/codec.test.js @@ -1,14 +1,28 @@ // @ts-check import test from 'ava'; +import path from 'path'; +import fs from 'fs'; import { makeSyrupReader } from '../src/decode.js'; import { makeSyrupWriter } from '../src/encode.js'; import { makeRecordUnionCodec, makeRecordCodecFromDefinition, StringCodec, + makeListCodecFromEntryCodec, } from '../src/codec.js'; +/** @typedef {import('../src/codec.js').SyrupCodec} SyrupCodec */ + +const textDecoder = new TextDecoder(); +const textEncoder = new TextEncoder(); +// zoo.bin from https://github.com/ocapn/syrup/tree/2214cbb7c0ee081699fdef64edbc2444af2bb1d2/test-data +// eslint-disable-next-line no-underscore-dangle +const __dirname = path.dirname(new URL(import.meta.url).pathname); +const zooBinRaw = fs.readFileSync(path.resolve(__dirname, '_zoo.bin')); +// nodejs can provide a buffer with a non-zero byteOffset, which confuses the buffer reader +const zooBin = Uint8Array.from(zooBinRaw); + const testCodecBidirectionally = (t, codec, value) => { const writer = makeSyrupWriter(); codec.write(value, writer); @@ -25,7 +39,7 @@ test('simple string codec', t => { }); test('basic record codec cases', t => { - const codec = makeRecordCodecFromDefinition('test', [ + const codec = makeRecordCodecFromDefinition('test', 'selector', [ ['field1', 'string'], ['field2', 'integer'], ]); @@ -39,11 +53,11 @@ test('basic record codec cases', t => { test('record union codec', t => { const codec = makeRecordUnionCodec({ - testA: makeRecordCodecFromDefinition('testA', [ + testA: makeRecordCodecFromDefinition('testA', 'selector', [ ['field1', 'string'], ['field2', 'integer'], ]), - testB: makeRecordCodecFromDefinition('testB', [ + testB: makeRecordCodecFromDefinition('testB', 'selector', [ ['field1', 'string'], ['field2', 'integer'], ]), @@ -55,3 +69,98 @@ test('record union codec', t => { }; testCodecBidirectionally(t, codec, value); }); + +test('zoo.bin', t => { + /** @type {SyrupCodec} */ + const inhabitantCodec = { + read: syrupReader => { + const result = {}; + syrupReader.enterDictionary(); + t.is(syrupReader.readSelectorAsString(), 'age'); + result.age = syrupReader.readInteger(); + t.is(syrupReader.readSelectorAsString(), 'eats'); + result.eats = []; + syrupReader.enterSet(); + while (!syrupReader.peekSetEnd()) { + result.eats.push(textDecoder.decode(syrupReader.readBytestring())); + } + syrupReader.exitSet(); + t.is(syrupReader.readSelectorAsString(), 'name'); + result.name = syrupReader.readString(); + t.is(syrupReader.readSelectorAsString(), 'alive?'); + result.alive = syrupReader.readBoolean(); + t.is(syrupReader.readSelectorAsString(), 'weight'); + result.weight = syrupReader.readFloat64(); + t.is(syrupReader.readSelectorAsString(), 'species'); + result.species = textDecoder.decode(syrupReader.readBytestring()); + syrupReader.exitDictionary(); + return result; + }, + write: (value, syrupWriter) => { + syrupWriter.enterDictionary(); + syrupWriter.writeSelector('age'); + syrupWriter.writeInteger(value.age); + syrupWriter.writeSelector('eats'); + syrupWriter.enterSet(); + for (const eat of value.eats) { + syrupWriter.writeBytestring(textEncoder.encode(eat)); + } + syrupWriter.exitSet(); + syrupWriter.writeSelector('name'); + syrupWriter.writeString(value.name); + syrupWriter.writeSelector('alive?'); + syrupWriter.writeBoolean(value.alive); + syrupWriter.writeSelector('weight'); + syrupWriter.writeDouble(value.weight); + syrupWriter.writeSelector('species'); + syrupWriter.writeBytestring(textEncoder.encode(value.species)); + syrupWriter.exitDictionary(); + }, + }; + + const inhabitantListCodec = makeListCodecFromEntryCodec(inhabitantCodec); + + const zooCodec = makeRecordCodecFromDefinition('zoo', 'bytestring', [ + ['title', 'string'], + ['inhabitants', inhabitantListCodec], + ]); + + const reader = makeSyrupReader(zooBin, { name: 'zoo' }); + const value = zooCodec.read(reader); + t.deepEqual(value, { + type: 'zoo', + title: 'The Grand Menagerie', + inhabitants: [ + { + age: 12n, + eats: ['fish', 'mice', 'kibble'], + name: 'Tabatha', + alive: true, + weight: 8.2, + species: 'cat', + }, + { + age: 6n, + eats: ['bananas', 'insects'], + name: 'George', + alive: false, + weight: 17.24, + species: 'monkey', + }, + { + age: -12n, + eats: [], + name: 'Casper', + alive: false, + weight: -34.5, + species: 'ghost', + }, + ], + }); + const writer = makeSyrupWriter(); + zooCodec.write(value, writer); + const bytes = writer.bufferWriter.subarray(0, writer.bufferWriter.length); + const resultSyrup = textDecoder.decode(bytes); + const originalSyrup = textDecoder.decode(zooBin); + t.deepEqual(resultSyrup, originalSyrup); +}); From 5c67c07dff04cb2e0fcc6e3573a165c09fda82da Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 14 Apr 2025 18:29:53 -1000 Subject: [PATCH 26/31] fix(syrup): prefer term "float64" over "double" --- packages/syrup/src/codec.js | 4 ++-- packages/syrup/src/decode.js | 8 ++++---- packages/syrup/src/encode.js | 14 +++++++------- packages/syrup/src/ocapn/passable.js | 4 ++-- packages/syrup/test/codec.test.js | 2 +- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/syrup/src/codec.js b/packages/syrup/src/codec.js index 4d80ddfadf..bd7c44ea33 100644 --- a/packages/syrup/src/codec.js +++ b/packages/syrup/src/codec.js @@ -46,8 +46,8 @@ export const IntegerCodec = freeze({ }); /** @type {SyrupCodec} */ -export const DoubleCodec = freeze({ - write: (value, syrupWriter) => syrupWriter.writeDouble(value), +export const Float64Codec = freeze({ + write: (value, syrupWriter) => syrupWriter.writeFloat64(value), read: syrupReader => syrupReader.readFloat64(), }); diff --git a/packages/syrup/src/decode.js b/packages/syrup/src/decode.js index ca2c09be77..b36b91e293 100644 --- a/packages/syrup/src/decode.js +++ b/packages/syrup/src/decode.js @@ -23,7 +23,7 @@ const RECORD_END = '>'.charCodeAt(0); const TRUE = 't'.charCodeAt(0); const FALSE = 'f'.charCodeAt(0); // const SINGLE = 'F'.charCodeAt(0); -const DOUBLE = 'D'.charCodeAt(0); +const FLOAT64 = 'D'.charCodeAt(0); const textDecoder = new TextDecoder(); @@ -97,7 +97,7 @@ function readTypeAndMaybeValue(bufferReader, name) { if (cc === FALSE) { return { type: 'boolean', value: false }; } - if (cc === DOUBLE) { + if (cc === FLOAT64) { // eslint-disable-next-line no-use-before-define const value = readFloat64Body(bufferReader, name); return { type: 'float64', value }; @@ -248,7 +248,7 @@ function readFloat64Body(bufferReader, name) { */ function readFloat64(bufferReader, name) { const cc = bufferReader.readByte(); - if (cc !== DOUBLE) { + if (cc !== FLOAT64) { throw Error( `Unexpected character ${quote(toChar(cc))}, at index ${bufferReader.index} of ${name}`, ); @@ -403,7 +403,7 @@ export function peekTypeHint(bufferReader, name) { if (cc === TRUE || cc === FALSE) { return 'boolean'; } - if (cc === DOUBLE) { + if (cc === FLOAT64) { return 'float64'; } if (cc === LIST_START) { diff --git a/packages/syrup/src/encode.js b/packages/syrup/src/encode.js index 97ecd8a790..44a92661b4 100644 --- a/packages/syrup/src/encode.js +++ b/packages/syrup/src/encode.js @@ -28,7 +28,7 @@ const RECORD_END = '>'.charCodeAt(0); const TRUE = 't'.charCodeAt(0); const FALSE = 'f'.charCodeAt(0); // const SINGLE = 'F'.charCodeAt(0); -const DOUBLE = 'D'.charCodeAt(0); +const FLOAT64 = 'D'.charCodeAt(0); const NAN64 = freeze([0x7f, 0xf8, 0, 0, 0, 0, 0, 0]); @@ -164,8 +164,8 @@ function writeList(bufferWriter, array, path) { * @param {import('./buffer-writer.js').BufferWriter} bufferWriter * @param {number} value */ -function writeDouble(bufferWriter, value) { - bufferWriter.writeByte(DOUBLE); +function writeFloat64(bufferWriter, value) { + bufferWriter.writeByte(FLOAT64); if (value === 0) { // no-op } else if (Number.isNaN(value)) { @@ -212,7 +212,7 @@ function writeAny(bufferWriter, value, path, pathSuffix) { } if (typeof value === 'number') { - writeDouble(bufferWriter, value); + writeFloat64(bufferWriter, value); return; } @@ -276,8 +276,8 @@ export class SyrupWriter { writeInteger(this.bufferWriter, value); } - writeDouble(value) { - writeDouble(this.bufferWriter, value); + writeFloat64(value) { + writeFloat64(this.bufferWriter, value); } enterRecord() { @@ -328,7 +328,7 @@ export class SyrupWriter { this.writeString(value); break; case 'float64': - this.writeDouble(value); + this.writeFloat64(value); break; case 'integer': this.writeInteger(value); diff --git a/packages/syrup/src/ocapn/passable.js b/packages/syrup/src/ocapn/passable.js index 947d7cb8e3..ed919e6892 100644 --- a/packages/syrup/src/ocapn/passable.js +++ b/packages/syrup/src/ocapn/passable.js @@ -1,7 +1,7 @@ import { BooleanCodec, IntegerCodec, - DoubleCodec, + Float64Codec, SelectorCodec, StringCodec, BytestringCodec, @@ -57,7 +57,7 @@ const AtomCodecs = { null: NullCodec, boolean: BooleanCodec, integer: IntegerCodec, - float64: DoubleCodec, + float64: Float64Codec, string: StringCodec, selector: SelectorCodec, byteArray: BytestringCodec, diff --git a/packages/syrup/test/codec.test.js b/packages/syrup/test/codec.test.js index af418debcd..e68d7a488a 100644 --- a/packages/syrup/test/codec.test.js +++ b/packages/syrup/test/codec.test.js @@ -111,7 +111,7 @@ test('zoo.bin', t => { syrupWriter.writeSelector('alive?'); syrupWriter.writeBoolean(value.alive); syrupWriter.writeSelector('weight'); - syrupWriter.writeDouble(value.weight); + syrupWriter.writeFloat64(value.weight); syrupWriter.writeSelector('species'); syrupWriter.writeBytestring(textEncoder.encode(value.species)); syrupWriter.exitDictionary(); From 0a5de4334cd16487a6e84fefb1e3aab4249a00a6 Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 14 Apr 2025 18:37:59 -1000 Subject: [PATCH 27/31] refactor(syrup): use private fields for buffer reader/writer --- packages/syrup/src/buffer-reader.js | 145 ++++++++++++++-------------- packages/syrup/src/buffer-writer.js | 104 +++++++++----------- 2 files changed, 117 insertions(+), 132 deletions(-) diff --git a/packages/syrup/src/buffer-reader.js b/packages/syrup/src/buffer-reader.js index f013e733f4..784fd669b5 100644 --- a/packages/syrup/src/buffer-reader.js +++ b/packages/syrup/src/buffer-reader.js @@ -12,26 +12,23 @@ const q = JSON.stringify; * @property {number} offset */ -/** @type {WeakMap} */ -const privateFields = new WeakMap(); - -/** @type {(bufferReader: BufferReader) => BufferReaderState} */ -const privateFieldsGet = privateFields.get.bind(privateFields); - export class BufferReader { + /** @type {BufferReaderState} */ + #state; + /** * @param {ArrayBuffer} buffer */ constructor(buffer) { const bytes = new Uint8Array(buffer); const data = new DataView(bytes.buffer); - privateFields.set(this, { + this.#state = { bytes, data, length: bytes.length, index: 0, offset: 0, - }); + }; } /** @@ -41,14 +38,14 @@ export class BufferReader { static fromBytes(bytes) { const empty = new ArrayBuffer(0); const reader = new BufferReader(empty); - const fields = privateFieldsGet(reader); - fields.bytes = bytes; - fields.data = new DataView(bytes.buffer); - fields.length = bytes.length; - fields.index = 0; - fields.offset = bytes.byteOffset; + const state = reader.#state; + state.bytes = bytes; + state.data = new DataView(bytes.buffer); + state.length = bytes.length; + state.index = 0; + state.offset = bytes.byteOffset; // Temporary check until we can handle non-zero byteOffset - if (fields.offset !== 0) { + if (state.offset !== 0) { throw Error( 'Cannot create BufferReader from Uint8Array with a non-zero byteOffset', ); @@ -60,14 +57,14 @@ export class BufferReader { * @returns {number} */ get length() { - return privateFieldsGet(this).length; + return this.#state.length; } /** * @returns {number} */ get index() { - return privateFieldsGet(this).index; + return this.#state.index; } /** @@ -81,15 +78,15 @@ export class BufferReader { * @param {number} offset */ set offset(offset) { - const fields = privateFieldsGet(this); - if (offset > fields.data.byteLength) { + const state = this.#state; + if (offset > state.data.byteLength) { throw Error('Cannot set offset beyond length of underlying data'); } if (offset < 0) { throw Error('Cannot set negative offset'); } - fields.offset = offset; - fields.length = fields.data.byteLength - fields.offset; + state.offset = offset; + state.length = state.data.byteLength - state.offset; } /** @@ -98,8 +95,8 @@ export class BufferReader { * index. */ canSeek(index) { - const fields = privateFieldsGet(this); - return index >= 0 && fields.offset + index <= fields.length; + const state = this.#state; + return index >= 0 && state.offset + index <= state.length; } /** @@ -107,10 +104,10 @@ export class BufferReader { * @throws {Error} an Error if the index is out of bounds. */ assertCanSeek(index) { - const fields = privateFieldsGet(this); + const state = this.#state; if (!this.canSeek(index)) { const err = Error( - `End of data reached (data length = ${fields.length}, asked index ${index}`, + `End of data reached (data length = ${state.length}, asked index ${index}`, ); // @ts-expect-error err.code = 'EOD'; @@ -125,10 +122,10 @@ export class BufferReader { * @returns {number} prior index */ seek(index) { - const fields = privateFieldsGet(this); - const restore = fields.index; + const state = this.#state; + const restore = state.index; this.assertCanSeek(index); - fields.index = index; + state.index = index; return restore; } @@ -137,32 +134,32 @@ export class BufferReader { * @returns {Uint8Array} */ peek(size) { - const fields = privateFieldsGet(this); + const state = this.#state; // Clamp size. - size = Math.max(0, Math.min(fields.length - fields.index, size)); + size = Math.max(0, Math.min(state.length - state.index, size)); if (size === 0) { // in IE10, when using subarray(idx, idx), we get the array [0x00] instead of []. return new Uint8Array(0); } - const result = fields.bytes.subarray( - fields.offset + fields.index, - fields.offset + fields.index + size, + const result = state.bytes.subarray( + state.offset + state.index, + state.offset + state.index + size, ); return result; } peekByte() { - const fields = privateFieldsGet(this); + const state = this.#state; this.assertCanRead(1); - return fields.bytes[fields.offset + fields.index]; + return state.bytes[state.offset + state.index]; } /** * @param {number} offset */ canRead(offset) { - const fields = privateFieldsGet(this); - return this.canSeek(fields.index + offset); + const state = this.#state; + return this.canSeek(state.index + offset); } /** @@ -172,8 +169,8 @@ export class BufferReader { * @throws {Error} an Error if the offset is out of bounds. */ assertCanRead(offset) { - const fields = privateFieldsGet(this); - this.assertCanSeek(fields.index + offset); + const state = this.#state; + this.assertCanSeek(state.index + offset); } /** @@ -183,10 +180,10 @@ export class BufferReader { * @returns {Uint8Array} the raw data. */ read(size) { - const fields = privateFieldsGet(this); + const state = this.#state; this.assertCanRead(size); const result = this.peek(size); - fields.index += size; + state.index += size; return result; } @@ -201,11 +198,11 @@ export class BufferReader { * @returns {number} */ readUint8() { - const fields = privateFieldsGet(this); + const state = this.#state; this.assertCanRead(1); - const index = fields.offset + fields.index; - const value = fields.data.getUint8(index); - fields.index += 1; + const index = state.offset + state.index; + const value = state.data.getUint8(index); + state.index += 1; return value; } @@ -214,11 +211,11 @@ export class BufferReader { * @param {boolean=} littleEndian */ readUint16(littleEndian) { - const fields = privateFieldsGet(this); + const state = this.#state; this.assertCanRead(2); - const index = fields.offset + fields.index; - const value = fields.data.getUint16(index, littleEndian); - fields.index += 2; + const index = state.offset + state.index; + const value = state.data.getUint16(index, littleEndian); + state.index += 2; return value; } @@ -227,11 +224,11 @@ export class BufferReader { * @param {boolean=} littleEndian */ readUint32(littleEndian) { - const fields = privateFieldsGet(this); + const state = this.#state; this.assertCanRead(4); - const index = fields.offset + fields.index; - const value = fields.data.getUint32(index, littleEndian); - fields.index += 4; + const index = state.offset + state.index; + const value = state.data.getUint32(index, littleEndian); + state.index += 4; return value; } @@ -240,11 +237,11 @@ export class BufferReader { * @returns {number} */ readFloat64(littleEndian = false) { - const fields = privateFieldsGet(this); + const state = this.#state; this.assertCanRead(8); - const index = fields.offset + fields.index; - const value = fields.data.getFloat64(index, littleEndian); - fields.index += 8; + const index = state.offset + state.index; + const value = state.data.getFloat64(index, littleEndian); + state.index += 8; return value; } @@ -253,8 +250,8 @@ export class BufferReader { * @returns {number} */ byteAt(index) { - const fields = privateFieldsGet(this); - return fields.bytes[fields.offset + index]; + const state = this.#state; + return state.bytes[state.offset + index]; } /** @@ -264,10 +261,10 @@ export class BufferReader { */ bytesAt(index, size) { this.assertCanSeek(index + size); - const fields = privateFieldsGet(this); - return fields.bytes.subarray( - fields.offset + index, - fields.offset + index + size, + const state = this.#state; + return state.bytes.subarray( + state.offset + index, + state.offset + index + size, ); } @@ -275,8 +272,8 @@ export class BufferReader { * @param {number} offset */ skip(offset) { - const fields = privateFieldsGet(this); - this.seek(fields.index + offset); + const state = this.#state; + this.seek(state.index + offset); } /** @@ -284,11 +281,11 @@ export class BufferReader { * @returns {boolean} */ expect(expected) { - const fields = privateFieldsGet(this); - if (!this.matchAt(fields.index, expected)) { + const state = this.#state; + if (!this.matchAt(state.index, expected)) { return false; } - fields.index += expected.length; + state.index += expected.length; return true; } @@ -298,8 +295,8 @@ export class BufferReader { * @returns {boolean} */ matchAt(index, expected) { - const fields = privateFieldsGet(this); - if (index + expected.length > fields.length || index < 0) { + const state = this.#state; + if (index + expected.length > state.length || index < 0) { return false; } for (let i = 0; i < expected.length; i += 1) { @@ -314,10 +311,10 @@ export class BufferReader { * @param {Uint8Array} expected */ assert(expected) { - const fields = privateFieldsGet(this); + const state = this.#state; if (!this.expect(expected)) { throw Error( - `Expected ${q(expected)} at ${fields.index}, got ${this.peek( + `Expected ${q(expected)} at ${state.index}, got ${this.peek( expected.length, )}`, ); @@ -329,8 +326,8 @@ export class BufferReader { * @returns {number} */ findLast(expected) { - const fields = privateFieldsGet(this); - let index = fields.length - expected.length; + const state = this.#state; + let index = state.length - expected.length; while (index >= 0 && !this.matchAt(index, expected)) { index -= 1; } diff --git a/packages/syrup/src/buffer-writer.js b/packages/syrup/src/buffer-writer.js index 75efd6e834..d7dc73809f 100644 --- a/packages/syrup/src/buffer-writer.js +++ b/packages/syrup/src/buffer-writer.js @@ -4,26 +4,14 @@ const textEncoder = new TextEncoder(); /** - * @type {WeakMap} + * }} BufferWriterState */ -const privateFields = new WeakMap(); - -/** - * @param {BufferWriter} self - */ -const getPrivateFields = self => { - const fields = privateFields.get(self); - if (!fields) { - throw Error('BufferWriter fields are not initialized'); - } - return fields; -}; const assertNatNumber = n => { if (Number.isSafeInteger(n) && /** @type {number} */ (n) >= 0) { @@ -33,18 +21,21 @@ const assertNatNumber = n => { }; export class BufferWriter { + /** @type {BufferWriterState} */ + #state; + /** * @returns {number} */ get length() { - return getPrivateFields(this).length; + return this.#state.length; } /** * @returns {number} */ get index() { - return getPrivateFields(this).index; + return this.#state.index; } /** @@ -60,13 +51,13 @@ export class BufferWriter { constructor(capacity = 16) { const bytes = new Uint8Array(capacity); const data = new DataView(bytes.buffer); - privateFields.set(this, { + this.#state = { bytes, data, index: 0, length: 0, capacity, - }); + }; } /** @@ -74,8 +65,8 @@ export class BufferWriter { */ ensureCanSeek(required) { assertNatNumber(required); - const fields = getPrivateFields(this); - let capacity = fields.capacity; + const state = this.#state; + let capacity = state.capacity; if (capacity >= required) { return; } @@ -84,20 +75,20 @@ export class BufferWriter { } const bytes = new Uint8Array(capacity); const data = new DataView(bytes.buffer); - bytes.set(fields.bytes.subarray(0, fields.length)); - fields.bytes = bytes; - fields.data = data; - fields.capacity = capacity; + bytes.set(state.bytes.subarray(0, state.length)); + state.bytes = bytes; + state.data = data; + state.capacity = capacity; } /** * @param {number} index */ seek(index) { - const fields = getPrivateFields(this); + const state = this.#state; this.ensureCanSeek(index); - fields.index = index; - fields.length = Math.max(fields.index, fields.length); + state.index = index; + state.length = Math.max(state.index, state.length); } /** @@ -105,19 +96,19 @@ export class BufferWriter { */ ensureCanWrite(size) { assertNatNumber(size); - const fields = getPrivateFields(this); - this.ensureCanSeek(fields.index + size); + const state = this.#state; + this.ensureCanSeek(state.index + size); } /** * @param {Uint8Array} bytes */ write(bytes) { - const fields = getPrivateFields(this); + const state = this.#state; this.ensureCanWrite(bytes.byteLength); - fields.bytes.set(bytes, fields.index); - fields.index += bytes.byteLength; - fields.length = Math.max(fields.index, fields.length); + state.bytes.set(bytes, state.index); + state.index += bytes.byteLength; + state.length = Math.max(state.index, state.length); } /** @@ -138,23 +129,23 @@ export class BufferWriter { writeCopy(start, end) { assertNatNumber(start); assertNatNumber(end); - const fields = getPrivateFields(this); + const state = this.#state; const size = end - start; this.ensureCanWrite(size); - fields.bytes.copyWithin(fields.index, start, end); - fields.index += size; - fields.length = Math.max(fields.index, fields.length); + state.bytes.copyWithin(state.index, start, end); + state.index += size; + state.length = Math.max(state.index, state.length); } /** * @param {number} value */ writeUint8(value) { - const fields = getPrivateFields(this); + const state = this.#state; this.ensureCanWrite(1); - fields.data.setUint8(fields.index, value); - fields.index += 1; - fields.length = Math.max(fields.index, fields.length); + state.data.setUint8(state.index, value); + state.index += 1; + state.length = Math.max(state.index, state.length); } /** @@ -162,12 +153,11 @@ export class BufferWriter { * @param {boolean=} littleEndian */ writeUint16(value, littleEndian) { - const fields = getPrivateFields(this); + const state = this.#state; this.ensureCanWrite(2); - const index = fields.index; - fields.data.setUint16(index, value, littleEndian); - fields.index += 2; - fields.length = Math.max(fields.index, fields.length); + state.data.setUint16(state.index, value, littleEndian); + state.index += 2; + state.length = Math.max(state.index, state.length); } /** @@ -175,12 +165,11 @@ export class BufferWriter { * @param {boolean=} littleEndian */ writeUint32(value, littleEndian) { - const fields = getPrivateFields(this); + const state = this.#state; this.ensureCanWrite(4); - const index = fields.index; - fields.data.setUint32(index, value, littleEndian); - fields.index += 4; - fields.length = Math.max(fields.index, fields.length); + state.data.setUint32(state.index, value, littleEndian); + state.index += 4; + state.length = Math.max(state.index, state.length); } /** @@ -188,12 +177,11 @@ export class BufferWriter { * @param {boolean=} littleEndian */ writeFloat64(value, littleEndian) { - const fields = getPrivateFields(this); + const state = this.#state; this.ensureCanWrite(8); - const index = fields.index; - fields.data.setFloat64(index, value, littleEndian); - fields.index += 8; - fields.length = Math.max(fields.index, fields.length); + state.data.setFloat64(state.index, value, littleEndian); + state.index += 8; + state.length = Math.max(state.index, state.length); } /** @@ -210,8 +198,8 @@ export class BufferWriter { * @returns {Uint8Array} */ subarray(begin, end) { - const fields = getPrivateFields(this); - return fields.bytes.subarray(0, fields.length).subarray(begin, end); + const state = this.#state; + return state.bytes.subarray(0, state.length).subarray(begin, end); } /** From 5775c0e0b2554f4f2b34ad52c29aa1edbb4757ba Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 14 Apr 2025 18:46:32 -1000 Subject: [PATCH 28/31] fix(syrup): ocapn tagged can contain any Passable value --- packages/syrup/src/ocapn/passable.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/syrup/src/ocapn/passable.js b/packages/syrup/src/ocapn/passable.js index ed919e6892..ef3089445c 100644 --- a/packages/syrup/src/ocapn/passable.js +++ b/packages/syrup/src/ocapn/passable.js @@ -81,8 +81,9 @@ const OCapNTaggedCodec = makeOCapNRecordCodec( // readBody syrupReader => { const tagName = syrupReader.readSelectorAsString(); - // @ts-expect-error any type - const value = syrupReader.readOfType('any'); + // Value can be any Passable. + /* eslint-disable-next-line no-use-before-define */ + const value = OCapNPassableUnionCodec.read(syrupReader); return { [Symbol.for('passStyle')]: 'tagged', [Symbol.toStringTag]: tagName, From f55a43c4fc516965fc7581d5b0148c28ea4ac267 Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 14 Apr 2025 18:52:51 -1000 Subject: [PATCH 29/31] feat(syrup): ocapn struct codec --- packages/syrup/src/ocapn/passable.js | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/syrup/src/ocapn/passable.js b/packages/syrup/src/ocapn/passable.js index ef3089445c..bc17936267 100644 --- a/packages/syrup/src/ocapn/passable.js +++ b/packages/syrup/src/ocapn/passable.js @@ -65,14 +65,31 @@ const AtomCodecs = { // OCapN Passable Containers -// TODO: dictionary but with only string keys /** @type {SyrupCodec} */ export const OCapNStructCodec = { read(syrupReader) { - throw Error('OCapNStructCodec: read must be implemented'); + syrupReader.enterDictionary(); + const result = {}; + while (!syrupReader.peekDictionaryEnd()) { + // OCapN Structs are always string keys. + const key = syrupReader.readString(); + // Value can be any Passable. + /* eslint-disable-next-line no-use-before-define */ + const value = OCapNPassableUnionCodec.read(syrupReader); + result[key] = value; + } + syrupReader.exitDictionary(); + return result; }, write(value, syrupWriter) { - throw Error('OCapNStructCodec: write must be implemented'); + syrupWriter.enterDictionary(); + for (const [key, structValue] of Object.entries(value)) { + syrupWriter.writeString(key); + // Value can be any Passable. + /* eslint-disable-next-line no-use-before-define */ + OCapNPassableUnionCodec.write(structValue, syrupWriter); + } + syrupWriter.exitDictionary(); }, }; From d70c21cb37fb5ea1a6c0a76b1d219cde20435d3a Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 14 Apr 2025 18:57:09 -1000 Subject: [PATCH 30/31] fix(syrup): remove underscore from unused vars --- packages/syrup/src/decode.js | 10 ++++++---- packages/syrup/src/ocapn/passable.js | 6 +++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/syrup/src/decode.js b/packages/syrup/src/decode.js index b36b91e293..d0530b39d5 100644 --- a/packages/syrup/src/decode.js +++ b/packages/syrup/src/decode.js @@ -278,8 +278,9 @@ function readListBody(bufferReader, name) { * @param {string} name * @returns {any[]} */ -// eslint-disable-next-line no-underscore-dangle -function _readList(bufferReader, name) { +// Provided for completeness, but not used. +// eslint-disable-next-line no-unused-vars +function readList(bufferReader, name) { const cc = bufferReader.readByte(); if (cc !== LIST_START) { throw Error( @@ -376,8 +377,9 @@ function readDictionaryBody(bufferReader, name) { * @param {BufferReader} bufferReader * @param {string} name */ -// eslint-disable-next-line no-underscore-dangle -function _readDictionary(bufferReader, name) { +// Provided for completeness, but not used. +// eslint-disable-next-line no-unused-vars +function readDictionary(bufferReader, name) { const start = bufferReader.index; const cc = bufferReader.readByte(); if (cc !== DICT_START) { diff --git a/packages/syrup/src/ocapn/passable.js b/packages/syrup/src/ocapn/passable.js index bc17936267..69e8172aa0 100644 --- a/packages/syrup/src/ocapn/passable.js +++ b/packages/syrup/src/ocapn/passable.js @@ -143,9 +143,9 @@ const OCapNErrorCodec = makeOCapNRecordCodecFromDefinition('desc:error', [ ['message', 'string'], ]); -// provided for completeness -// eslint-disable-next-line no-underscore-dangle -const _OCapNPassableCodecs = { +// Provided for completeness, but not used. +// eslint-disable-next-line no-unused-vars +const OCapNPassableCodecs = { ...AtomCodecs, ...ContainerCodecs, ...OCapNReferenceCodecs, From a630077c9290ec793b2b6e80726cc5dd3d5a1ad3 Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 14 Apr 2025 19:00:59 -1000 Subject: [PATCH 31/31] fix(syrup): syrup writer exposes getBytes function --- packages/syrup/src/encode.js | 39 +++++++++++++++----------- packages/syrup/src/ocapn/components.js | 2 +- packages/syrup/test/codec.test.js | 4 +-- packages/syrup/test/ocapn.test.js | 5 +--- 4 files changed, 27 insertions(+), 23 deletions(-) diff --git a/packages/syrup/src/encode.js b/packages/syrup/src/encode.js index 44a92661b4..f970eb4607 100644 --- a/packages/syrup/src/encode.js +++ b/packages/syrup/src/encode.js @@ -248,68 +248,71 @@ function writeAny(bufferWriter, value, path, pathSuffix) { } export class SyrupWriter { + /** @type {BufferWriter} */ + #bufferWriter; + constructor(bufferWriter) { - this.bufferWriter = bufferWriter; + this.#bufferWriter = bufferWriter; } writeAny(value) { - writeAny(this.bufferWriter, value, [], '/'); + writeAny(this.#bufferWriter, value, [], '/'); } writeSelector(value) { - writeSelector(this.bufferWriter, value); + writeSelector(this.#bufferWriter, value); } writeString(value) { - writeString(this.bufferWriter, value); + writeString(this.#bufferWriter, value); } writeBytestring(value) { - writeBytestring(this.bufferWriter, value); + writeBytestring(this.#bufferWriter, value); } writeBoolean(value) { - writeBoolean(this.bufferWriter, value); + writeBoolean(this.#bufferWriter, value); } writeInteger(value) { - writeInteger(this.bufferWriter, value); + writeInteger(this.#bufferWriter, value); } writeFloat64(value) { - writeFloat64(this.bufferWriter, value); + writeFloat64(this.#bufferWriter, value); } enterRecord() { - this.bufferWriter.writeByte(RECORD_START); + this.#bufferWriter.writeByte(RECORD_START); } exitRecord() { - this.bufferWriter.writeByte(RECORD_END); + this.#bufferWriter.writeByte(RECORD_END); } enterList() { - this.bufferWriter.writeByte(LIST_START); + this.#bufferWriter.writeByte(LIST_START); } exitList() { - this.bufferWriter.writeByte(LIST_END); + this.#bufferWriter.writeByte(LIST_END); } enterDictionary() { - this.bufferWriter.writeByte(DICT_START); + this.#bufferWriter.writeByte(DICT_START); } exitDictionary() { - this.bufferWriter.writeByte(DICT_END); + this.#bufferWriter.writeByte(DICT_END); } enterSet() { - this.bufferWriter.writeByte(SET_START); + this.#bufferWriter.writeByte(SET_START); } exitSet() { - this.bufferWriter.writeByte(SET_END); + this.#bufferWriter.writeByte(SET_END); } /** @@ -340,6 +343,10 @@ export class SyrupWriter { throw Error(`writeTypeOf: unknown type ${typeof value}`); } } + + getBytes() { + return this.#bufferWriter.subarray(0, this.#bufferWriter.length); + } } /** diff --git a/packages/syrup/src/ocapn/components.js b/packages/syrup/src/ocapn/components.js index 86d718d796..2a2b74fd60 100644 --- a/packages/syrup/src/ocapn/components.js +++ b/packages/syrup/src/ocapn/components.js @@ -72,5 +72,5 @@ export const readOCapComponent = syrupReader => { export const writeOCapComponent = (component, syrupWriter) => { OCapNComponentUnionCodec.write(component, syrupWriter); - return syrupWriter.bufferWriter.subarray(0, syrupWriter.bufferWriter.length); + return syrupWriter.getBytes(); }; diff --git a/packages/syrup/test/codec.test.js b/packages/syrup/test/codec.test.js index e68d7a488a..f2a71f9065 100644 --- a/packages/syrup/test/codec.test.js +++ b/packages/syrup/test/codec.test.js @@ -26,7 +26,7 @@ const zooBin = Uint8Array.from(zooBinRaw); const testCodecBidirectionally = (t, codec, value) => { const writer = makeSyrupWriter(); codec.write(value, writer); - const bytes = writer.bufferWriter.subarray(0, writer.bufferWriter.length); + const bytes = writer.getBytes(); const reader = makeSyrupReader(bytes); const result = codec.read(reader); t.deepEqual(result, value); @@ -159,7 +159,7 @@ test('zoo.bin', t => { }); const writer = makeSyrupWriter(); zooCodec.write(value, writer); - const bytes = writer.bufferWriter.subarray(0, writer.bufferWriter.length); + const bytes = writer.getBytes(); const resultSyrup = textDecoder.decode(bytes); const originalSyrup = textDecoder.decode(zooBin); t.deepEqual(resultSyrup, originalSyrup); diff --git a/packages/syrup/test/ocapn.test.js b/packages/syrup/test/ocapn.test.js index 7818757583..c366174faf 100644 --- a/packages/syrup/test/ocapn.test.js +++ b/packages/syrup/test/ocapn.test.js @@ -28,10 +28,7 @@ const testBidirectionally = (t, codec, syrup, value, testName) => { t.notThrows(() => { codec.write(value, syrupWriter); }, testName); - const bytes2 = syrupWriter.bufferWriter.subarray( - 0, - syrupWriter.bufferWriter.length, - ); + const bytes2 = syrupWriter.getBytes(); const syrup2 = new TextDecoder().decode(bytes2); t.deepEqual(syrup2, syrup, testName); };