UNPKG

@ckb-ccc/core

Version:

Core of CCC - CKBer's Codebase

499 lines (498 loc) 19.6 kB
"use strict"; /* eslint-disable @typescript-eslint/no-explicit-any */ Object.defineProperty(exports, "__esModule", { value: true }); exports.Codec = void 0; exports.fixedItemVec = fixedItemVec; exports.dynItemVec = dynItemVec; exports.vector = vector; exports.option = option; exports.byteVec = byteVec; exports.table = table; exports.union = union; exports.struct = struct; exports.array = array; exports.uint = uint; exports.uintNumber = uintNumber; const index_js_1 = require("../bytes/index.js"); const index_js_2 = require("../num/index.js"); class Codec { constructor(encode, decode, byteLength) { this.encode = encode; this.decode = decode; this.byteLength = byteLength; } static from({ encode, decode, byteLength, }) { return new Codec((encodable) => { const encoded = encode(encodable); if (byteLength !== undefined && encoded.byteLength !== byteLength) { throw new Error(`Codec.encode: expected byte length ${byteLength}, got ${encoded.byteLength}`); } return encoded; }, (decodable, config) => { const decodableBytes = (0, index_js_1.bytesFrom)(decodable); if (byteLength !== undefined && decodableBytes.byteLength !== byteLength) { throw new Error(`Codec.decode: expected byte length ${byteLength}, got ${decodableBytes.byteLength}`); } return decode(decodable, config); }, byteLength); } map({ inMap, outMap, }) { return new Codec((encodable) => this.encode((inMap ? inMap(encodable) : encodable)), (buffer, config) => (outMap ? outMap(this.decode(buffer, config)) : this.decode(buffer, config)), this.byteLength); } mapIn(map) { return this.map({ inMap: map }); } mapOut(map) { return this.map({ outMap: map }); } } exports.Codec = Codec; function uint32To(numLike) { return (0, index_js_2.numToBytes)(numLike, 4); } function uint32From(bytesLike) { return Number((0, index_js_2.numFromBytes)(bytesLike)); } /** * Vector with fixed size item codec * @param itemCodec fixed-size vector item codec */ function fixedItemVec(itemCodec) { const itemByteLength = itemCodec.byteLength; if (itemByteLength === undefined) { throw new Error("fixedItemVec: itemCodec requires a byte length"); } return Codec.from({ encode(userDefinedItems) { try { const concatted = []; (0, index_js_1.bytesConcatTo)(concatted, uint32To(userDefinedItems.length)); for (const item of userDefinedItems) { (0, index_js_1.bytesConcatTo)(concatted, itemCodec.encode(item)); } return (0, index_js_1.bytesFrom)(concatted); } catch (e) { throw new Error(`fixedItemVec(${e?.toString()})`); } }, decode(buffer, config) { const value = (0, index_js_1.bytesFrom)(buffer); if (value.byteLength < 4) { throw new Error(`fixedItemVec: too short buffer, expected at least 4 bytes, but got ${value.byteLength}`); } const itemCount = uint32From(value.slice(0, 4)); const byteLength = 4 + itemCount * itemByteLength; if (value.byteLength !== byteLength) { throw new Error(`fixedItemVec: invalid buffer size, expected ${byteLength}, but got ${value.byteLength}`); } try { const decodedArray = []; for (let offset = 4; offset < byteLength; offset += itemByteLength) { decodedArray.push(itemCodec.decode(value.slice(offset, offset + itemByteLength), config)); } return decodedArray; } catch (e) { throw new Error(`fixedItemVec(${e?.toString()})`); } }, }); } /** * Vector with dynamic size item codec, you can create a recursive vector with this function * @param itemCodec the vector item codec. It can be fixed-size or dynamic-size. */ function dynItemVec(itemCodec) { return Codec.from({ encode(userDefinedItems) { try { let offset = 4 + userDefinedItems.length * 4; const header = []; const body = []; for (const item of userDefinedItems) { const encoded = itemCodec.encode(item); (0, index_js_1.bytesConcatTo)(header, uint32To(offset)); (0, index_js_1.bytesConcatTo)(body, encoded); offset += encoded.byteLength; } const packedTotalSize = uint32To(header.length + body.length + 4); return (0, index_js_1.bytesConcat)(packedTotalSize, header, body); } catch (e) { throw new Error(`dynItemVec(${e?.toString()})`); } }, decode(buffer, config) { const value = (0, index_js_1.bytesFrom)(buffer); if (value.byteLength < 4) { throw new Error(`dynItemVec: too short buffer, expected at least 4 bytes, but got ${value.byteLength}`); } const byteLength = uint32From(value.slice(0, 4)); if (byteLength !== value.byteLength) { throw new Error(`dynItemVec: invalid buffer size, expected ${byteLength}, but got ${value.byteLength}`); } if (byteLength === 4) { return []; } const offset = uint32From(value.slice(4, 8)); const itemCount = (offset - 4) / 4; const offsets = Array.from(new Array(itemCount), (_, index) => uint32From(value.slice(4 + index * 4, 8 + index * 4))); offsets.push(byteLength); try { const decodedArray = []; for (let index = 0; index < offsets.length - 1; index++) { const start = offsets[index]; const end = offsets[index + 1]; const itemBuffer = value.slice(start, end); decodedArray.push(itemCodec.decode(itemBuffer, config)); } return decodedArray; } catch (e) { throw new Error(`dynItemVec(${e?.toString()})`); } }, }); } /** * General vector codec, if `itemCodec` is fixed size type, it will create a fixvec codec, otherwise a dynvec codec will be created. * @param itemCodec */ function vector(itemCodec) { if (itemCodec.byteLength !== undefined) { return fixedItemVec(itemCodec); } return dynItemVec(itemCodec); } /** * 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 innerCodec */ function option(innerCodec) { return Codec.from({ encode(userDefinedOrNull) { if (userDefinedOrNull == null) { return (0, index_js_1.bytesFrom)([]); } try { return innerCodec.encode(userDefinedOrNull); } catch (e) { throw new Error(`option(${e?.toString()})`); } }, decode(buffer, config) { const value = (0, index_js_1.bytesFrom)(buffer); if (value.byteLength === 0) { return undefined; } try { return innerCodec.decode(buffer, config); } catch (e) { throw new Error(`option(${e?.toString()})`); } }, }); } /** * Wrap the encoded value with a fixed-length buffer * @param codec */ function byteVec(codec) { return Codec.from({ encode(userDefined) { try { const payload = (0, index_js_1.bytesFrom)(codec.encode(userDefined)); const byteLength = uint32To(payload.byteLength); return (0, index_js_1.bytesConcat)(byteLength, payload); } catch (e) { throw new Error(`byteVec(${e?.toString()})`); } }, decode(buffer, config) { const value = (0, index_js_1.bytesFrom)(buffer); if (value.byteLength < 4) { throw new Error(`byteVec: too short buffer, expected at least 4 bytes, but got ${value.byteLength}`); } const byteLength = uint32From(value.slice(0, 4)); if (byteLength !== value.byteLength - 4) { throw new Error(`byteVec: invalid buffer size, expected ${byteLength}, but got ${value.byteLength}`); } try { return codec.decode(value.slice(4), config); } catch (e) { throw new Error(`byteVec(${e?.toString()})`); } }, }); } /** * Table is a dynamic-size type. It can be considered as a dynvec but the length is fixed. * @param codecLayout */ function table(codecLayout) { const keys = Object.keys(codecLayout); return Codec.from({ encode(object) { let offset = 4 + keys.length * 4; const header = []; const body = []; for (const key of keys) { try { const encoded = codecLayout[key].encode(object[key]); (0, index_js_1.bytesConcatTo)(header, uint32To(offset)); (0, index_js_1.bytesConcatTo)(body, encoded); offset += encoded.byteLength; } catch (e) { throw new Error(`table.${key}(${e?.toString()})`); } } const packedTotalSize = uint32To(header.length + body.length + 4); return (0, index_js_1.bytesConcat)(packedTotalSize, header, body); }, decode(buffer, config) { const value = (0, index_js_1.bytesFrom)(buffer); if (value.byteLength < 4) { throw new Error(`table: too short buffer, expected at least 4 bytes, but got ${value.byteLength}`); } const byteLength = uint32From(value.slice(0, 4)); const headerLength = uint32From(value.slice(4, 8)); const actualFieldCount = (headerLength - 4) / 4; if (byteLength !== value.byteLength) { throw new Error(`table: invalid buffer size, expected ${byteLength}, but got ${value.byteLength}`); } if (actualFieldCount < keys.length) { throw new Error(`table: invalid field count, expected ${keys.length}, but got ${actualFieldCount}`); } if (actualFieldCount > keys.length && !config?.isExtraFieldIgnored) { throw new Error(`table: invalid field count, expected ${keys.length}, but got ${actualFieldCount}, and extra fields are not allowed in the current configuration. If you want to ignore extra fields, set isExtraFieldIgnored to true.`); } const offsets = keys.map((_, index) => uint32From(value.slice(4 + index * 4, 8 + index * 4))); // If there are extra fields, add the last offset to the offsets array if (actualFieldCount > keys.length) { offsets.push(uint32From(value.slice(4 + keys.length * 4, 8 + keys.length * 4))); } else { // If there are no extra fields, add the byte length to the offsets array offsets.push(byteLength); } const object = {}; for (let i = 0; i < offsets.length - 1; i++) { const start = offsets[i]; const end = offsets[i + 1]; const field = keys[i]; const codec = codecLayout[field]; const payload = value.slice(start, end); try { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment Object.assign(object, { [field]: codec.decode(payload, config) }); } catch (e) { throw new Error(`table.${field}(${e?.toString()})`); } } return object; }, }); } /** * 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 codecLayout the union item record * @param fields the custom item type id record * @example * // without custom id * union({ cafe: Uint8, bee: Uint8 }) * // with custom id * union({ cafe: Uint8, bee: Uint8 }, { cafe: 0xcafe, bee: 0xbee }) */ function union(codecLayout, fields) { const keys = Object.keys(codecLayout); return Codec.from({ encode({ type, value }) { const typeStr = type.toString(); const codec = codecLayout[typeStr]; if (!codec) { throw new Error(`union: invalid type, expected ${keys.toString()}, but got ${typeStr}`); } const fieldId = fields ? (fields[typeStr] ?? -1) : keys.indexOf(typeStr); if (fieldId < 0) { throw new Error(`union: invalid field id ${fieldId} of ${typeStr}`); } const header = uint32To(fieldId); try { const body = codec.encode(value); return (0, index_js_1.bytesConcat)(header, body); } catch (e) { throw new Error(`union.(${typeStr})(${e?.toString()})`); } }, decode(buffer, config) { const value = (0, index_js_1.bytesFrom)(buffer); const fieldIndex = uint32From(value.slice(0, 4)); const keys = Object.keys(codecLayout); const field = (() => { if (!fields) { return keys[fieldIndex]; } const entry = Object.entries(fields).find(([, id]) => id === fieldIndex); return entry?.[0]; })(); if (!field) { if (!fields) { throw new Error(`union: unknown union field index ${fieldIndex}, only ${keys.toString()} are allowed`); } const fieldKeys = Object.keys(fields); throw new Error(`union: unknown union field index ${fieldIndex}, only ${fieldKeys.toString()} and ${keys.toString()} are allowed`); } return { type: field, // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment value: codecLayout[field].decode(value.slice(4), config), }; }, }); } /** * 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 codecLayout a object contains all fields' codec */ function struct(codecLayout) { const codecArray = Object.values(codecLayout); const keys = Object.keys(codecLayout); return Codec.from({ byteLength: codecArray.reduce((acc, codec) => { if (codec.byteLength === undefined) { throw new Error("struct: all fields must be fixed-size"); } return acc + codec.byteLength; }, 0), encode(object) { const bytes = []; for (const key of keys) { try { const encoded = codecLayout[key].encode(object[key]); (0, index_js_1.bytesConcatTo)(bytes, encoded); } catch (e) { throw new Error(`struct.${key}(${e?.toString()})`); } } return (0, index_js_1.bytesFrom)(bytes); }, decode(buffer, config) { const value = (0, index_js_1.bytesFrom)(buffer); const object = {}; let offset = 0; Object.entries(codecLayout).forEach(([key, codec]) => { const payload = value.slice(offset, offset + codec.byteLength); try { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment Object.assign(object, { [key]: codec.decode(payload, config) }); } catch (e) { throw new Error(`struct.${key}(${e.toString()})`); } offset = offset + codec.byteLength; }); return object; }, }); } /** * 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 */ function array(itemCodec, itemCount) { if (itemCodec.byteLength === undefined) { throw new Error("array: itemCodec requires a byte length"); } const byteLength = itemCodec.byteLength * itemCount; return Codec.from({ byteLength, encode(items) { try { const bytes = []; for (const item of items) { (0, index_js_1.bytesConcatTo)(bytes, itemCodec.encode(item)); } return (0, index_js_1.bytesFrom)(bytes); } catch (e) { throw new Error(`array(${e?.toString()})`); } }, decode(buffer, config) { const value = (0, index_js_1.bytesFrom)(buffer); if (value.byteLength != byteLength) { throw new Error(`array: invalid buffer size, expected ${byteLength}, but got ${value.byteLength}`); } try { const result = []; for (let i = 0; i < value.byteLength; i += itemCodec.byteLength) { result.push(itemCodec.decode(value.slice(i, i + itemCodec.byteLength), config)); } return result; } catch (e) { throw new Error(`array(${e?.toString()})`); } }, }); } /** * Create a codec to deal with fixed LE or BE bytes. * @param byteLength * @param littleEndian */ function uint(byteLength, littleEndian = false) { return Codec.from({ byteLength, encode: (numLike) => { if (littleEndian) { return (0, index_js_2.numToBytes)(numLike, byteLength); } else { return (0, index_js_2.numBeToBytes)(numLike, byteLength); } }, decode: (buffer) => { if (littleEndian) { return (0, index_js_2.numFromBytes)(buffer); } else { return (0, index_js_2.numBeFromBytes)(buffer); } }, }); } /** * Create a codec to deal with fixed LE or BE bytes. * @param byteLength * @param littleEndian */ function uintNumber(byteLength, littleEndian = false) { if (byteLength > 4) { throw new Error("uintNumber: byteLength must be less than or equal to 4"); } return uint(byteLength, littleEndian).map({ outMap: (num) => Number(num), }); }