@ckb-ccc/core
Version:
Core of CCC - CKBer's Codebase
484 lines (483 loc) • 18.8 kB
JavaScript
/* eslint-disable @typescript-eslint/no-explicit-any */
import { bytesConcat, bytesConcatTo, bytesFrom, } from "../bytes/index.js";
import { numBeFromBytes, numBeToBytes, numFromBytes, numToBytes, } from "../num/index.js";
export 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 = 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 });
}
}
function uint32To(numLike) {
return numToBytes(numLike, 4);
}
function uint32From(bytesLike) {
return Number(numFromBytes(bytesLike));
}
/**
* Vector with fixed size item codec
* @param itemCodec fixed-size vector item codec
*/
export 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 = [];
bytesConcatTo(concatted, uint32To(userDefinedItems.length));
for (const item of userDefinedItems) {
bytesConcatTo(concatted, itemCodec.encode(item));
}
return bytesFrom(concatted);
}
catch (e) {
throw new Error(`fixedItemVec(${e?.toString()})`);
}
},
decode(buffer, config) {
const value = 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.
*/
export 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);
bytesConcatTo(header, uint32To(offset));
bytesConcatTo(body, encoded);
offset += encoded.byteLength;
}
const packedTotalSize = uint32To(header.length + body.length + 4);
return bytesConcat(packedTotalSize, header, body);
}
catch (e) {
throw new Error(`dynItemVec(${e?.toString()})`);
}
},
decode(buffer, config) {
const value = 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
*/
export 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
*/
export function option(innerCodec) {
return Codec.from({
encode(userDefinedOrNull) {
if (userDefinedOrNull == null) {
return bytesFrom([]);
}
try {
return innerCodec.encode(userDefinedOrNull);
}
catch (e) {
throw new Error(`option(${e?.toString()})`);
}
},
decode(buffer, config) {
const value = 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
*/
export function byteVec(codec) {
return Codec.from({
encode(userDefined) {
try {
const payload = bytesFrom(codec.encode(userDefined));
const byteLength = uint32To(payload.byteLength);
return bytesConcat(byteLength, payload);
}
catch (e) {
throw new Error(`byteVec(${e?.toString()})`);
}
},
decode(buffer, config) {
const value = 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
*/
export 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]);
bytesConcatTo(header, uint32To(offset));
bytesConcatTo(body, encoded);
offset += encoded.byteLength;
}
catch (e) {
throw new Error(`table.${key}(${e?.toString()})`);
}
}
const packedTotalSize = uint32To(header.length + body.length + 4);
return bytesConcat(packedTotalSize, header, body);
},
decode(buffer, config) {
const value = 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 })
*/
export 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 bytesConcat(header, body);
}
catch (e) {
throw new Error(`union.(${typeStr})(${e?.toString()})`);
}
},
decode(buffer, config) {
const value = 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
*/
export 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]);
bytesConcatTo(bytes, encoded);
}
catch (e) {
throw new Error(`struct.${key}(${e?.toString()})`);
}
}
return bytesFrom(bytes);
},
decode(buffer, config) {
const value = 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
*/
export 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) {
bytesConcatTo(bytes, itemCodec.encode(item));
}
return bytesFrom(bytes);
}
catch (e) {
throw new Error(`array(${e?.toString()})`);
}
},
decode(buffer, config) {
const value = 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
*/
export function uint(byteLength, littleEndian = false) {
return Codec.from({
byteLength,
encode: (numLike) => {
if (littleEndian) {
return numToBytes(numLike, byteLength);
}
else {
return numBeToBytes(numLike, byteLength);
}
},
decode: (buffer) => {
if (littleEndian) {
return numFromBytes(buffer);
}
else {
return numBeFromBytes(buffer);
}
},
});
}
/**
* Create a codec to deal with fixed LE or BE bytes.
* @param byteLength
* @param littleEndian
*/
export 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),
});
}