diff --git a/.changeset/olive-dryers-live.md b/.changeset/olive-dryers-live.md new file mode 100644 index 00000000..59272911 --- /dev/null +++ b/.changeset/olive-dryers-live.md @@ -0,0 +1,5 @@ +--- +"@ckb-ccc/core": minor +--- + +feat: compatible mode for molecule decode diff --git a/packages/core/src/molecule/codec.ts b/packages/core/src/molecule/codec.ts index 734b13f1..cf0af43a 100644 --- a/packages/core/src/molecule/codec.ts +++ b/packages/core/src/molecule/codec.ts @@ -18,13 +18,19 @@ import { export type CodecLike = { readonly encode: (encodable: Encodable) => Bytes; - readonly decode: (decodable: BytesLike) => Decoded; + readonly decode: ( + decodable: BytesLike, + config?: { isExtraFieldIgnored?: boolean }, + ) => Decoded; readonly byteLength?: number; }; export class Codec { constructor( public readonly encode: (encodable: Encodable) => Bytes, - public readonly decode: (decodable: BytesLike) => Decoded, + public readonly decode: ( + decodable: BytesLike, + config?: { isExtraFieldIgnored?: boolean }, // This is equivalent to "compatible" in the Rust implementation of Molecule. + ) => Decoded, public readonly byteLength?: number, // if provided, treat codec as fixed length ) {} @@ -43,7 +49,7 @@ export class Codec { } return encoded; }, - (decodable) => { + (decodable, config) => { const decodableBytes = bytesFrom(decodable); if ( byteLength !== undefined && @@ -53,7 +59,7 @@ export class Codec { `Codec.decode: expected byte length ${byteLength}, got ${decodableBytes.byteLength}`, ); } - return decode(decodable); + return decode(decodable, config); }, byteLength, ); @@ -69,10 +75,10 @@ export class Codec { return new Codec( (encodable) => this.encode((inMap ? inMap(encodable) : encodable) as Encodable), - (buffer) => + (buffer, config) => (outMap - ? outMap(this.decode(buffer)) - : this.decode(buffer)) as NewDecoded, + ? outMap(this.decode(buffer, config)) + : this.decode(buffer, config)) as NewDecoded, this.byteLength, ); } @@ -128,7 +134,7 @@ export function fixedItemVec( throw new Error(`fixedItemVec(${e?.toString()})`); } }, - decode(buffer) { + decode(buffer, config) { const value = bytesFrom(buffer); if (value.byteLength < 4) { throw new Error( @@ -147,7 +153,10 @@ export function fixedItemVec( const decodedArray: Array = []; for (let offset = 4; offset < byteLength; offset += itemByteLength) { decodedArray.push( - itemCodec.decode(value.slice(offset, offset + itemByteLength)), + itemCodec.decode( + value.slice(offset, offset + itemByteLength), + config, + ), ); } return decodedArray; @@ -185,7 +194,7 @@ export function dynItemVec( throw new Error(`dynItemVec(${e?.toString()})`); } }, - decode(buffer) { + decode(buffer, config) { const value = bytesFrom(buffer); if (value.byteLength < 4) { throw new Error( @@ -215,7 +224,7 @@ export function dynItemVec( const start = offsets[index]; const end = offsets[index + 1]; const itemBuffer = value.slice(start, end); - decodedArray.push(itemCodec.decode(itemBuffer)); + decodedArray.push(itemCodec.decode(itemBuffer, config)); } return decodedArray; } catch (e) { @@ -259,13 +268,13 @@ export function option( throw new Error(`option(${e?.toString()})`); } }, - decode(buffer) { + decode(buffer, config) { const value = bytesFrom(buffer); if (value.byteLength === 0) { return undefined; } try { - return innerCodec.decode(buffer); + return innerCodec.decode(buffer, config); } catch (e) { throw new Error(`option(${e?.toString()})`); } @@ -290,7 +299,7 @@ export function byteVec( throw new Error(`byteVec(${e?.toString()})`); } }, - decode(buffer) { + decode(buffer, config) { const value = bytesFrom(buffer); if (value.byteLength < 4) { throw new Error( @@ -304,7 +313,7 @@ export function byteVec( ); } try { - return codec.decode(value.slice(4)); + return codec.decode(value.slice(4), config); } catch (e: unknown) { throw new Error(`byteVec(${e?.toString()})`); } @@ -371,7 +380,7 @@ export function table< const packedTotalSize = uint32To(header.length + body.length + 4); return bytesConcat(packedTotalSize, header, body); }, - decode(buffer) { + decode(buffer, config) { const value = bytesFrom(buffer); if (value.byteLength < 4) { throw new Error( @@ -379,15 +388,38 @@ export function table< ); } 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)), ); - offsets.push(byteLength); + // 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]; @@ -397,7 +429,7 @@ export function table< const payload = value.slice(start, end); try { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - Object.assign(object, { [field]: codec.decode(payload) }); + Object.assign(object, { [field]: codec.decode(payload, config) }); } catch (e: unknown) { throw new Error(`table.${field}(${e?.toString()})`); } @@ -466,7 +498,7 @@ export function union>>( throw new Error(`union.(${typeStr})(${e?.toString()})`); } }, - decode(buffer) { + decode(buffer, config) { const value = bytesFrom(buffer); const fieldIndex = uint32From(value.slice(0, 4)); const keys = Object.keys(codecLayout); @@ -496,7 +528,7 @@ export function union>>( return { type: field, // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - value: codecLayout[field].decode(value.slice(4)), + value: codecLayout[field].decode(value.slice(4), config), } as UnionDecoded; }, }); @@ -535,7 +567,7 @@ export function struct< return bytesFrom(bytes); }, - decode(buffer) { + decode(buffer, config) { const value = bytesFrom(buffer); const object = {}; let offset = 0; @@ -543,7 +575,7 @@ export function struct< 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) }); + Object.assign(object, { [key]: codec.decode(payload, config) }); } catch (e: unknown) { throw new Error(`struct.${key}(${(e as Error).toString()})`); } @@ -583,7 +615,7 @@ export function array( throw new Error(`array(${e?.toString()})`); } }, - decode(buffer) { + decode(buffer, config) { const value = bytesFrom(buffer); if (value.byteLength != byteLength) { throw new Error( @@ -594,7 +626,7 @@ export function array( const result: Array = []; for (let i = 0; i < value.byteLength; i += itemCodec.byteLength!) { result.push( - itemCodec.decode(value.slice(i, i + itemCodec.byteLength!)), + itemCodec.decode(value.slice(i, i + itemCodec.byteLength!), config), ); } return result;