UNPKG

binary-layout

Version:

Typescript-native, declarative DSL for working with binary data

1 lines 106 kB
{"version":3,"sources":["../src/layout.ts","../src/utils.ts","../src/size.ts","../src/serialize.ts","../src/deserialize.ts","../src/fixedDynamic.ts","../src/discriminate.ts","../src/items.ts","../src/setEndianness.ts"],"sourcesContent":["export type NumType = number | bigint;\nexport type BytesType = Uint8Array;\nexport type PrimitiveType = NumType | BytesType;\n\n//used wherever an object is expected that sprung from the DeriveType type defined below\nexport type LayoutObject = { readonly [key: string]: any };\n\nexport const binaryLiterals = [\"int\", \"uint\", \"bytes\", \"array\", \"switch\"] as const;\nexport type BinaryLiterals = typeof binaryLiterals[number];\nexport type Endianness = \"little\" | \"big\";\nexport const defaultEndianness = \"big\";\n\nexport const numberMaxSize = 6; //Math.log2(Number.MAX_SAFE_INTEGER) / 8 = 6.625;\nexport type NumberSize = 1 | 2 | 3 | 4 | 5 | 6;\n\nexport type NumSizeToPrimitive<Size extends number> =\n Size extends NumberSize\n ? number\n : Size & NumberSize extends never\n ? bigint\n : number | bigint;\n\nexport type FixedConversion<FromType extends PrimitiveType | LayoutObject, ToType> = {\n readonly to: ToType,\n readonly from: FromType,\n};\n\nexport type CustomConversion<FromType extends PrimitiveType | LayoutObject, ToType> = {\n readonly to: (val: FromType) => ToType,\n readonly from: (val: ToType) => FromType,\n};\n\nexport interface ItemBase<BL extends BinaryLiterals> {\n readonly binary: BL,\n};\n\ninterface FixedOmittableCustom<T extends PrimitiveType> {\n custom: T,\n omit?: boolean\n};\n\n//length size: number of bytes used to encode the preceeding length field which in turn\n// holds either the number of bytes (for bytes) or elements (for array)\nexport interface LengthPrefixed {\n readonly lengthSize: NumberSize,\n readonly lengthEndianness?: Endianness, //see defaultEndianness\n // //restricts the datarange of lengthSize to a maximum value to prevent out of memory\n // // attacks/issues\n // readonly maxLength?: number,\n}\n\n//size: number of bytes used to encode the item\ninterface NumItemBase<T extends NumType, Signed extends Boolean>\n extends ItemBase<Signed extends true ? \"int\" : \"uint\"> {\n size: T extends bigint ? number : NumberSize,\n endianness?: Endianness, //see defaultEndianness\n};\n\nexport interface FixedPrimitiveNum<\n T extends NumType,\n Signed extends Boolean\n> extends NumItemBase<T, Signed>, FixedOmittableCustom<T> {};\n\nexport interface OptionalToFromNum<\n T extends NumType,\n Signed extends Boolean\n> extends NumItemBase<T, Signed> {\n custom?: FixedConversion<T, any> | CustomConversion<T, any>\n};\n\nexport interface FixedPrimitiveBytes\n extends ItemBase<\"bytes\">, FixedOmittableCustom<BytesType> {};\nexport interface FlexPureBytes extends ItemBase<\"bytes\"> {\n readonly custom?: BytesType | FixedConversion<BytesType, any> | CustomConversion<BytesType, any>,\n};\n\nexport interface FlexLayoutBytes extends ItemBase<\"bytes\"> {\n readonly custom?: FixedConversion<LayoutObject, any> | CustomConversion<LayoutObject, any>,\n readonly layout: Layout,\n}\n\nexport interface ManualSizePureBytes extends FlexPureBytes {\n readonly size: number,\n};\n\nexport interface LengthPrefixedPureBytes extends FlexPureBytes, LengthPrefixed {};\n\nexport interface ManualSizeLayoutBytes extends FlexLayoutBytes {\n readonly size: number,\n};\n\nexport interface LengthPrefixedLayoutBytes extends FlexLayoutBytes, LengthPrefixed {};\n\ninterface ArrayItemBase extends ItemBase<\"array\"> {\n readonly layout: Layout,\n};\n\nexport interface FixedLengthArray extends ArrayItemBase {\n readonly length: number,\n};\n\nexport interface LengthPrefixedArray extends ArrayItemBase, LengthPrefixed {};\n\n//consumes the rest of the data on deserialization\nexport interface RemainderArray extends ArrayItemBase {};\n\ntype PlainId = number;\ntype ConversionId = readonly [number, unknown];\ntype IdProperLayoutPair<\n Id extends PlainId | ConversionId,\n P extends ProperLayout = ProperLayout\n> = readonly [Id, P];\ntype IdProperLayoutPairs =\n readonly IdProperLayoutPair<PlainId>[] |\n readonly IdProperLayoutPair<ConversionId>[];\ntype DistributiveAtLeast1<T> = T extends any ? readonly [T, ...T[]] : never;\nexport interface SwitchItem extends ItemBase<\"switch\"> {\n readonly idSize: NumberSize,\n readonly idEndianness?: Endianness, //see defaultEndianness\n readonly idTag?: string,\n readonly layouts:\n DistributiveAtLeast1<IdProperLayoutPair<PlainId> | IdProperLayoutPair<ConversionId>>,\n}\n\nexport type NumItem<Signed extends boolean = boolean> =\n //force distribution over union\n Signed extends infer S extends boolean\n ? FixedPrimitiveNum<number, S> |\n OptionalToFromNum<number, S> |\n FixedPrimitiveNum<bigint, S> |\n OptionalToFromNum<bigint, S>\n : never;\n\nexport type UintItem = NumItem<false>;\nexport type IntItem = NumItem<true>;\nexport type BytesItem =\n FixedPrimitiveBytes |\n FlexPureBytes |\n ManualSizePureBytes |\n LengthPrefixedPureBytes |\n FlexLayoutBytes |\n ManualSizeLayoutBytes |\n LengthPrefixedLayoutBytes;\nexport type ArrayItem = FixedLengthArray | LengthPrefixedArray | RemainderArray;\nexport type Item = NumItem | BytesItem | ArrayItem | SwitchItem;\nexport type NamedItem = Item & { readonly name: string };\nexport type ProperLayout = readonly NamedItem[];\nexport type Layout = Item | ProperLayout;\n\ntype NameOrOmitted<T extends { name: string }> = T extends {omit: true} ? never : T[\"name\"];\n\nexport type DeriveType<L extends Layout> =\n Layout extends L\n ? unknown\n : L extends infer LI extends Item\n ? ItemToType<LI>\n : L extends infer P extends ProperLayout\n ? { readonly [I in P[number] as NameOrOmitted<I>]: ItemToType<I> }\n : never;\n\ntype ItemToType<II extends Item> =\n II extends infer I extends Item\n ? I extends NumItem\n ? NumItemToType<I>\n : I extends BytesItem\n ? BytesItemToType<I>\n : I extends ArrayItem\n ? ArrayItemToType<I>\n : I extends SwitchItem\n ? SwitchItemToType<I>\n : never\n : never;\n\n//---NumItem---\ntype NumItemToType<I extends NumItem> =\n //we must infer FromType here to make sure we \"hit\" the correct type of the conversion\n I[\"custom\"] extends CustomConversion<infer From extends NumType, infer To>\n ? To\n : I[\"custom\"] extends FixedConversion<infer From extends NumType, infer To>\n ? To\n : I[\"custom\"] extends undefined\n ? NumSizeToPrimitive<I[\"size\"]>\n : I[\"custom\"] extends NumType\n ? I[\"custom\"]\n : NumSizeToPrimitive<I[\"size\"]>;\n\n//---BytesItem---\ntype BytesItemToType<I extends BytesItem> =\n I extends { layout: Layout }\n ? I[\"custom\"] extends CustomConversion<infer From extends LayoutObject, infer To>\n ? To\n : I[\"custom\"] extends FixedConversion<infer From extends LayoutObject, infer To>\n ? To\n : DeriveType<I[\"layout\"]>\n : I[\"custom\"] extends CustomConversion<BytesType, infer To>\n ? To\n : I[\"custom\"] extends FixedConversion<BytesType, infer To>\n ? To\n : BytesType;\n\n//---ArrayItem---\ntype TupleWithLength<T, L extends number, A extends T[] = []> =\n A[\"length\"] extends L\n ? A\n : TupleWithLength<T, L, [...A, T]>;\n\ntype ArrayItemToType<I extends ArrayItem> =\n DeriveType<I[\"layout\"]> extends infer DT\n ? I extends { length: infer AL extends number }\n ? number extends AL\n ? readonly DT[]\n : Readonly<TupleWithLength<DT, AL>>\n : readonly DT[]\n : never;\n\n//---SwitchItem---\ntype MaybeConvert<Id extends PlainId | ConversionId> =\n Id extends readonly [number, infer Converted] ? Converted : Id;\n\ntype IdLayoutPairsToTypeUnion<A extends IdProperLayoutPairs, IdTag extends string> =\n A extends infer V extends IdProperLayoutPairs\n ? V extends readonly [infer Head,...infer Tail extends IdProperLayoutPairs]\n ? Head extends IdProperLayoutPair<infer MaybeConversionId, infer P extends ProperLayout>\n ? MaybeConvert<MaybeConversionId> extends infer Id\n ? DeriveType<P> extends infer DT extends LayoutObject\n ? { readonly [K in IdTag | keyof DT]: K extends keyof DT ? DT[K] : Id }\n | IdLayoutPairsToTypeUnion<Tail, IdTag>\n : never\n : never\n : never\n : never\n : never;\n\ntype SwitchItemToType<I extends SwitchItem> =\n IdLayoutPairsToTypeUnion<\n I[\"layouts\"],\n I[\"idTag\"] extends infer ID extends string\n ? ID extends undefined\n ? \"id\"\n : ID\n : never\n >;\n","import type {\n Layout,\n Item,\n SwitchItem,\n FixedConversion,\n NumType,\n BytesType,\n PrimitiveType,\n FlexLayoutBytes,\n LengthPrefixedLayoutBytes,\n ManualSizeLayoutBytes,\n} from \"./layout\";\nimport { binaryLiterals } from \"./layout\";\n\nexport const isNumType = (x: any): x is NumType =>\n typeof x === \"number\" || typeof x === \"bigint\";\n\nexport const isBytesType = (x: any): x is BytesType => x instanceof Uint8Array;\n\nexport const isPrimitiveType = (x: any): x is PrimitiveType =>\n isNumType(x) || isBytesType(x);\n\nexport const isItem = (x: any): x is Item => binaryLiterals.includes(x?.binary);\n\nexport const isLayout = (x: any): x is Layout =>\n isItem(x) || Array.isArray(x) && x.every(isItem);\n\nconst isFixedNumberConversion = (custom: any): custom is FixedConversion<number, any> =>\n typeof custom?.from === \"number\";\n\nconst isFixedBigintConversion = (custom: any): custom is FixedConversion<bigint, any> =>\n typeof custom?.from === \"bigint\";\n\nexport const isFixedUintConversion = (custom: any): custom is\n FixedConversion<number, any> | FixedConversion<bigint, any> =>\n isFixedNumberConversion(custom) || isFixedBigintConversion(custom);\n\nexport const isFixedBytesConversion = (custom: any): custom is FixedConversion<BytesType, any> =>\n isBytesType(custom?.from);\n\nexport const isFixedPrimitiveConversion = (custom: any): custom is\n FixedConversion<number, any> | FixedConversion<bigint, any> | FixedConversion<BytesType, any> =>\n isFixedUintConversion(custom) || isFixedBytesConversion(custom);\n\nexport const checkSize = (layoutSize: number, dataSize: number): number => {\n if (layoutSize !== dataSize)\n throw new Error(`size mismatch: layout size: ${layoutSize}, data size: ${dataSize}`);\n\n return dataSize;\n}\n\n//In a better world, we wouldn't need this type guard and could just check for \"layout\" in bytesItem\n// directly because no layout item actually allows for its layout property (if it has one) to be\n// `undefined`.\n//\n//The problem arises in how TypeScript checks `satisfies` constraints that involve unions:\n//Consider:\n//```\n//const shouldBeIllegal = {\n// binary: \"bytes\", layout: undefined,\n//} as const satisfies FlexPureBytes | FlexLayoutBytes;\n//```\n//\n//This should be illegal because `FlexPureBytes` does not specify a layout property, so its\n// specification would be excessive and `FlexLayoutBytes` does not allow for its `layout` property\n// to be `undefined`.\n//But when checking a `satisfies` constraint of unions of interfaces, excessive properties are\n// actually ignored and so the `satisfies` constraint will be considered fulfilled, even though it\n// neither member of the union by itself satisfies it - how utterly counterintuitive.\n//\n//Given that it is fairly natural - though strictly speaking incorrect - to write the following:\n//```\n//const someBytesTemplate = <const L extends Layout | undefined = undefined>(layout?: L) => ({\n// binary: \"bytes\", layout: layout as L,\n//} as const satisfies Item);\n//```\n//and because TypeScript fails to alert us that it does in fact not satisfy any bytes item at all,\n// we instead introduce an additional check in our implementation that not only is a layout\n// property present but that it is also not `undefined`.\nexport const bytesItemHasLayout = (bytesItem: { readonly binary: \"bytes\" }):\n bytesItem is FlexLayoutBytes | ManualSizeLayoutBytes | LengthPrefixedLayoutBytes =>\n \"layout\" in bytesItem && bytesItem.layout !== undefined;\n\nexport const checkItemSize = (item: any, dataSize: number): number =>\n (\"size\" in item && item.size !== undefined) ? checkSize(item.size, dataSize) : dataSize;\n\nexport const checkNumEquals = (custom: number | bigint, data: number | bigint): void => {\n if (custom != data)\n throw new Error(`value mismatch: (constant) layout value: ${custom}, data value: ${data}`);\n}\n\nexport const checkBytesTypeEqual = (\n custom: BytesType,\n data: BytesType,\n opts?: {\n customSlice?: number | readonly [number, number];\n dataSlice?: number | readonly [number, number];\n }): void => {\n const toSlice = (bytes: BytesType, slice?: number | readonly [number, number]) =>\n slice === undefined\n ? [0, bytes.length] as const\n : Array.isArray(slice)\n ? slice\n : [slice, bytes.length] as const;\n\n const [customStart, customEnd] = toSlice(custom, opts?.customSlice);\n const [dataStart, dataEnd] = toSlice(data, opts?.dataSlice);\n const length = customEnd - customStart;\n checkSize(length, dataEnd - dataStart);\n\n for (let i = 0; i < custom.length; ++i)\n if (custom[i + customStart] !== data[i + dataStart])\n throw new Error(`binary data mismatch: ` +\n `layout value: ${custom}, offset: ${customStart}, data value: ${data}, offset: ${dataStart}`\n );\n}\n\nexport function findIdLayoutPair(item: SwitchItem, data: any) {\n const id = data[item.idTag ?? \"id\"];\n return (item.layouts as readonly any[]).find(([idOrConversionId]) =>\n (Array.isArray(idOrConversionId) ? idOrConversionId[1] : idOrConversionId) == id\n )!;\n}\n","import type {\n Layout,\n Item,\n DeriveType,\n} from \"./layout\";\nimport {\n findIdLayoutPair,\n isBytesType,\n isItem,\n isFixedBytesConversion,\n checkItemSize,\n bytesItemHasLayout,\n} from \"./utils\";\n\nexport function calcSize<const L extends Layout>(layout: L, data: DeriveType<L>): number {\n const size = internalCalcSize(layout, data);\n if (size === null)\n throw new Error(\n `coding error: couldn't calculate layout size for layout ${layout} with data ${data}`\n );\n\n return size;\n}\n\n//no way to use overloading here:\n// export function calcSize<const L extends Layout>(layout: L): number | null;\n// export function calcSize<const L extends Layout>(layout: L, data: DeriveType<L>): number;\n// export function calcSize<const L extends Layout>(\n// layout: L,\n// data?: DeriveType<L>\n// ): number | null; //impl\n//results in \"instantiation too deep\" error.\n//\n//Trying to pack everything into a single function definition means we either can't narrow the\n// return type correctly:\n// export function calcSize<const L extends Layout>(\n// layout: L,\n// data: DeriveType<L>,\n// ): number | null;\n//or we have to make data overly permissive via:\n// export function calcSize<\n// L extends Layout,\n// const D extends DeriveType<L> | undefined,\n// >(\n// layout: L,\n// data?: D, //data can now contain additional properties\n// ): undefined extends D ? number | null : number;\n//so we're stuck with having to use to separate names\nexport function calcStaticSize(layout: Layout): number | null {\n return internalCalcSize(layout, staticCalc);\n}\n\n// --- implementation ---\n\n//The implementation here shares code for calcSize and calcStaticSize. It's slightly less efficient\n// from a runtime PoV but it avoids what would effectively be code duplication.\n//Since `undefined` is a valid \"to\" type for custom conversions, it can be a valid value for data.\n// Therefore, we can't use it to differentiate between calcSize and calcStaticSize, where in the\n// former we know that data will adhere to the layout, while in the latter data will not exist.\n//So, to mark data as \"does not exist\", i.e. we are in the static calc version, we use a\n// local (and hence unique) symbol instead.\nconst staticCalc = Symbol(\"staticCalc\");\n\n//stores the results of custom.from calls for bytes items to avoid duplicated effort upon\n// subsequent serialization\nexport function calcSizeForSerialization<const L extends Layout>(\n layout: L,\n data: DeriveType<L>\n): [number, any[]] {\n const bytesConversions: any[] = [];\n const size = internalCalcSize(layout, data, bytesConversions);\n if (size === null)\n throw new Error(\n `coding error: couldn't calculate layout size for layout ${layout} with data ${data}`\n );\n\n return [size, bytesConversions];\n}\n\nfunction calcItemSize(item: Item, data: any, bytesConversions?: any[]): number | null {\n const storeInCache = (cachedFrom: any) => {\n if (bytesConversions !== undefined)\n bytesConversions.push(cachedFrom);\n\n return cachedFrom;\n };\n\n switch (item.binary) {\n case \"int\":\n case \"uint\":\n return item.size;\n case \"bytes\": {\n if (\"size\" in item && data === staticCalc)\n return item.size;\n\n //items only have a size or a lengthSize, never both\n const lengthSize = (\"lengthSize\" in item) ? item.lengthSize | 0 : 0;\n\n if (bytesItemHasLayout(item)) {\n const { custom } = item;\n const layoutSize = internalCalcSize(\n item.layout,\n custom === undefined\n ? data\n : typeof custom.from === \"function\"\n ? (data !== staticCalc ? storeInCache(custom.from(data)) : staticCalc)\n : custom.from, //flex layout bytes only allows conversions, not fixed values\n bytesConversions\n );\n if (layoutSize === null)\n return (\"size\" in item ) ? item.size ?? null : null;\n\n return lengthSize + checkItemSize(item, layoutSize);\n }\n\n const { custom } = item;\n if (isBytesType(custom))\n return lengthSize + custom.length; //assumed to equal item.size if it exists\n\n if (isFixedBytesConversion(custom))\n return lengthSize + custom.from.length; //assumed to equal item.size if it exists\n\n if (data === staticCalc)\n return null;\n\n return lengthSize + checkItemSize(\n item,\n custom !== undefined\n ? storeInCache(custom.from(data)).length\n : data.length\n );\n }\n case \"array\": {\n const length = \"length\" in item ? item.length : undefined;\n if (data === staticCalc) {\n if (length !== undefined) {\n const layoutSize = internalCalcSize(item.layout, staticCalc, bytesConversions);\n return layoutSize !== null ? length * layoutSize: null;\n }\n return null;\n }\n\n let size = 0;\n if (length !== undefined && length !== data.length)\n throw new Error(\n `array length mismatch: layout length: ${length}, data length: ${data.length}`\n );\n else if (\"lengthSize\" in item && item.lengthSize !== undefined)\n size += item.lengthSize;\n\n for (let i = 0; i < data.length; ++i) {\n const entrySize = internalCalcSize(item.layout, data[i], bytesConversions);\n if (entrySize === null)\n return null;\n\n size += entrySize;\n }\n\n return size;\n }\n case \"switch\": {\n if (data !== staticCalc) {\n const [_, layout] = findIdLayoutPair(item, data);\n const layoutSize = internalCalcSize(layout, data, bytesConversions);\n return layoutSize !== null ? item.idSize + layoutSize : null;\n }\n\n let size: number | null = null;\n for (const [_, layout] of item.layouts) {\n const layoutSize = internalCalcSize(layout, staticCalc, bytesConversions);\n if (size === null)\n size = layoutSize;\n else if (layoutSize !== size)\n return null;\n }\n return item.idSize + size!;\n }\n }\n}\n\nfunction internalCalcSize(layout: Layout, data: any, bytesConversions?: any[]): number | null {\n if (isItem(layout))\n return calcItemSize(layout as Item, data, bytesConversions);\n\n let size = 0;\n for (const item of layout) {\n let itemData;\n if (data === staticCalc)\n itemData = staticCalc;\n else if (!(\"omit\" in item) || !item.omit) {\n if (!(item.name in data))\n throw new Error(`missing data for layout item: ${item.name}`);\n\n itemData = data[item.name];\n }\n\n const itemSize = calcItemSize(item, itemData, bytesConversions);\n if (itemSize === null) {\n if (data !== staticCalc)\n throw new Error(`coding error: couldn't calculate size for layout item: ${item.name}`);\n\n return null;\n }\n size += itemSize;\n }\n return size;\n}\n","import type {\n Endianness,\n Layout,\n Item,\n DeriveType,\n CustomConversion,\n NumType,\n BytesType,\n FixedConversion,\n LayoutObject,\n ItemBase\n} from \"./layout\";\nimport {\n defaultEndianness,\n numberMaxSize,\n} from \"./layout\";\nimport { calcSizeForSerialization } from \"./size\";\nimport {\n checkItemSize,\n checkBytesTypeEqual,\n checkNumEquals,\n findIdLayoutPair,\n isFixedBytesConversion,\n isItem,\n isNumType,\n isBytesType,\n bytesItemHasLayout,\n} from \"./utils\";\n\ntype Cursor = {\n bytes: BytesType;\n offset: number;\n};\n\nconst cursorWrite = (cursor: Cursor, bytes: BytesType) => {\n cursor.bytes.set(bytes, cursor.offset);\n cursor.offset += bytes.length;\n}\n\ntype BytesConversionQueue = {\n bytesConversions: any[],\n position: number,\n}\n\nconst bcqGetNext = (bcq: BytesConversionQueue) => bcq.bytesConversions[bcq.position++];\n\n//returns a BytesType if no encoded data is provided, otherwise returns the number of bytes written\n// to the provided Uint8Array. Callers should use Uint8Array.subarray if they want to serialize\n// in place of a larger, pre-allocated Uint8Array.\nexport function serialize<\n const L extends Layout,\n E extends BytesType | undefined = undefined\n>(layout: L, data: DeriveType<L>, encoded?: E) {\n const [size, bytesConversions] = calcSizeForSerialization(layout, data);\n const cursor = { bytes: encoded ?? new Uint8Array(size), offset: 0 };\n internalSerialize(layout, data, cursor, {bytesConversions, position: 0});\n if (!encoded && cursor.offset !== cursor.bytes.length)\n throw new Error(\n `encoded data is shorter than expected: ${cursor.bytes.length} > ${cursor.offset}`\n );\n\n return (encoded ? cursor.offset : cursor.bytes) as E extends undefined ? BytesType : number;\n}\n\n//see numberMaxSize comment in layout.ts\nconst maxAllowedNumberVal = 2 ** (numberMaxSize * 8);\n\nexport function serializeNum(\n val: NumType,\n size: number,\n cursor: Cursor,\n endianness: Endianness = defaultEndianness,\n signed: boolean = false,\n) {\n if (!signed && val < 0)\n throw new Error(`Value ${val} is negative but unsigned`);\n\n if (typeof val === \"number\") {\n if (!Number.isInteger(val))\n throw new Error(`Value ${val} is not an integer`);\n\n if (size > numberMaxSize) {\n if (val >= maxAllowedNumberVal)\n throw new Error(`Value ${val} is too large to be safely converted into an integer`);\n\n if (signed && val < -maxAllowedNumberVal)\n throw new Error(`Value ${val} is too small to be safely converted into an integer`);\n }\n }\n\n const bound = 2n ** BigInt(size * 8 - (signed ? 1 : 0));\n if (val >= bound)\n throw new Error(`Value ${val} is too large for ${size} bytes`);\n\n if (signed && val < -bound)\n throw new Error(`Value ${val} is too small for ${size} bytes`);\n\n //correctly handles both signed and unsigned values\n for (let i = 0; i < size; ++i)\n cursor.bytes[cursor.offset + i] =\n Number((BigInt(val) >> BigInt(8 * (endianness === \"big\" ? size - i - 1 : i)) & 0xffn));\n\n cursor.offset += size;\n}\n\nfunction internalSerialize(layout: Layout, data: any, cursor: Cursor, bcq: BytesConversionQueue) {\n if (isItem(layout))\n serializeItem(layout as Item, data, cursor, bcq);\n else\n for (const item of layout)\n try {\n serializeItem(item, data[item.name], cursor, bcq);\n }\n catch (e: any) {\n e.message = `when serializing item '${item.name}': ${e.message}`;\n throw e;\n }\n}\n\nfunction serializeItem(item: Item, data: any, cursor: Cursor, bcq: BytesConversionQueue) {\n switch (item.binary) {\n case \"int\":\n case \"uint\": {\n const value = (() => {\n if (isNumType(item.custom)) {\n if (!(\"omit\" in item && item.omit))\n checkNumEquals(item.custom, data);\n return item.custom;\n }\n\n if (isNumType(item?.custom?.from))\n //no proper way to deeply check equality of item.custom.to and data in JS\n return item!.custom!.from;\n\n type narrowedCustom = CustomConversion<number, any> | CustomConversion<bigint, any>;\n return item.custom !== undefined ? (item.custom as narrowedCustom).from(data) : data;\n })();\n\n serializeNum(value, item.size, cursor, item.endianness, item.binary === \"int\");\n break;\n }\n case \"bytes\": {\n const offset = cursor.offset;\n if (\"lengthSize\" in item && item.lengthSize !== undefined)\n cursor.offset += item.lengthSize;\n\n if (bytesItemHasLayout(item)) {\n const { custom } = item;\n let layoutData;\n if (custom === undefined)\n layoutData = data;\n else if (typeof custom.from !== \"function\")\n layoutData = custom.from;\n else\n layoutData = bcqGetNext(bcq);\n\n internalSerialize(item.layout, layoutData, cursor, bcq);\n }\n else {\n const { custom } = item;\n if (isBytesType(custom)) {\n if (!(\"omit\" in item && item.omit))\n checkBytesTypeEqual(custom, data);\n\n cursorWrite(cursor, custom);\n }\n else if (isFixedBytesConversion(custom))\n //no proper way to deeply check equality of custom.to and data\n cursorWrite(cursor, custom.from);\n else\n cursorWrite(cursor, custom !== undefined ? bcqGetNext(bcq) : data);\n }\n\n if (\"lengthSize\" in item && item.lengthSize !== undefined) {\n const itemSize = cursor.offset - offset - item.lengthSize;\n const curOffset = cursor.offset;\n cursor.offset = offset;\n serializeNum(itemSize, item.lengthSize, cursor, item.lengthEndianness);\n cursor.offset = curOffset;\n }\n else\n checkItemSize(item, cursor.offset - offset);\n\n break;\n }\n case \"array\": {\n if (\"length\" in item && item.length !== data.length)\n throw new Error(\n `array length mismatch: layout length: ${item.length}, data length: ${data.length}`\n );\n\n if (\"lengthSize\" in item && item.lengthSize !== undefined)\n serializeNum(data.length, item.lengthSize, cursor, item.lengthEndianness);\n\n for (let i = 0; i < data.length; ++i)\n internalSerialize(item.layout, data[i], cursor, bcq);\n\n break;\n }\n case \"switch\": {\n const [idOrConversionId, layout] = findIdLayoutPair(item, data);\n const idNum = (Array.isArray(idOrConversionId) ? idOrConversionId[0] : idOrConversionId);\n serializeNum(idNum, item.idSize, cursor, item.idEndianness);\n internalSerialize(layout, data, cursor, bcq);\n break;\n }\n }\n};\n\n//slightly hacky, but the only way to ensure that we are actually deserializing the\n// right data without having to re-serialize the layout every time\nexport function getCachedSerializedFrom(\n item: ItemBase<\"bytes\"> & {layout: Layout; custom: FixedConversion<LayoutObject, any>}\n) {\n const custom =\n item.custom as FixedConversion<LayoutObject, any> & {cachedSerializedFrom?: BytesType};\n if (!(\"cachedSerializedFrom\" in custom)) {\n custom.cachedSerializedFrom = serialize(item.layout, custom.from);\n if (\"size\" in item &&\n item.size !== undefined &&\n item.size !== custom.cachedSerializedFrom.length\n )\n throw new Error(\n `Layout specification error: custom.from does not serialize to specified size`\n );\n }\n return custom.cachedSerializedFrom!;\n}\n","import type {\n Endianness,\n Layout,\n Item,\n DeriveType,\n CustomConversion,\n NumSizeToPrimitive,\n NumType,\n BytesType,\n} from \"./layout\";\nimport { defaultEndianness, numberMaxSize } from \"./layout\";\n\nimport {\n isNumType,\n isBytesType,\n isFixedBytesConversion,\n checkBytesTypeEqual,\n checkNumEquals,\n bytesItemHasLayout,\n} from \"./utils\";\nimport { getCachedSerializedFrom } from \"./serialize\";\n\ntype DeserializeReturn<L extends Layout, B extends boolean> =\n B extends true ? DeriveType<L> : readonly [DeriveType<L>, number];\n\nexport function deserialize<const L extends Layout, const B extends boolean = true>(\n layout: L,\n bytes: BytesType,\n consumeAll?: B,\n): DeserializeReturn<L, B> {\n const boolConsumeAll = consumeAll ?? true;\n const encoded = {\n bytes,\n offset: 0,\n end: bytes.length,\n };\n const decoded = internalDeserialize(layout, encoded);\n\n if (boolConsumeAll && encoded.offset !== encoded.end)\n throw new Error(`encoded data is longer than expected: ${encoded.end} > ${encoded.offset}`);\n\n return (boolConsumeAll ? decoded : [decoded, encoded.offset]) as DeserializeReturn<L, B>;\n}\n\n// --- implementation ---\n\ntype BytesChunk = {\n bytes: BytesType,\n offset: number,\n end: number,\n};\n\nfunction updateOffset(encoded: BytesChunk, size: number) {\n const newOffset = encoded.offset + size;\n if (newOffset > encoded.end)\n throw new Error(`chunk is shorter than expected: ${encoded.end} < ${newOffset}`);\n\n encoded.offset = newOffset;\n}\n\nfunction internalDeserialize(layout: Layout, encoded: BytesChunk): any {\n if (!Array.isArray(layout))\n return deserializeItem(layout as Item, encoded);\n\n let decoded = {} as any;\n for (const item of layout)\n try {\n ((item as any).omit ? {} : decoded)[item.name] = deserializeItem(item, encoded);\n }\n catch (e) {\n (e as Error).message = `when deserializing item '${item.name}': ${(e as Error).message}`;\n throw e;\n }\n\n return decoded;\n}\n\nfunction deserializeNum<S extends number>(\n encoded: BytesChunk,\n size: S,\n endianness: Endianness = defaultEndianness,\n signed: boolean = false,\n) {\n let val = 0n;\n for (let i = 0; i < size; ++i)\n val |= BigInt(encoded.bytes[encoded.offset + i]!)\n << BigInt(8 * (endianness === \"big\" ? size - i - 1 : i));\n\n //check sign bit if value is indeed signed and adjust accordingly\n if (signed && (encoded.bytes[encoded.offset + (endianness === \"big\" ? 0 : size - 1)]! & 0x80))\n val -= 1n << BigInt(8 * size);\n\n updateOffset(encoded, size);\n\n return ((size > numberMaxSize) ? val : Number(val)) as NumSizeToPrimitive<S>;\n}\n\nfunction deserializeItem(item: Item, encoded: BytesChunk): any {\n switch (item.binary) {\n case \"int\":\n case \"uint\": {\n const value = deserializeNum(encoded, item.size, item.endianness, item.binary === \"int\");\n\n const { custom } = item;\n if (isNumType(custom)) {\n checkNumEquals(custom, value);\n return custom;\n }\n if (isNumType(custom?.from)) {\n checkNumEquals(custom!.from, value);\n return custom!.to;\n }\n\n //narrowing to CustomConversion<UintType, any> is a bit hacky here, since the true type\n // would be CustomConversion<number, any> | CustomConversion<bigint, any>, but then we'd\n // have to further tease that apart still for no real gain...\n return custom !== undefined ? (custom as CustomConversion<NumType, any>).to(value) : value;\n }\n case \"bytes\": {\n const expectedSize = (\"lengthSize\" in item && item.lengthSize !== undefined)\n ? deserializeNum(encoded, item.lengthSize, item.lengthEndianness)\n : (item as {size?: number})?.size;\n\n if (bytesItemHasLayout(item)) { //handle layout conversions\n const { custom } = item;\n const offset = encoded.offset;\n let layoutData;\n if (expectedSize === undefined)\n layoutData = internalDeserialize(item.layout, encoded);\n else {\n const subChunk = {...encoded, end: encoded.offset + expectedSize};\n updateOffset(encoded, expectedSize);\n layoutData = internalDeserialize(item.layout, subChunk);\n if (subChunk.offset !== subChunk.end)\n throw new Error(\n `read less data than expected: ${subChunk.offset - encoded.offset} < ${expectedSize}`\n );\n }\n\n if (custom !== undefined) {\n if (typeof custom.from !== \"function\") {\n checkBytesTypeEqual(\n getCachedSerializedFrom(item as any),\n encoded.bytes,\n {dataSlice: [offset, encoded.offset]}\n );\n return custom.to;\n }\n return custom.to(layoutData);\n }\n\n return layoutData;\n }\n\n const { custom } = item;\n { //handle fixed conversions\n let fixedFrom;\n let fixedTo;\n if (isBytesType(custom))\n fixedFrom = custom;\n else if (isFixedBytesConversion(custom)) {\n fixedFrom = custom.from;\n fixedTo = custom.to;\n }\n if (fixedFrom !== undefined) {\n const size = expectedSize ?? fixedFrom.length;\n const value = encoded.bytes.subarray(encoded.offset, encoded.offset + size);\n checkBytesTypeEqual(fixedFrom, value);\n updateOffset(encoded, size);\n return fixedTo ?? fixedFrom;\n }\n }\n\n //handle no or custom conversions\n const start = encoded.offset;\n const end = (expectedSize !== undefined) ? encoded.offset + expectedSize : encoded.end;\n updateOffset(encoded, end - start);\n\n const value = encoded.bytes.subarray(start, end);\n return custom !== undefined ? (custom as CustomConversion<BytesType, any>).to(value) : value;\n }\n case \"array\": {\n let ret = [] as any[];\n const { layout } = item;\n const deserializeArrayItem = () => {\n const deserializedItem = internalDeserialize(layout, encoded);\n ret.push(deserializedItem);\n }\n\n let length: number | null = null;\n if (\"length\" in item && item.length !== undefined)\n length = item.length;\n else if (\"lengthSize\" in item && item.lengthSize !== undefined)\n length = deserializeNum(encoded, item.lengthSize, item.lengthEndianness);\n\n if (length !== null)\n for (let i = 0; i < length; ++i)\n deserializeArrayItem();\n else\n while (encoded.offset < encoded.end)\n deserializeArrayItem();\n\n return ret;\n }\n case \"switch\": {\n const id = deserializeNum(encoded, item.idSize, item.idEndianness);\n const {layouts} = item;\n if (layouts.length === 0)\n throw new Error(`switch item has no layouts`);\n\n const hasPlainIds = typeof layouts[0]![0] === \"number\";\n const pair = (layouts as readonly any[]).find(([idOrConversionId]) =>\n hasPlainIds ? idOrConversionId === id : (idOrConversionId)[0] === id);\n\n if (pair === undefined)\n throw new Error(`unknown id value: ${id}`);\n\n const [idOrConversionId, idLayout] = pair;\n const decoded = internalDeserialize(idLayout, encoded);\n return {\n [item.idTag ?? \"id\"]: hasPlainIds ? id : (idOrConversionId as any)[1],\n ...decoded\n };\n }\n }\n}\n","import type {\n Layout,\n ProperLayout,\n Item,\n NumItem,\n BytesItem,\n ArrayItem,\n SwitchItem,\n DeriveType,\n NumType,\n BytesType,\n LayoutObject,\n FixedConversion,\n CustomConversion,\n} from \"./layout\";\n\nimport { isPrimitiveType, isItem, isFixedPrimitiveConversion, bytesItemHasLayout } from \"./utils\";\n\nexport type FixedItemsOf<L extends Layout> = StartFilterItemsOf<L, true>;\nexport type DynamicItemsOf<L extends Layout> = StartFilterItemsOf<L, false>;\n\nexport const fixedItemsOf = <const L extends Layout>(layout: L) =>\n filterItemsOf(layout, true);\n\nexport const dynamicItemsOf = <const L extends Layout>(layout: L) =>\n filterItemsOf(layout, false);\n\nexport function addFixedValues<const L extends Layout>(\n layout: L,\n dynamicValues: DeriveType<DynamicItemsOf<L>>,\n): DeriveType<L> {\n return internalAddFixedValues(layout, dynamicValues) as DeriveType<L>;\n}\n\n// --- implementation ---\n\ntype NonEmpty = readonly [unknown, ...unknown[]];\n\ntype IPLPair = readonly [any, ProperLayout];\n\ntype FilterItemsOfIPLPairs<ILA extends readonly IPLPair[], Fixed extends boolean> =\n ILA extends infer V extends readonly IPLPair[]\n ? V extends readonly [infer H extends IPLPair, ...infer T extends readonly IPLPair[]]\n ? FilterItemsOf<H[1], Fixed> extends infer P extends ProperLayout | void\n ? P extends NonEmpty\n ? [[H[0], P], ...FilterItemsOfIPLPairs<T, Fixed>]\n : FilterItemsOfIPLPairs<T, Fixed>\n : never\n : []\n : never;\n\ntype FilterLayoutOfItem<I extends { layout: Layout }, Fixed extends boolean> =\n FilterItemsOf<I[\"layout\"], Fixed> extends infer L extends Item | NonEmpty\n ? { readonly [K in keyof I]: K extends \"layout\" ? L : I[K] }\n : void;\n\ntype FilterItem<II extends Item, Fixed extends boolean> =\n II extends infer I extends Item\n ? I extends NumItem\n ? I[\"custom\"] extends NumType | FixedConversion<infer From extends NumType, infer To>\n ? Fixed extends true ? I : void\n : Fixed extends true ? void : I\n : I extends ArrayItem\n ? FilterLayoutOfItem<I, Fixed>\n : I extends BytesItem & { layout: Layout }\n ? I[\"custom\"] extends { custom: FixedConversion<infer From extends LayoutObject, infer To>}\n ? Fixed extends true ? I : void\n : I extends { custom: CustomConversion<infer From extends LayoutObject, infer To>}\n ? Fixed extends true ? void : I\n : FilterLayoutOfItem<I, Fixed>\n : I extends BytesItem\n ? I[\"custom\"] extends BytesType | FixedConversion<infer From extends BytesType, infer To>\n ? Fixed extends true ? I : void\n : Fixed extends true ? void : I\n : I extends SwitchItem\n ? { readonly [K in keyof I]:\n K extends \"layouts\" ? FilterItemsOfIPLPairs<I[\"layouts\"], Fixed> : I[K]\n }\n : never\n : never;\n\ntype FilterItemsOf<L extends Layout, Fixed extends boolean> =\n L extends infer LI extends Item\n ? FilterItem<LI, Fixed>\n : L extends infer P extends ProperLayout\n ? P extends readonly [infer H extends Item, ...infer T extends ProperLayout]\n ? FilterItem<H, Fixed> extends infer NI\n ? NI extends Item\n // @ts-ignore TODO: figure out and fix this\n ? [NI, ...FilterItemsOf<T, Fixed>]\n : FilterItemsOf<T, Fixed>\n : never\n : []\n : never;\n\ntype StartFilterItemsOf<L extends Layout, Fixed extends boolean> =\n FilterItemsOf<L, Fixed> extends infer V extends Layout\n ? V\n : never;\n\nfunction filterItem(item: Item, fixed: boolean): Item | null {\n switch (item.binary) {\n // @ts-ignore - fallthrough is intentional\n case \"bytes\": {\n if (bytesItemHasLayout(item)) {\n const { custom } = item;\n if (custom === undefined) {\n const { layout } = item;\n if (isItem(layout))\n return filterItem(layout, fixed);\n\n const filteredItems = internalFilterItemsOfProperLayout(layout, fixed);\n return (filteredItems.length > 0) ? { ...item, layout: filteredItems } : null;\n }\n const isFixedItem = typeof custom.from !== \"function\";\n return (fixed && isFixedItem || !fixed && !isFixedItem) ? item : null;\n }\n }\n case \"int\":\n case \"uint\": {\n const { custom } = item;\n const isFixedItem = isPrimitiveType(custom) || isFixedPrimitiveConversion(custom);\n return (fixed && isFixedItem || !fixed && !isFixedItem) ? item : null;\n }\n case \"array\": {\n const filtered = internalFilterItemsOf(item.layout, fixed);\n return (filtered !== null) ? { ...item, layout: filtered } : null;\n }\n case \"switch\": {\n const filteredIdLayoutPairs = (item.layouts as readonly any[]).reduce(\n (acc: any, [idOrConversionId, idLayout]: any) => {\n const filteredItems = internalFilterItemsOfProperLayout(idLayout, fixed);\n return filteredItems.length > 0\n ? [...acc, [idOrConversionId, filteredItems]]\n : acc;\n },\n [] as any[]\n );\n return { ...item, layouts: filteredIdLayoutPairs };\n }\n }\n}\n\nfunction internalFilterItemsOfProperLayout(proper: ProperLayout, fixed: boolean): ProperLayout {\n return proper.reduce(\n (acc, item) => {\n const filtered = filterItem(item, fixed) as ProperLayout[number] | null;\n return filtered !== null ? [...acc, filtered] : acc;\n },\n [] as ProperLayout\n );\n}\n\nfunction internalFilterItemsOf(layout: Layout, fixed: boolean): any {\n return (Array.isArray(layout)\n ? internalFilterItemsOfProperLayout(layout, fixed)\n : filterItem(layout as Item, fixed)\n );\n}\n\nfunction filterItemsOf<L extends Layout, const Fixed extends boolean>(\n layout: L,\n fixed: Fixed\n): FilterItemsOf<L, Fixed> {\n return internalFilterItemsOf(layout, fixed);\n}\n\nfunction internalAddFixedValuesItem(item: Item, dynamicValue: any): any {\n switch (item.binary) {\n // @ts-ignore - fallthrough is intentional\n case \"bytes\": {\n if (bytesItemHasLayout(item)) {\n const { custom } = item;\n if (custom === undefined || typeof custom.from !== \"function\")\n return internalAddFixedValues(item.layout, custom ? custom.from : dynamicValue);\n\n return dynamicValue;\n }\n }\n case \"int\":\n case \"uint\": {\n const { custom } = item;\n return (item as {omit?: boolean})?.omit\n ? undefined\n : isPrimitiveType(custom)\n ? custom\n : isFixedPrimitiveConversion(custom)\n ? custom.to\n : dynamicValue;\n }\n case \"array\":\n return Array.isArray(dynamicValue)\n ? dynamicValue.map(element => internalAddFixedValues(item.layout, element))\n : undefined;\n case \"switch\": {\n const id = dynamicValue[item.idTag ?? \"id\"];\n const [_, idLayout] = (item.layouts as readonly IPLPair[]).find(([idOrConversionId]) =>\n (Array.isArray(idOrConversionId) ? idOrConversionId[1] : idOrConversionId) == id\n )!;\n return {\n [item.idTag ?? \"id\"]: id,\n ...internalAddFixedValues(idLayout, dynamicValue)\n };\n }\n }\n}\n\nfunction internalAddFixedValues(layout: Layout, dynamicValues: any): any {\n dynamicValues = dynamicValues ?? {};\n if (isItem(layout))\n return internalAddFixedValuesItem(layout as Item, dynamicValues);\n\n const ret = {} as any;\n for (const item of layout) {\n const fixedVals = internalAddFixedValuesItem(\n item,\n dynamicValues[item.name as keyof typeof dynamicValues] ?? {}\n );\n if (fixedVals !== undefined)\n ret[item.name] = fixedVals;\n }\n return ret;\n}\n","import type { Layout, Item, LengthPrefixed, BytesType } from \"./layout\";\nimport { serializeNum, getCachedSerializedFrom } from \"./serialize\";\nimport { isNumType, isBytesType, isFixedBytesConversion, bytesItemHasLayout } from \"./utils\";\nimport { calcStaticSize } from \"./size\";\n\ntype LayoutIndex = number;\n\nexport type Discriminator<B extends boolean = false> =\n (encoded: BytesType) => B extends false ? LayoutIndex | null : readonly LayoutIndex[];\n\nexport function buildDiscriminator<B extends boolean = false>(\n layouts: readonly Layout[],\n allowAmbiguous?: B\n): Discriminator<B> {\n const [distinguishable, discriminator] = internalBuildDiscriminator(layouts);\n if (!distinguishable && !allowAmbiguous)\n throw new Error(\"Cannot uniquely distinguished the given layouts\");\n\n return (\n !allowAmbiguous\n ? (encoded: BytesType) => {\n const layout = discriminator(encoded);\n return layout.length === 0 ? null : layout[0];\n }\n : discriminator\n ) as Discriminator<B>;\n}\n\n// --- implementation ---\n\ntype Uint = number;\ntype Bitset = bigint;\ntype Size = Uint;\ntype BytePos = Uint;\ntype ByteVal = Uint; //actually a uint8\ntype Candidates = Bitset;\ntype FixedBytes = (readonly [BytePos, BytesType])[];\n//using a Bounds type (even though currently the upper bound can only either be equal to the lower\n// bound or Infinity) in anticipation of a future switch layout item that might contain multiple\n// sublayouts which, unlike arrays currently, could all be bounded but potentially with\n// different sizes\ntype Bounds = [Size, Size];\n\nfunction arrayToBitset(arr: readonly number[]): Bitset {\n return arr.reduce((bit, i) => bit | BigInt(1) << BigInt(i), BigInt(0));\n}\n\nfunction bitsetToArray(bitset: Bitset): number[] {\n const ret: number[] = [];\n for (let i = 0n; bitset > 0n; bitset >>= 1n, ++i)\n if (bitset & 1n)\n ret.push(Number(i));\n\n return ret;\n}\n\nfunction count(candidates: Candidates) {\n let count = 0;\n for (; candidates > 0n; candidates >>= 1n)\n count += Number(candidates & 1n);\n return count;\n}\n\nconst lengthSizeMax = (lengthSize: number) =>\n lengthSize > 0 ? 2**(8 * lengthSize) - 1 : Infinity;\n\nfunction layoutItemMeta(\n item: Item,\n offset: BytePos | null,\n fixedBytes: FixedBytes,\n): Bounds {\n switch (item.binary) {\n case \"int\":\n case \"uint\": {\n const fixedVal =\n isNumType(item.custom)\n ? item.custom\n : isNumType(item?.custom?.from)\n ? item!.custom!.from\n : null;\n\n if (fixedVal !== null && offset !== null) {\n const cursor = {bytes: new Uint8Array(item.size), offset: 0};\n serializeNum(fixedVal, item.size, cursor, item.endianness, item.binary === \"int\");\n fixedBytes.push([offset, cursor.bytes]);\n }\n\n return [item.size, item.size];\n }\n case \"bytes\": {\n const lengthSize = (\"lengthSize\" in item) ? item.lengthSize | 0 : 0;\n\n let fixed;\n let fixedSize;\n if (bytesItemHasLayout(item)) {\n const { custom } = item;\n if (custom !== undefined && typeof custom.from !== \"function\") {\n fixed = getCachedSerializedFrom(item as any);\n fixedSize = fixed.length;\n }\n else {\n const layoutSize = calcStaticSize(item.layout);\n if (layoutSize !== null)\n fixedSize = layoutSize;\n }\n }\n else {\n const { custom } = item;\n if (isBytesType(custom)) {\n fixed = custom;\n fixedSize = custom.length;\n }\n else if (isFixedBytesConversion(custom)) {\n fixed = custom.from;\n fixedSize = custom.from.length;\n }\n }\n\n if (lengthSize > 0 && offset !== null) {\n if (fixedSize !== undefined) {\n const cursor = {bytes: new Uint8Array(lengthSize), offset: 0};\n const endianess = (item as LengthPrefixed).lengthEndianness;\n serializeNum(fixedSize, lengthSize, cursor, endianess, false);\n fixedBytes.push([offset, cursor.bytes]);\n }\n offset += lengthSize;\n }\n\n if (fixed !== undefined) {\n if (offset !== null)\n fixedBytes.push([offset, fixed]);\n\n return [lengthSize + fixed.length, lengthSize + fixed.length];\n }\n\n //lengthSize must be 0 if size is defined\n const ret = (\"size\" in item && item.size !== undefined)\n ? [item.size, item.size] as Bounds\n : undefined;\n\n if (bytesItemHasLayout(item)) {\n const lm = createLayoutMeta(item.layout, offset, fixedBytes)\n return ret ?? [lengthSize + lm[0], lengthSize + lm[1]];\n }\n\n return ret ?? [lengthSize, lengthSizeMax(lengthSize)];\n }\n case \"array\": {\n if (\"length\" in item) {\n let localFixedBytes = [] as FixedBytes;\n const itemSize = createLayoutMeta(item.layout, 0, localFixedBytes);\n if (offset !== null) {\n if (itemSize[0] !== itemSize[1]) {\n //if the size of an array item is not fixed we can only add the fixed bytes of the\n // first item\n if (item.length > 0)\n for (const [o, s] of localFixedBytes)\n fixedBytes.push([offset + o, s]);\n }\n else {\n //otherwise we can add fixed know bytes for each array item\n for (let i = 0; i < item.length; ++i)\n for (const [o, s] of localFixedBytes)\n fixedBytes.push([offset + o + i * itemSize[0], s]);\n }\n }\n\n return [item.length * itemSize[0], item.length * itemSize[1]];\n }\n const lengthSize = (item as LengthPrefixed).lengthSize | 0;\n return [lengthSize, lengthSizeMax(lengthSize)];\n }\n case \"switch\": {\n const caseFixedBytes = item.layouts.map(_ => []) as FixedBytes[];\n const {idSize, idEndianness} = item;\n const caseBounds = item.layouts.map(([idOrConversionId, layout], caseIndex) => {\n const idVal = Array.isArray(idOrConversionId) ? idOrConversionId[0] : idOrConversionId;\n if (offset !== null) {\n const cursor = {bytes: new Uint8Array(idSize), offset: 0};\n serializeNum(idVal, idSize, cursor, idEndianness);\n caseFixedBytes[caseIndex]!.push([0, cursor.bytes]);\n }\n const ret = createLayoutMeta(\n layout,\n offset !== null ? idSize : null,\n caseFixedBytes[caseIn