UNPKG

@ckb-lumos/codec

Version:

Make your own molecule binding in JavaScript(TypeScript)

447 lines (414 loc) 14.2 kB
/** * | Type | Header | Body | * |--------+--------------------------------------------------+-----------------------------------| * | array | | item-0 | item-1 | ... | item-N | * | struct | | field-0 | field-1 | ... | field-N | * | fixvec | items-count | item-0 | item-1 | ... | item-N | * | dynvec | full-size | offset-0 | offset-1 | ... | offset-N | item-0 | item-1 | ... | item-N | * | table | full-size | offset-0 | offset-1 | ... | offset-N | filed-0 | field-1 | ... | field-N | * | option | | item or none (zero bytes) | * | union | item-type-id | item | */ import { BytesCodec, Fixed, FixedBytesCodec, PackParam, UnpackResult, createBytesCodec, createFixedBytesCodec, isFixedCodec, } from "../base"; import { Uint32LE } from "../number"; import { concat } from "../bytes"; import { CodecBaseParseError } from "../error"; import { createObjectCodec, createArrayCodec, createNullableCodec, } from "../high-order"; type NullableKeys<O extends Record<string, unknown>> = { [K in keyof O]-?: [O[K] & (undefined | null)] extends [never] ? never : K; }[keyof O]; type NonNullableKeys<O extends Record<string, unknown>> = { [K in keyof O]-?: [O[K] & (undefined | null)] extends [never] ? K : never; }[keyof O]; // prettier-ignore type PartialNullable<O extends Record<string, unknown>> = & Partial<Pick<O, NullableKeys<O>>> & Pick<O, NonNullableKeys<O>>; /** * A codec for struct and table of Molecule */ export type ObjectLayoutCodec<T extends Record<string, BytesCodec>> = BytesCodec< PartialNullable<{ [key in keyof T]: UnpackResult<T[key]> }>, PartialNullable<{ [key in keyof T]: PackParam<T[key]> }> >; /** * A codec for option of Molecule */ export interface OptionLayoutCodec<T extends BytesCodec> extends BytesCodec<UnpackResult<T> | undefined> { pack: (packable?: PackParam<T>) => Uint8Array; } /** * A code for array and vector of Molecule */ export type ArrayLayoutCodec<T extends BytesCodec> = BytesCodec< Array<UnpackResult<T>>, Array<PackParam<T>> >; /** * A molecule codec for ` */ export type UnionLayoutCodec<T extends Record<string, BytesCodec>> = BytesCodec< { [key in keyof T]: { type: key; value: UnpackResult<T[key]> } }[keyof T], { [key in keyof T]: { type: key; value: PackParam<T[key]> } }[keyof T] >; /** * The array is a fixed-size type: it has a fixed-size inner type and a fixed length. * The size of an array is the size of inner type times the length. * @param itemCodec the fixed-size array item codec * @param itemCount */ export function array<T extends FixedBytesCodec>( itemCodec: T, itemCount: number ): ArrayLayoutCodec<T> & Fixed { const enhancedArrayCodec = createArrayCodec(itemCodec); return createFixedBytesCodec({ byteLength: itemCodec.byteLength * itemCount, pack(items) { const itemsBuf = enhancedArrayCodec.pack(items); return concat(...itemsBuf); }, unpack(buf) { const result: UnpackResult<T>[] = []; const itemLength = itemCodec.byteLength; for (let offset = 0; offset < buf.byteLength; offset += itemLength) { result.push(itemCodec.unpack(buf.slice(offset, offset + itemLength))); } return result; }, }); } function diff(x1: unknown[], x2: unknown[]) { return x1.filter((x) => !x2.includes(x)); } function checkShape<T extends object>(shape: T, fields: (keyof T)[]) { const shapeKeys = Object.keys(shape) as (keyof T)[]; const missingFields = diff(shapeKeys, fields); const missingShape = diff(fields, shapeKeys); if (missingFields.length > 0 || missingShape.length > 0) { throw new Error( `Invalid shape: missing fields ${missingFields.join( ", " )} or shape ${missingShape.join(", ")}` ); } } /** * Struct is a fixed-size type: all fields in struct are fixed-size and it has a fixed quantity of fields. * The size of a struct is the sum of all fields' size. * @param shape a object contains all fields' codec * @param fields the shape's keys. It provide an order for serialization/deserialization. */ export function struct<T extends Record<string, FixedBytesCodec>>( shape: T, fields: (keyof T)[] ): ObjectLayoutCodec<T> & Fixed { checkShape(shape, fields); const objectCodec = createObjectCodec(shape); return createFixedBytesCodec({ byteLength: fields.reduce((sum, field) => sum + shape[field].byteLength, 0), pack(obj) { const packed = objectCodec.pack( obj as { [K in keyof T]: PackParam<T[K]> } ); return fields.reduce((result, field) => { return concat(result, packed[field]); }, Uint8Array.from([])); }, unpack(buf) { const result = {} as PartialNullable<{ [key in keyof T]: UnpackResult<T[key]>; }>; let offset = 0; fields.forEach((field) => { const itemCodec = shape[field]; const itemBuf = buf.slice(offset, offset + itemCodec.byteLength); Object.assign(result, { [field]: itemCodec.unpack(itemBuf) }); offset = offset + itemCodec.byteLength; }); return result; }, }); } /** * Vector with fixed size item codec * @param itemCodec fixed-size vector item codec */ export function fixvec<T extends FixedBytesCodec>( itemCodec: T ): ArrayLayoutCodec<T> { return createBytesCodec({ pack(items) { const arrayCodec = createArrayCodec(itemCodec); return concat( Uint32LE.pack(items.length), arrayCodec .pack(items) .reduce((buf, item) => concat(buf, item), new ArrayBuffer(0)) ); }, unpack(buf) { if (buf.byteLength < 4) { throw new Error( `fixvec: buffer is too short, expected at least 4 bytes, got ${buf.byteLength}` ); } const itemCount = Uint32LE.unpack(buf.slice(0, 4)); return array(itemCodec, itemCount).unpack(buf.slice(4)); }, }); } /** * Vector with dynamic size item codec * @param itemCodec the vector item codec. It can be fixed-size or dynamic-size. * For example, you can create a recursive vector with this. */ export function dynvec<T extends BytesCodec>( itemCodec: T ): ArrayLayoutCodec<T> { return createBytesCodec({ pack(obj) { const arrayCodec = createArrayCodec(itemCodec); const packed = arrayCodec.pack(obj).reduce( (result, item) => { const packedHeader = Uint32LE.pack(result.offset); return { header: concat(result.header, packedHeader), body: concat(result.body, item), offset: result.offset + item.byteLength, }; }, { header: new ArrayBuffer(0), body: new ArrayBuffer(0), offset: 4 + obj.length * 4, } ); const packedTotalSize = Uint32LE.pack( packed.header.byteLength + packed.body.byteLength + 4 ); return concat(packedTotalSize, packed.header, packed.body); }, unpack(buf) { const totalSize = Uint32LE.unpack(buf.slice(0, 4)); if (totalSize !== buf.byteLength) { throw new Error( `Invalid buffer size, read from header: ${totalSize}, actual: ${buf.byteLength}` ); } const result: UnpackResult<T>[] = []; if (totalSize <= 4) { return result; } else { const offset0 = Uint32LE.unpack(buf.slice(4, 8)); const itemCount = (offset0 - 4) / 4; const offsets = new Array(itemCount) .fill(1) .map((_, index) => Uint32LE.unpack(buf.slice(4 + index * 4, 8 + index * 4)) ); offsets.push(totalSize); const result: UnpackResult<T>[] = []; for (let index = 0; index < offsets.length - 1; index++) { const start = offsets[index]; const end = offsets[index + 1]; const itemBuf = buf.slice(start, end); result.push(itemCodec.unpack(itemBuf)); } return result; } }, }); } /** * General vector codec, if `itemCodec` is fixed size type, it will create a fixvec codec, otherwise a dynvec codec will be created. * @param itemCodec */ export function vector<T extends BytesCodec>( itemCodec: T ): ArrayLayoutCodec<T> { if (isFixedCodec(itemCodec)) { return fixvec(itemCodec); } return dynvec(itemCodec); } /** * Table is a dynamic-size type. It can be considered as a dynvec but the length is fixed. * @param shape The table shape, item codec can be dynamic size * @param fields the shape's keys. Also provide an order for pack/unpack. */ export function table<T extends Record<string, BytesCodec>>( shape: T, fields: (keyof T)[] ): ObjectLayoutCodec<T> { checkShape(shape, fields); return createBytesCodec({ pack(obj) { const headerLength = 4 + fields.length * 4; const objectCodec = createObjectCodec(shape); const packedObj = objectCodec.pack( obj as { [K in keyof T]: PackParam<T[K]> } ); const packed = fields.reduce( (result, field) => { const packedItem = packedObj[field]; const packedOffset = Uint32LE.pack(result.offset); return { header: concat(result.header, packedOffset), body: concat(result.body, packedItem), offset: result.offset + packedItem.byteLength, }; }, { header: new ArrayBuffer(0), body: new ArrayBuffer(0), offset: headerLength, } ); const packedTotalSize = Uint32LE.pack( packed.header.byteLength + packed.body.byteLength + 4 ); return concat(packedTotalSize, packed.header, packed.body); }, unpack(buf) { const totalSize = Uint32LE.unpack(buf.slice(0, 4)); if (totalSize !== buf.byteLength) { throw new Error( `Invalid buffer size, read from header: ${totalSize}, actual: ${buf.byteLength}` ); } if (totalSize <= 4 || fields.length === 0) { return {} as PartialNullable<{ [key in keyof T]: UnpackResult<T[key]>; }>; } else { const offsets = fields.map((_, index) => Uint32LE.unpack(buf.slice(4 + index * 4, 8 + index * 4)) ); offsets.push(totalSize); const obj = {}; for (let index = 0; index < offsets.length - 1; index++) { const start = offsets[index]; const end = offsets[index + 1]; const field = fields[index]; const itemCodec = shape[field]; const itemBuf = buf.slice(start, end); Object.assign(obj, { [field]: itemCodec.unpack(itemBuf) }); } return obj as PartialNullable<{ [key in keyof T]: UnpackResult<T[key]>; }>; } }, }); } /** * Union is a dynamic-size type. * Serializing a union has two steps: * - Serialize an item type id in bytes as a 32 bit unsigned integer in little-endian. The item type id is the index of the inner items, and it's starting at 0. * - Serialize the inner item. * @param itemCodec the union item record * @param fields the union item keys, can be an array or an object with custom id * @example * // without custom id * union({ cafe: Uint8, bee: Uint8 }, ['cafe', 'bee']) * // with custom id * union({ cafe: Uint8, bee: Uint8 }, { cafe: 0xcafe, bee: 0xbee }) */ export function union<T extends Record<string, BytesCodec>>( itemCodec: T, fields: (keyof T)[] | Record<keyof T, number> ): UnionLayoutCodec<T> { checkShape(itemCodec, Array.isArray(fields) ? fields : Object.keys(fields)); // check duplicated id if (!Array.isArray(fields)) { const ids = Object.values(fields); if (ids.length !== new Set(ids).size) { throw new Error(`Duplicated id in union: ${ids.join(", ")}`); } } return createBytesCodec({ pack(obj) { const availableFields: (keyof T)[] = Object.keys(itemCodec); const type = obj.type; const typeName = `Union(${availableFields.join(" | ")})`; /* c8 ignore next */ if (typeof type !== "string") { throw new CodecBaseParseError( `Invalid type in union, type must be a string`, typeName ); } const fieldId = Array.isArray(fields) ? fields.indexOf(type) : fields[type]; if (fieldId < 0) { throw new CodecBaseParseError( `Unknown union type: ${String(obj.type)}`, typeName ); } const packedFieldIndex = Uint32LE.pack(fieldId); const packedBody = itemCodec[type].pack(obj.value); return concat(packedFieldIndex, packedBody); }, unpack(buf) { const fieldId = Uint32LE.unpack(buf.slice(0, 4)); const type: keyof T | undefined = (() => { if (Array.isArray(fields)) { return fields[fieldId]; } const entry = Object.entries(fields).find(([, id]) => id === fieldId); return entry?.[0]; })(); if (!type) { throw new Error( `Unknown union field id: ${fieldId}, only ${fields} are allowed` ); } return { type, value: itemCodec[type].unpack(buf.slice(4)) }; }, }); } /** * Option is a dynamic-size type. * Serializing an option depends on whether it is empty or not: * - if it's empty, there is zero bytes (the size is 0). * - if it's not empty, just serialize the inner item (the size is same as the inner item's size). * @param itemCodec */ export function option<T extends BytesCodec>( itemCodec: T ): OptionLayoutCodec<T> { return createBytesCodec({ pack(obj?) { const nullableCodec = createNullableCodec(itemCodec); if (obj !== undefined && obj !== null) { return nullableCodec.pack(obj); } else { return Uint8Array.from([]); } }, unpack(buf) { if (buf.byteLength === 0) { return undefined; } return itemCodec.unpack(buf); }, }); }