@ckb-ccc/core
Version:
Core of CCC - CKBer's Codebase
499 lines (498 loc) • 19.6 kB
JavaScript
/* 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),
});
}
;