import { assertByteArrayHasEnoughBytesForCodec } from './assertions'; import { Codec, createDecoder, createEncoder, Decoder, Encoder, FixedSizeCodec, FixedSizeDecoder, FixedSizeEncoder, getEncodedSize, isFixedSize, VariableSizeCodec, VariableSizeDecoder, VariableSizeEncoder, } from './codec'; import { combineCodec } from './combine-codec'; type NumberEncoder = Encoder | Encoder; type FixedSizeNumberEncoder = | FixedSizeEncoder | FixedSizeEncoder; type NumberDecoder = Decoder | Decoder; type FixedSizeNumberDecoder = | FixedSizeDecoder | FixedSizeDecoder; type NumberCodec = Codec | Codec; type FixedSizeNumberCodec = | FixedSizeCodec | FixedSizeCodec; /** * Stores the size of the `encoder` in bytes as a prefix using the `prefix` encoder. * * See {@link addCodecSizePrefix} for more information. * * @typeParam TFrom - The type of the value to encode. * * @see {@link addCodecSizePrefix} */ export function addEncoderSizePrefix( encoder: FixedSizeEncoder, prefix: FixedSizeNumberEncoder, ): FixedSizeEncoder; export function addEncoderSizePrefix(encoder: Encoder, prefix: NumberEncoder): VariableSizeEncoder; export function addEncoderSizePrefix(encoder: Encoder, prefix: NumberEncoder): Encoder { const write = ((value, bytes, offset) => { // Here we exceptionally use the `encode` function instead of the `write` // function to contain the content of the encoder within its own bounds. const encoderBytes = encoder.encode(value); offset = prefix.write(encoderBytes.length, bytes, offset); bytes.set(encoderBytes, offset); return offset + encoderBytes.length; }) as Encoder['write']; if (isFixedSize(prefix) && isFixedSize(encoder)) { return createEncoder({ ...encoder, fixedSize: prefix.fixedSize + encoder.fixedSize, write }); } const prefixMaxSize = isFixedSize(prefix) ? prefix.fixedSize : (prefix.maxSize ?? null); const encoderMaxSize = isFixedSize(encoder) ? encoder.fixedSize : (encoder.maxSize ?? null); const maxSize = prefixMaxSize !== null && encoderMaxSize !== null ? prefixMaxSize + encoderMaxSize : null; return createEncoder({ ...encoder, ...(maxSize !== null ? { maxSize } : {}), getSizeFromValue: value => { const encoderSize = getEncodedSize(value, encoder); return getEncodedSize(encoderSize, prefix) + encoderSize; }, write, }); } /** * Bounds the size of the nested `decoder` by reading its encoded `prefix`. * * See {@link addCodecSizePrefix} for more information. * * @typeParam TTo - The type of the decoded value. * * @see {@link addCodecSizePrefix} */ export function addDecoderSizePrefix( decoder: FixedSizeDecoder, prefix: FixedSizeNumberDecoder, ): FixedSizeDecoder; export function addDecoderSizePrefix(decoder: Decoder, prefix: NumberDecoder): VariableSizeDecoder; export function addDecoderSizePrefix(decoder: Decoder, prefix: NumberDecoder): Decoder { const read = ((bytes, offset) => { const [bigintSize, decoderOffset] = prefix.read(bytes, offset); const size = Number(bigintSize); offset = decoderOffset; // Slice the byte array to the contained size if necessary. if (offset > 0 || bytes.length > size) { bytes = bytes.slice(offset, offset + size); } assertByteArrayHasEnoughBytesForCodec('addDecoderSizePrefix', size, bytes); // Here we exceptionally use the `decode` function instead of the `read` // function to contain the content of the decoder within its own bounds. return [decoder.decode(bytes), offset + size]; }) as Decoder['read']; if (isFixedSize(prefix) && isFixedSize(decoder)) { return createDecoder({ ...decoder, fixedSize: prefix.fixedSize + decoder.fixedSize, read }); } const prefixMaxSize = isFixedSize(prefix) ? prefix.fixedSize : (prefix.maxSize ?? null); const decoderMaxSize = isFixedSize(decoder) ? decoder.fixedSize : (decoder.maxSize ?? null); const maxSize = prefixMaxSize !== null && decoderMaxSize !== null ? prefixMaxSize + decoderMaxSize : null; return createDecoder({ ...decoder, ...(maxSize !== null ? { maxSize } : {}), read }); } /** * Stores the byte size of any given codec as an encoded number prefix. * * This sets a limit on variable-size codecs and tells us when to stop decoding. * When encoding, the size of the encoded data is stored before the encoded data itself. * When decoding, the size is read first to know how many bytes to read next. * * @typeParam TFrom - The type of the value to encode. * @typeParam TTo - The type of the decoded value. * * @example * For example, say we want to bound a variable-size base-58 string using a `u32` size prefix. * Here’s how you can use the `addCodecSizePrefix` function to achieve that. * * ```ts * const getU32Base58Codec = () => addCodecSizePrefix(getBase58Codec(), getU32Codec()); * * getU32Base58Codec().encode('hello world'); * // 0x0b00000068656c6c6f20776f726c64 * // | └-- Our encoded base-58 string. * // └-- Our encoded u32 size prefix. * ``` * * @remarks * Separate {@link addEncoderSizePrefix} and {@link addDecoderSizePrefix} functions are also available. * * ```ts * const bytes = addEncoderSizePrefix(getBase58Encoder(), getU32Encoder()).encode('hello'); * const value = addDecoderSizePrefix(getBase58Decoder(), getU32Decoder()).decode(bytes); * ``` * * @see {@link addEncoderSizePrefix} * @see {@link addDecoderSizePrefix} */ export function addCodecSizePrefix( codec: FixedSizeCodec, prefix: FixedSizeNumberCodec, ): FixedSizeCodec; export function addCodecSizePrefix( codec: Codec, prefix: NumberCodec, ): VariableSizeCodec; export function addCodecSizePrefix( codec: Codec, prefix: NumberCodec, ): Codec { return combineCodec(addEncoderSizePrefix(codec, prefix), addDecoderSizePrefix(codec, prefix)); }