binary-layout
Version:
Typescript-native, declarative DSL for working with binary data
1,081 lines (1,073 loc) • 40.5 kB
JavaScript
// 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