UNPKG

binary-layout

Version:

Typescript-native, declarative DSL for working with binary data

1,081 lines (1,073 loc) 40.5 kB
// src/layout.ts var binaryLiterals = ["int", "uint", "bytes", "array", "switch"]; var defaultEndianness = "big"; var numberMaxSize = 6; // src/utils.ts var isNumType = (x) => typeof x === "number" || typeof x === "bigint"; var isBytesType = (x) => x instanceof Uint8Array; var isPrimitiveType = (x) => isNumType(x) || isBytesType(x); var isItem = (x) => binaryLiterals.includes(x?.binary); var isLayout = (x) => isItem(x) || Array.isArray(x) && x.every(isItem); var isFixedNumberConversion = (custom) => typeof custom?.from === "number"; var isFixedBigintConversion = (custom) => typeof custom?.from === "bigint"; var isFixedUintConversion = (custom) => isFixedNumberConversion(custom) || isFixedBigintConversion(custom); var isFixedBytesConversion = (custom) => isBytesType(custom?.from); var isFixedPrimitiveConversion = (custom) => isFixedUintConversion(custom) || isFixedBytesConversion(custom); var checkSize = (layoutSize, dataSize) => { if (layoutSize !== dataSize) throw new Error(`size mismatch: layout size: ${layoutSize}, data size: ${dataSize}`); return dataSize; }; var bytesItemHasLayout = (bytesItem) => "layout" in bytesItem && bytesItem.layout !== void 0; var checkItemSize = (item, dataSize) => "size" in item && item.size !== void 0 ? checkSize(item.size, dataSize) : dataSize; var checkNumEquals = (custom, data) => { if (custom != data) throw new Error(`value mismatch: (constant) layout value: ${custom}, data value: ${data}`); }; var checkBytesTypeEqual = (custom, data, opts) => { const toSlice = (bytes, slice) => slice === void 0 ? [0, bytes.length] : Array.isArray(slice) ? slice : [slice, bytes.length]; const [customStart, customEnd] = toSlice(custom, opts?.customSlice); const [dataStart, dataEnd] = toSlice(data, opts?.dataSlice); const length = customEnd - customStart; checkSize(length, dataEnd - dataStart); for (let i = 0; i < custom.length; ++i) if (custom[i + customStart] !== data[i + dataStart]) throw new Error( `binary data mismatch: layout value: ${custom}, offset: ${customStart}, data value: ${data}, offset: ${dataStart}` ); }; function findIdLayoutPair(item, data) { const id = data[item.idTag ?? "id"]; return item.layouts.find( ([idOrConversionId]) => (Array.isArray(idOrConversionId) ? idOrConversionId[1] : idOrConversionId) == id ); } // src/size.ts function calcSize(layout, data) { const size = internalCalcSize(layout, data); if (size === null) throw new Error( `coding error: couldn't calculate layout size for layout ${layout} with data ${data}` ); return size; } function calcStaticSize(layout) { return internalCalcSize(layout, staticCalc); } var staticCalc = Symbol("staticCalc"); function calcSizeForSerialization(layout, data) { const bytesConversions = []; const size = internalCalcSize(layout, data, bytesConversions); if (size === null) throw new Error( `coding error: couldn't calculate layout size for layout ${layout} with data ${data}` ); return [size, bytesConversions]; } function calcItemSize(item, data, bytesConversions) { const storeInCache = (cachedFrom) => { if (bytesConversions !== void 0) bytesConversions.push(cachedFrom); return cachedFrom; }; switch (item.binary) { case "int": case "uint": return item.size; case "bytes": { if ("size" in item && data === staticCalc) return item.size; const lengthSize = "lengthSize" in item ? item.lengthSize | 0 : 0; if (bytesItemHasLayout(item)) { const { custom: custom2 } = item; const layoutSize = internalCalcSize( item.layout, custom2 === void 0 ? data : typeof custom2.from === "function" ? data !== staticCalc ? storeInCache(custom2.from(data)) : staticCalc : custom2.from, //flex layout bytes only allows conversions, not fixed values bytesConversions ); if (layoutSize === null) return "size" in item ? item.size ?? null : null; return lengthSize + checkItemSize(item, layoutSize); } const { custom } = item; if (isBytesType(custom)) return lengthSize + custom.length; if (isFixedBytesConversion(custom)) return lengthSize + custom.from.length; if (data === staticCalc) return null; return lengthSize + checkItemSize( item, custom !== void 0 ? storeInCache(custom.from(data)).length : data.length ); } case "array": { const length = "length" in item ? item.length : void 0; if (data === staticCalc) { if (length !== void 0) { const layoutSize = internalCalcSize(item.layout, staticCalc, bytesConversions); return layoutSize !== null ? length * layoutSize : null; } return null; } let size = 0; if (length !== void 0 && length !== data.length) throw new Error( `array length mismatch: layout length: ${length}, data length: ${data.length}` ); else if ("lengthSize" in item && item.lengthSize !== void 0) size += item.lengthSize; for (let i = 0; i < data.length; ++i) { const entrySize = internalCalcSize(item.layout, data[i], bytesConversions); if (entrySize === null) return null; size += entrySize; } return size; } case "switch": { if (data !== staticCalc) { const [_, layout] = findIdLayoutPair(item, data); const layoutSize = internalCalcSize(layout, data, bytesConversions); return layoutSize !== null ? item.idSize + layoutSize : null; } let size = null; for (const [_, layout] of item.layouts) { const layoutSize = internalCalcSize(layout, staticCalc, bytesConversions); if (size === null) size = layoutSize; else if (layoutSize !== size) return null; } return item.idSize + size; } } } function internalCalcSize(layout, data, bytesConversions) { if (isItem(layout)) return calcItemSize(layout, data, bytesConversions); let size = 0; for (const item of layout) { let itemData; if (data === staticCalc) itemData = staticCalc; else if (!("omit" in item) || !item.omit) { if (!(item.name in data)) throw new Error(`missing data for layout item: ${item.name}`); itemData = data[item.name]; } const itemSize = calcItemSize(item, itemData, bytesConversions); if (itemSize === null) { if (data !== staticCalc) throw new Error(`coding error: couldn't calculate size for layout item: ${item.name}`); return null; } size += itemSize; } return size; } // src/serialize.ts var cursorWrite = (cursor, bytes) => { cursor.bytes.set(bytes, cursor.offset); cursor.offset += bytes.length; }; var bcqGetNext = (bcq) => bcq.bytesConversions[bcq.position++]; function serialize(layout, data, encoded) { const [size, bytesConversions] = calcSizeForSerialization(layout, data); const cursor = { bytes: encoded ?? new Uint8Array(size), offset: 0 }; internalSerialize(layout, data, cursor, { bytesConversions, position: 0 }); if (!encoded && cursor.offset !== cursor.bytes.length) throw new Error( `encoded data is shorter than expected: ${cursor.bytes.length} > ${cursor.offset}` ); return encoded ? cursor.offset : cursor.bytes; } var maxAllowedNumberVal = 2 ** (numberMaxSize * 8); function serializeNum(val, size, cursor, endianness = defaultEndianness, signed = false) { if (!signed && val < 0) throw new Error(`Value ${val} is negative but unsigned`); if (typeof val === "number") { if (!Number.isInteger(val)) throw new Error(`Value ${val} is not an integer`); if (size > numberMaxSize) { if (val >= maxAllowedNumberVal) throw new Error(`Value ${val} is too large to be safely converted into an integer`); if (signed && val < -maxAllowedNumberVal) throw new Error(`Value ${val} is too small to be safely converted into an integer`); } } const bound = 2n ** BigInt(size * 8 - (signed ? 1 : 0)); if (val >= bound) throw new Error(`Value ${val} is too large for ${size} bytes`); if (signed && val < -bound) throw new Error(`Value ${val} is too small for ${size} bytes`); for (let i = 0; i < size; ++i) cursor.bytes[cursor.offset + i] = Number(BigInt(val) >> BigInt(8 * (endianness === "big" ? size - i - 1 : i)) & 0xffn); cursor.offset += size; } function internalSerialize(layout, data, cursor, bcq) { if (isItem(layout)) serializeItem(layout, data, cursor, bcq); else for (const item of layout) try { serializeItem(item, data[item.name], cursor, bcq); } catch (e) { e.message = `when serializing item '${item.name}': ${e.message}`; throw e; } } function serializeItem(item, data, cursor, bcq) { switch (item.binary) { case "int": case "uint": { const value = (() => { if (isNumType(item.custom)) { if (!("omit" in item && item.omit)) checkNumEquals(item.custom, data); return item.custom; } if (isNumType(item?.custom?.from)) return item.custom.from; return item.custom !== void 0 ? item.custom.from(data) : data; })(); serializeNum(value, item.size, cursor, item.endianness, item.binary === "int"); break; } case "bytes": { const offset = cursor.offset; if ("lengthSize" in item && item.lengthSize !== void 0) cursor.offset += item.lengthSize; if (bytesItemHasLayout(item)) { const { custom } = item; let layoutData; if (custom === void 0) layoutData = data; else if (typeof custom.from !== "function") layoutData = custom.from; else layoutData = bcqGetNext(bcq); internalSerialize(item.layout, layoutData, cursor, bcq); } else { const { custom } = item; if (isBytesType(custom)) { if (!("omit" in item && item.omit)) checkBytesTypeEqual(custom, data); cursorWrite(cursor, custom); } else if (isFixedBytesConversion(custom)) cursorWrite(cursor, custom.from); else cursorWrite(cursor, custom !== void 0 ? bcqGetNext(bcq) : data); } if ("lengthSize" in item && item.lengthSize !== void 0) { const itemSize = cursor.offset - offset - item.lengthSize; const curOffset = cursor.offset; cursor.offset = offset; serializeNum(itemSize, item.lengthSize, cursor, item.lengthEndianness); cursor.offset = curOffset; } else checkItemSize(item, cursor.offset - offset); break; } case "array": { if ("length" in item && item.length !== data.length) throw new Error( `array length mismatch: layout length: ${item.length}, data length: ${data.length}` ); if ("lengthSize" in item && item.lengthSize !== void 0) serializeNum(data.length, item.lengthSize, cursor, item.lengthEndianness); for (let i = 0; i < data.length; ++i) internalSerialize(item.layout, data[i], cursor, bcq); break; } case "switch": { const [idOrConversionId, layout] = findIdLayoutPair(item, data); const idNum = Array.isArray(idOrConversionId) ? idOrConversionId[0] : idOrConversionId; serializeNum(idNum, item.idSize, cursor, item.idEndianness); internalSerialize(layout, data, cursor, bcq); break; } } } function getCachedSerializedFrom(item) { const custom = item.custom; if (!("cachedSerializedFrom" in custom)) { custom.cachedSerializedFrom = serialize(item.layout, custom.from); if ("size" in item && item.size !== void 0 && item.size !== custom.cachedSerializedFrom.length) throw new Error( `Layout specification error: custom.from does not serialize to specified size` ); } return custom.cachedSerializedFrom; } // src/deserialize.ts function deserialize(layout, bytes, consumeAll) { const boolConsumeAll = consumeAll ?? true; const encoded = { bytes, offset: 0, end: bytes.length }; const decoded = internalDeserialize(layout, encoded); if (boolConsumeAll && encoded.offset !== encoded.end) throw new Error(`encoded data is longer than expected: ${encoded.end} > ${encoded.offset}`); return boolConsumeAll ? decoded : [decoded, encoded.offset]; } function updateOffset(encoded, size) { const newOffset = encoded.offset + size; if (newOffset > encoded.end) throw new Error(`chunk is shorter than expected: ${encoded.end} < ${newOffset}`); encoded.offset = newOffset; } function internalDeserialize(layout, encoded) { if (!Array.isArray(layout)) return deserializeItem(layout, encoded); let decoded = {}; for (const item of layout) try { (item.omit ? {} : decoded)[item.name] = deserializeItem(item, encoded); } catch (e) { e.message = `when deserializing item '${item.name}': ${e.message}`; throw e; } return decoded; } function deserializeNum(encoded, size, endianness = defaultEndianness, signed = false) { let val = 0n; for (let i = 0; i < size; ++i) val |= BigInt(encoded.bytes[encoded.offset + i]) << BigInt(8 * (endianness === "big" ? size - i - 1 : i)); if (signed && encoded.bytes[encoded.offset + (endianness === "big" ? 0 : size - 1)] & 128) val -= 1n << BigInt(8 * size); updateOffset(encoded, size); return size > numberMaxSize ? val : Number(val); } function deserializeItem(item, encoded) { switch (item.binary) { case "int": case "uint": { const value = deserializeNum(encoded, item.size, item.endianness, item.binary === "int"); const { custom } = item; if (isNumType(custom)) { checkNumEquals(custom, value); return custom; } if (isNumType(custom?.from)) { checkNumEquals(custom.from, value); return custom.to; } return custom !== void 0 ? custom.to(value) : value; } case "bytes": { const expectedSize = "lengthSize" in item && item.lengthSize !== void 0 ? deserializeNum(encoded, item.lengthSize, item.lengthEndianness) : item?.size; if (bytesItemHasLayout(item)) { const { custom: custom2 } = item; const offset = encoded.offset; let layoutData; if (expectedSize === void 0) layoutData = internalDeserialize(item.layout, encoded); else { const subChunk = { ...encoded, end: encoded.offset + expectedSize }; updateOffset(encoded, expectedSize); layoutData = internalDeserialize(item.layout, subChunk); if (subChunk.offset !== subChunk.end) throw new Error( `read less data than expected: ${subChunk.offset - encoded.offset} < ${expectedSize}` ); } if (custom2 !== void 0) { if (typeof custom2.from !== "function") { checkBytesTypeEqual( getCachedSerializedFrom(item), encoded.bytes, { dataSlice: [offset, encoded.offset] } ); return custom2.to; } return custom2.to(layoutData); } return layoutData; } const { custom } = item; { let fixedFrom; let fixedTo; if (isBytesType(custom)) fixedFrom = custom; else if (isFixedBytesConversion(custom)) { fixedFrom = custom.from; fixedTo = custom.to; } if (fixedFrom !== void 0) { const size = expectedSize ?? fixedFrom.length; const value2 = encoded.bytes.subarray(encoded.offset, encoded.offset + size); checkBytesTypeEqual(fixedFrom, value2); updateOffset(encoded, size); return fixedTo ?? fixedFrom; } } const start = encoded.offset; const end = expectedSize !== void 0 ? encoded.offset + expectedSize : encoded.end; updateOffset(encoded, end - start); const value = encoded.bytes.subarray(start, end); return custom !== void 0 ? custom.to(value) : value; } case "array": { let ret = []; const { layout } = item; const deserializeArrayItem = () => { const deserializedItem = internalDeserialize(layout, encoded); ret.push(deserializedItem); }; let length = null; if ("length" in item && item.length !== void 0) length = item.length; else if ("lengthSize" in item && item.lengthSize !== void 0) length = deserializeNum(encoded, item.lengthSize, item.lengthEndianness); if (length !== null) for (let i = 0; i < length; ++i) deserializeArrayItem(); else while (encoded.offset < encoded.end) deserializeArrayItem(); return ret; } case "switch": { const id = deserializeNum(encoded, item.idSize, item.idEndianness); const { layouts } = item; if (layouts.length === 0) throw new Error(`switch item has no layouts`); const hasPlainIds = typeof layouts[0][0] === "number"; const pair = layouts.find(([idOrConversionId2]) => hasPlainIds ? idOrConversionId2 === id : idOrConversionId2[0] === id); if (pair === void 0) throw new Error(`unknown id value: ${id}`); const [idOrConversionId, idLayout] = pair; const decoded = internalDeserialize(idLayout, encoded); return { [item.idTag ?? "id"]: hasPlainIds ? id : idOrConversionId[1], ...decoded }; } } } // src/fixedDynamic.ts var fixedItemsOf = (layout) => filterItemsOf(layout, true); var dynamicItemsOf = (layout) => filterItemsOf(layout, false); function addFixedValues(layout, dynamicValues) { return internalAddFixedValues(layout, dynamicValues); } function filterItem(item, fixed) { switch (item.binary) { // @ts-ignore - fallthrough is intentional case "bytes": { if (bytesItemHasLayout(item)) { const { custom } = item; if (custom === void 0) { const { layout } = item; if (isItem(layout)) return filterItem(layout, fixed); const filteredItems = internalFilterItemsOfProperLayout(layout, fixed); return filteredItems.length > 0 ? { ...item, layout: filteredItems } : null; } const isFixedItem = typeof custom.from !== "function"; return fixed && isFixedItem || !fixed && !isFixedItem ? item : null; } } case "int": case "uint": { const { custom } = item; const isFixedItem = isPrimitiveType(custom) || isFixedPrimitiveConversion(custom); return fixed && isFixedItem || !fixed && !isFixedItem ? item : null; } case "array": { const filtered = internalFilterItemsOf(item.layout, fixed); return filtered !== null ? { ...item, layout: filtered } : null; } case "switch": { const filteredIdLayoutPairs = item.layouts.reduce( (acc, [idOrConversionId, idLayout]) => { const filteredItems = internalFilterItemsOfProperLayout(idLayout, fixed); return filteredItems.length > 0 ? [...acc, [idOrConversionId, filteredItems]] : acc; }, [] ); return { ...item, layouts: filteredIdLayoutPairs }; } } } function internalFilterItemsOfProperLayout(proper, fixed) { return proper.reduce( (acc, item) => { const filtered = filterItem(item, fixed); return filtered !== null ? [...acc, filtered] : acc; }, [] ); } function internalFilterItemsOf(layout, fixed) { return Array.isArray(layout) ? internalFilterItemsOfProperLayout(layout, fixed) : filterItem(layout, fixed); } function filterItemsOf(layout, fixed) { return internalFilterItemsOf(layout, fixed); } function internalAddFixedValuesItem(item, dynamicValue) { switch (item.binary) { // @ts-ignore - fallthrough is intentional case "bytes": { if (bytesItemHasLayout(item)) { const { custom } = item; if (custom === void 0 || typeof custom.from !== "function") return internalAddFixedValues(item.layout, custom ? custom.from : dynamicValue); return dynamicValue; } } case "int": case "uint": { const { custom } = item; return item?.omit ? void 0 : isPrimitiveType(custom) ? custom : isFixedPrimitiveConversion(custom) ? custom.to : dynamicValue; } case "array": return Array.isArray(dynamicValue) ? dynamicValue.map((element) => internalAddFixedValues(item.layout, element)) : void 0; case "switch": { const id = dynamicValue[item.idTag ?? "id"]; const [_, idLayout] = item.layouts.find( ([idOrConversionId]) => (Array.isArray(idOrConversionId) ? idOrConversionId[1] : idOrConversionId) == id ); return { [item.idTag ?? "id"]: id, ...internalAddFixedValues(idLayout, dynamicValue) }; } } } function internalAddFixedValues(layout, dynamicValues) { dynamicValues = dynamicValues ?? {}; if (isItem(layout)) return internalAddFixedValuesItem(layout, dynamicValues); const ret = {}; for (const item of layout) { const fixedVals = internalAddFixedValuesItem( item, dynamicValues[item.name] ?? {} ); if (fixedVals !== void 0) ret[item.name] = fixedVals; } return ret; } // src/discriminate.ts function buildDiscriminator(layouts, allowAmbiguous) { const [distinguishable, discriminator] = internalBuildDiscriminator(layouts); if (!distinguishable && !allowAmbiguous) throw new Error("Cannot uniquely distinguished the given layouts"); return !allowAmbiguous ? (encoded) => { const layout = discriminator(encoded); return layout.length === 0 ? null : layout[0]; } : discriminator; } function arrayToBitset(arr) { return arr.reduce((bit, i) => bit | BigInt(1) << BigInt(i), BigInt(0)); } function bitsetToArray(bitset) { const ret = []; for (let i = 0n; bitset > 0n; bitset >>= 1n, ++i) if (bitset & 1n) ret.push(Number(i)); return ret; } function count(candidates) { let count2 = 0; for (; candidates > 0n; candidates >>= 1n) count2 += Number(candidates & 1n); return count2; } var lengthSizeMax = (lengthSize) => lengthSize > 0 ? 2 ** (8 * lengthSize) - 1 : Infinity; function layoutItemMeta(item, offset, fixedBytes) { switch (item.binary) { case "int": case "uint": { const fixedVal = isNumType(item.custom) ? item.custom : isNumType(item?.custom?.from) ? item.custom.from : null; if (fixedVal !== null && offset !== null) { const cursor = { bytes: new Uint8Array(item.size), offset: 0 }; serializeNum(fixedVal, item.size, cursor, item.endianness, item.binary === "int"); fixedBytes.push([offset, cursor.bytes]); } return [item.size, item.size]; } case "bytes": { const lengthSize = "lengthSize" in item ? item.lengthSize | 0 : 0; let fixed; let fixedSize; if (bytesItemHasLayout(item)) { const { custom } = item; if (custom !== void 0 && typeof custom.from !== "function") { fixed = getCachedSerializedFrom(item); fixedSize = fixed.length; } else { const layoutSize = calcStaticSize(item.layout); if (layoutSize !== null) fixedSize = layoutSize; } } else { const { custom } = item; if (isBytesType(custom)) { fixed = custom; fixedSize = custom.length; } else if (isFixedBytesConversion(custom)) { fixed = custom.from; fixedSize = custom.from.length; } } if (lengthSize > 0 && offset !== null) { if (fixedSize !== void 0) { const cursor = { bytes: new Uint8Array(lengthSize), offset: 0 }; const endianess = item.lengthEndianness; serializeNum(fixedSize, lengthSize, cursor, endianess, false); fixedBytes.push([offset, cursor.bytes]); } offset += lengthSize; } if (fixed !== void 0) { if (offset !== null) fixedBytes.push([offset, fixed]); return [lengthSize + fixed.length, lengthSize + fixed.length]; } const ret = "size" in item && item.size !== void 0 ? [item.size, item.size] : void 0; if (bytesItemHasLayout(item)) { const lm = createLayoutMeta(item.layout, offset, fixedBytes); return ret ?? [lengthSize + lm[0], lengthSize + lm[1]]; } return ret ?? [lengthSize, lengthSizeMax(lengthSize)]; } case "array": { if ("length" in item) { let localFixedBytes = []; const itemSize = createLayoutMeta(item.layout, 0, localFixedBytes); if (offset !== null) { if (itemSize[0] !== itemSize[1]) { if (item.length > 0) for (const [o, s] of localFixedBytes) fixedBytes.push([offset + o, s]); } else { for (let i = 0; i < item.length; ++i) for (const [o, s] of localFixedBytes) fixedBytes.push([offset + o + i * itemSize[0], s]); } } return [item.length * itemSize[0], item.length * itemSize[1]]; } const lengthSize = item.lengthSize | 0; return [lengthSize, lengthSizeMax(lengthSize)]; } case "switch": { const caseFixedBytes = item.layouts.map((_) => []); const { idSize, idEndianness } = item; const caseBounds = item.layouts.map(([idOrConversionId, layout], caseIndex) => { const idVal = Array.isArray(idOrConversionId) ? idOrConversionId[0] : idOrConversionId; if (offset !== null) { const cursor = { bytes: new Uint8Array(idSize), offset: 0 }; serializeNum(idVal, idSize, cursor, idEndianness); caseFixedBytes[caseIndex].push([0, cursor.bytes]); } const ret = createLayoutMeta( layout, offset !== null ? idSize : null, caseFixedBytes[caseIndex] ); return [ret[0] + idSize, ret[1] + idSize]; }); if (offset !== null && caseFixedBytes.every((fbs) => fbs.length > 0)) (() => { const minLen = Math.min( ...caseFixedBytes.map((fbs) => fbs.at(-1)[0] + fbs.at(-1)[1].length) ); const itIndexes = caseFixedBytes.map((_) => 0); for (let bytePos = 0; bytePos < minLen; ) { let byteVal = null; let caseIndex = 0; while (caseIndex < caseFixedBytes.length) { let curItIndex = itIndexes[caseIndex]; const curFixedBytes = caseFixedBytes[caseIndex]; const [curOffset, curSerialized] = curFixedBytes[curItIndex]; if (curOffset + curSerialized.length <= bytePos) { ++curItIndex; if (curItIndex === curFixedBytes.length) return; itIndexes[caseIndex] = curItIndex; bytePos = curFixedBytes[curItIndex][0]; break; } const curByteVal = curSerialized[bytePos - curOffset]; if (byteVal === null) byteVal = curByteVal; if (curByteVal !== byteVal) { ++bytePos; break; } ++caseIndex; } if (caseIndex === caseFixedBytes.length) { fixedBytes.push([offset + bytePos, new Uint8Array([byteVal])]); ++bytePos; } } })(); return [ Math.min(...caseBounds.map(([lower]) => lower)), Math.max(...caseBounds.map(([_, upper]) => upper)) ]; } } } function createLayoutMeta(layout, offset, fixedBytes) { if (!Array.isArray(layout)) return layoutItemMeta(layout, offset, fixedBytes); let bounds = [0, 0]; for (const item of layout) { const itemSize = layoutItemMeta(item, offset, fixedBytes); bounds[0] += itemSize[0]; bounds[1] += itemSize[1]; if (offset !== null) offset = itemSize[0] === itemSize[1] ? offset + itemSize[0] : null; } return bounds; } function buildAscendingBounds(sortedBounds) { const ascendingBounds = /* @__PURE__ */ new Map(); let sortedCandidates = []; const closeCandidatesBefore = (before) => { while (sortedCandidates.length > 0 && sortedCandidates[0][0] < before) { const end = sortedCandidates[0][0] + 1; const removeIndex = sortedCandidates.findIndex(([upper]) => end <= upper); if (removeIndex === -1) sortedCandidates = []; else sortedCandidates.splice(0, removeIndex); ascendingBounds.set(end, arrayToBitset(sortedCandidates.map(([, j]) => j))); } }; for (const [[lower, upper], i] of sortedBounds) { closeCandidatesBefore(lower); const insertIndex = sortedCandidates.findIndex(([u]) => u > upper); if (insertIndex === -1) sortedCandidates.push([upper, i]); else sortedCandidates.splice(insertIndex, 0, [upper, i]); ascendingBounds.set(lower, arrayToBitset(sortedCandidates.map(([, j]) => j))); } closeCandidatesBefore(Infinity); return ascendingBounds; } function internalBuildDiscriminator(layouts) { if (layouts.length === 0) throw new Error("Cannot discriminate empty set of layouts"); const emptySet = 0n; const allLayouts = (1n << BigInt(layouts.length)) - 1n; const fixedKnown = layouts.map(() => []); const sizeBounds = layouts.map((l, i) => createLayoutMeta(l, 0, fixedKnown[i])); const sortedBounds = sizeBounds.map((b, i) => [b, i]).sort(([[l1]], [[l2]]) => l1 - l2); const mustHaveByteAt = (() => { let remaining = allLayouts; const ret = /* @__PURE__ */ new Map(); for (const [[lower], i] of sortedBounds) { remaining ^= 1n << BigInt(i); ret.set(lower, remaining); } return ret; })(); const ascendingBounds = buildAscendingBounds(sortedBounds); const sizePower = layouts.length - Math.max( ...[...ascendingBounds.values()].map((candidates) => count(candidates)) ); const layoutsWithByteAt = (bytePos) => { let ret = allLayouts; for (const [lower, candidates] of mustHaveByteAt) { if (bytePos < lower) break; ret = candidates; } return ret; }; const layoutsWithSize = (size) => { let ret = emptySet; for (const [lower, candidates] of ascendingBounds) { if (size < lower) break; ret = candidates; } return ret; }; const fixedKnownBytes = Array.from({ length: Math.max(...fixedKnown.map((fkb) => fkb.length > 0 ? fkb.at(-1)[0] + fkb.at(-1)[1].length : 0)) }).map(() => []); for (let i = 0; i < fixedKnown.length; ++i) for (const [offset, serialized] of fixedKnown[i]) for (let j = 0; j < serialized.length; ++j) fixedKnownBytes[offset + j].push([serialized[j], i]); let bestBytes = []; for (const [bytePos, fixedKnownByte] of fixedKnownBytes.entries()) { const lwba = layoutsWithByteAt(bytePos); const anyValueLayouts = lwba ^ arrayToBitset(fixedKnownByte.map(([, layoutIdx]) => layoutIdx)); const outOfBoundsLayouts = allLayouts ^ lwba; const distinctValues = /* @__PURE__ */ new Map(); for (const [byteVal, candidate] of fixedKnownByte) { if (!distinctValues.has(byteVal)) distinctValues.set(byteVal, emptySet); distinctValues.set(byteVal, distinctValues.get(byteVal) | 1n << BigInt(candidate)); } let power = layouts.length - Math.max(count(anyValueLayouts), count(outOfBoundsLayouts)); for (const layoutsWithValue of distinctValues.values()) { const curPower = fixedKnownByte.length - count(layoutsWithValue) + count(outOfBoundsLayouts); power = Math.min(power, curPower); } if (power === 0) continue; if (power === layouts.length - 1) return [ true, (encoded) => bitsetToArray( encoded.length <= bytePos ? outOfBoundsLayouts : distinctValues.get(encoded[bytePos]) ?? emptySet ) ]; bestBytes.push([power, bytePos, outOfBoundsLayouts, distinctValues, anyValueLayouts]); } if (sizePower === layouts.length - 1) return [true, (encoded) => bitsetToArray(layoutsWithSize(encoded.length))]; bestBytes.sort(([lhsPower], [rhsPower]) => rhsPower - lhsPower); let distinguishable = true; const strategies = /* @__PURE__ */ new Map(); const candidatesBySize = /* @__PURE__ */ new Map(); const addStrategy = (candidates, strategy) => { strategies.set(candidates, strategy); if (!candidatesBySize.has(count(candidates))) candidatesBySize.set(count(candidates), []); candidatesBySize.get(count(candidates)).push(candidates); }; const recursivelyBuildStrategy = (candidates, bestBytes2) => { if (count(candidates) <= 1 || strategies.has(candidates)) return; let sizePower2 = 0; const narrowedBounds = /* @__PURE__ */ new Map(); for (const candidate of bitsetToArray(candidates)) { const lower = sizeBounds[candidate][0]; const overlap = ascendingBounds.get(lower) & candidates; narrowedBounds.set(lower, overlap); sizePower2 = Math.max(sizePower2, count(overlap)); } sizePower2 = count(candidates) - sizePower2; const narrowedBestBytes = []; for (const [power, bytePos, outOfBoundsLayouts, distinctValues, anyValueLayouts] of bestBytes2) { const narrowedDistinctValues = /* @__PURE__ */ new Map(); let fixedKnownCount = 0; for (const [byteVal, layoutsWithValue] of distinctValues) { const lwv = layoutsWithValue & candidates; if (count(lwv) > 0) { narrowedDistinctValues.set(byteVal, lwv); fixedKnownCount += count(lwv); } } const narrowedOutOfBoundsLayouts = outOfBoundsLayouts & candidates; let narrowedPower = narrowedDistinctValues.size > 0 ? power : 0; for (const layoutsWithValue of narrowedDistinctValues.values()) { const curPower = fixedKnownCount - count(layoutsWithValue) + count(narrowedOutOfBoundsLayouts); narrowedPower = Math.min(narrowedPower, curPower); } if (narrowedPower === 0) continue; if (narrowedPower === count(candidates) - 1) { addStrategy(candidates, [bytePos, narrowedOutOfBoundsLayouts, narrowedDistinctValues]); return; } narrowedBestBytes.push([ narrowedPower, bytePos, narrowedOutOfBoundsLayouts, narrowedDistinctValues, anyValueLayouts & candidates ]); } if (sizePower2 === count(candidates) - 1) { addStrategy(candidates, "size"); return; } narrowedBestBytes.sort(([lhsPower], [rhsPower]) => rhsPower - lhsPower); if (narrowedBestBytes.length > 0 && narrowedBestBytes[0][0] >= sizePower2) { const [, bytePos, narrowedOutOfBoundsLayouts, narrowedDistinctValues, anyValueLayouts] = narrowedBestBytes[0]; addStrategy(candidates, [bytePos, narrowedOutOfBoundsLayouts, narrowedDistinctValues]); recursivelyBuildStrategy(narrowedOutOfBoundsLayouts, narrowedBestBytes); for (const cand of narrowedDistinctValues.values()) recursivelyBuildStrategy(cand | anyValueLayouts, narrowedBestBytes.slice(1)); return; } if (sizePower2 > 0) { addStrategy(candidates, "size"); for (const cands of narrowedBounds.values()) recursivelyBuildStrategy(cands, narrowedBestBytes); return; } addStrategy(candidates, "indistinguishable"); distinguishable = false; }; recursivelyBuildStrategy(allLayouts, bestBytes); const findSmallestSuperSetStrategy = (candidates) => { for (let size = count(candidates) + 1; size < layouts.length - 2; ++size) for (const larger of candidatesBySize.get(size) ?? []) if ((candidates & larger) == candidates) return strategies.get(larger); throw new Error("Implementation error in layout discrimination algorithm"); }; return [distinguishable, (encoded) => { let candidates = allLayouts; let strategy = strategies.get(candidates); while (strategy !== "indistinguishable") { if (strategy === "size") candidates &= layoutsWithSize(encoded.length); else { const [bytePos, outOfBoundsLayouts, distinctValues] = strategy; if (encoded.length <= bytePos) candidates &= outOfBoundsLayouts; else { const byteVal = encoded[bytePos]; for (const [val, cands] of distinctValues) if (val !== byteVal) candidates ^= candidates & cands; candidates ^= candidates & outOfBoundsLayouts; } } if (count(candidates) <= 1) break; strategy = strategies.get(candidates) ?? findSmallestSuperSetStrategy(candidates); } return bitsetToArray(candidates); }]; } // src/items.ts var customizableBytes = (base, spec) => ({ ...base, binary: "bytes", ...(() => { if (spec === void 0) return {}; if (isLayout(spec)) return { layout: spec }; if (spec instanceof Uint8Array || isFixedBytesConversion(spec) || !Array.isArray(spec)) return { custom: spec }; return { layout: spec[0], custom: spec[1] }; })() }); function boolItem(permissive = false) { return { binary: "uint", size: 1, custom: { to: (encoded) => { if (encoded === 0) return false; if (permissive || encoded === 1) return true; throw new Error(`Invalid bool value: ${encoded}`); }, from: (value) => value ? 1 : 0 } }; } function enumItem(entries, opts) { const valueToName = Object.fromEntries(entries.map(([name, value]) => [value, name])); const nameToValue = Object.fromEntries(entries); return { binary: "uint", size: opts?.size ?? 1, endianness: opts?.endianness ?? "big", custom: { to: (encoded) => { const name = valueToName[encoded]; if (name === void 0) throw new Error(`Invalid enum value: ${encoded}`); return name; }, from: (name) => nameToValue[name] } }; } var baseOptionItem = (someType) => ({ binary: "switch", idSize: 1, idTag: "isSome", layouts: [ [[0, false], []], [[1, true], [customizableBytes({ name: "value" }, someType)]] ] }); function optionItem(optVal) { return { binary: "bytes", layout: baseOptionItem(optVal), custom: { to: (obj) => obj.isSome === true ? obj["value"] : void 0, from: (value) => value === void 0 ? { isSome: false } : { isSome: true, value } //good luck narrowing this type } }; } function bitsetItem(bitnames, size) { return { binary: "uint", size: size ?? Math.ceil(bitnames.length / 8), custom: { to: (encoded) => { const ret = {}; for (let i = 0; i < bitnames.length; ++i) if (bitnames[i]) ret[bitnames[i]] = (BigInt(encoded) & 1n << BigInt(i)) !== 0n; return ret; }, from: (obj) => { let val = 0n; for (let i = 0; i < bitnames.length; ++i) if (bitnames[i] && obj[bitnames[i]]) val |= 1n << BigInt(i); return bitnames.length > numberMaxSize ? val : Number(val); } } }; } // src/setEndianness.ts function setEndianness(layout, endianness) { return isItem(layout) ? setItemEndianness(layout, endianness) : layout.map((item) => setItemEndianness(item, endianness)); } function setItemEndianness(item, endianness) { switch (item.binary) { case "uint": case "int": return item?.size === 1 ? item : { ...item, endianness }; case "bytes": case "array": { const layout = "layout" in item ? { layout: setEndianness(item.layout, endianness) } : {}; const lengthEndianness = "lengthSize" in item && item.lengthSize !== 1 ? { lengthEndianness: endianness } : {}; return { ...item, ...layout, ...lengthEndianness }; } case "switch": { const idEndianness = item.idSize !== 1 ? { idEndianness: endianness } : {}; const layouts = item.layouts.map(([id, layout]) => [id, setEndianness(layout, endianness)]); return { ...item, ...idEndianness, layouts }; } } } export { addFixedValues, bitsetItem, boolItem, buildDiscriminator, calcSize, calcStaticSize, customizableBytes, deserialize, dynamicItemsOf, enumItem, fixedItemsOf, numberMaxSize, optionItem, serialize, setEndianness }; //# sourceMappingURL=index.mjs.map