@solana/codecs-core 6.3.1 → 6.3.2-canary-20260313112147

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,133 @@
1
+ import {
2
+ SOLANA_ERROR__CODECS__ENCODER_DECODER_FIXED_SIZE_MISMATCH,
3
+ SOLANA_ERROR__CODECS__ENCODER_DECODER_MAX_SIZE_MISMATCH,
4
+ SOLANA_ERROR__CODECS__ENCODER_DECODER_SIZE_COMPATIBILITY_MISMATCH,
5
+ SolanaError,
6
+ } from '@solana/errors';
7
+
8
+ import {
9
+ Codec,
10
+ Decoder,
11
+ Encoder,
12
+ FixedSizeCodec,
13
+ FixedSizeDecoder,
14
+ FixedSizeEncoder,
15
+ isFixedSize,
16
+ VariableSizeCodec,
17
+ VariableSizeDecoder,
18
+ VariableSizeEncoder,
19
+ } from './codec';
20
+
21
+ /**
22
+ * Combines an `Encoder` and a `Decoder` into a `Codec`.
23
+ *
24
+ * That is, given a `Encoder<TFrom>` and a `Decoder<TTo>`, this function returns a `Codec<TFrom, TTo>`.
25
+ *
26
+ * This allows for modular composition by keeping encoding and decoding logic separate
27
+ * while still offering a convenient way to bundle them into a single `Codec`.
28
+ * This is particularly useful for library maintainers who want to expose `Encoders`,
29
+ * `Decoders`, and `Codecs` separately, enabling tree-shaking of unused logic.
30
+ *
31
+ * The provided `Encoder` and `Decoder` must be compatible in terms of:
32
+ * - **Fixed Size:** If both are fixed-size, they must have the same `fixedSize` value.
33
+ * - **Variable Size:** If either has a `maxSize` attribute, it must match the other.
34
+ *
35
+ * If these conditions are not met, a {@link SolanaError} will be thrown.
36
+ *
37
+ * @typeParam TFrom - The type of the value to encode.
38
+ * @typeParam TTo - The type of the decoded value.
39
+ * @typeParam TSize - The fixed size of the encoded value in bytes (for fixed-size codecs).
40
+ *
41
+ * @param encoder - The `Encoder` to combine.
42
+ * @param decoder - The `Decoder` to combine.
43
+ * @returns A `Codec` that provides both `encode` and `decode` methods.
44
+ *
45
+ * @throws {SolanaError}
46
+ * - `SOLANA_ERROR__CODECS__ENCODER_DECODER_SIZE_COMPATIBILITY_MISMATCH`
47
+ * Thrown if the encoder and decoder have mismatched size types (fixed vs. variable).
48
+ * - `SOLANA_ERROR__CODECS__ENCODER_DECODER_FIXED_SIZE_MISMATCH`
49
+ * Thrown if both are fixed-size but have different `fixedSize` values.
50
+ * - `SOLANA_ERROR__CODECS__ENCODER_DECODER_MAX_SIZE_MISMATCH`
51
+ * Thrown if the `maxSize` attributes do not match.
52
+ *
53
+ * @example
54
+ * Creating a fixed-size `Codec` from an encoder and a decoder.
55
+ * ```ts
56
+ * const encoder = getU32Encoder();
57
+ * const decoder = getU32Decoder();
58
+ * const codec = combineCodec(encoder, decoder);
59
+ *
60
+ * const bytes = codec.encode(42); // 0x2a000000
61
+ * const value = codec.decode(bytes); // 42
62
+ * ```
63
+ *
64
+ * @example
65
+ * Creating a variable-size `Codec` from an encoder and a decoder.
66
+ * ```ts
67
+ * const encoder = addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder());
68
+ * const decoder = addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder());
69
+ * const codec = combineCodec(encoder, decoder);
70
+ *
71
+ * const bytes = codec.encode("hello"); // 0x0500000068656c6c6f
72
+ * const value = codec.decode(bytes); // "hello"
73
+ * ```
74
+ *
75
+ * @remarks
76
+ * The recommended pattern for defining codecs in libraries is to expose separate functions for the encoder, decoder, and codec.
77
+ * This allows users to import only what they need, improving tree-shaking efficiency.
78
+ *
79
+ * ```ts
80
+ * type MyType = \/* ... *\/;
81
+ * const getMyTypeEncoder = (): Encoder<MyType> => { \/* ... *\/ };
82
+ * const getMyTypeDecoder = (): Decoder<MyType> => { \/* ... *\/ };
83
+ * const getMyTypeCodec = (): Codec<MyType> =>
84
+ * combineCodec(getMyTypeEncoder(), getMyTypeDecoder());
85
+ * ```
86
+ *
87
+ * @see {@link Codec}
88
+ * @see {@link Encoder}
89
+ * @see {@link Decoder}
90
+ */
91
+ export function combineCodec<TFrom, TTo extends TFrom, TSize extends number>(
92
+ encoder: FixedSizeEncoder<TFrom, TSize>,
93
+ decoder: FixedSizeDecoder<TTo, TSize>,
94
+ ): FixedSizeCodec<TFrom, TTo, TSize>;
95
+ export function combineCodec<TFrom, TTo extends TFrom>(
96
+ encoder: VariableSizeEncoder<TFrom>,
97
+ decoder: VariableSizeDecoder<TTo>,
98
+ ): VariableSizeCodec<TFrom, TTo>;
99
+ export function combineCodec<TFrom, TTo extends TFrom>(
100
+ encoder: Encoder<TFrom>,
101
+ decoder: Decoder<TTo>,
102
+ ): Codec<TFrom, TTo>;
103
+ export function combineCodec<TFrom, TTo extends TFrom>(
104
+ encoder: Encoder<TFrom>,
105
+ decoder: Decoder<TTo>,
106
+ ): Codec<TFrom, TTo> {
107
+ if (isFixedSize(encoder) !== isFixedSize(decoder)) {
108
+ throw new SolanaError(SOLANA_ERROR__CODECS__ENCODER_DECODER_SIZE_COMPATIBILITY_MISMATCH);
109
+ }
110
+
111
+ if (isFixedSize(encoder) && isFixedSize(decoder) && encoder.fixedSize !== decoder.fixedSize) {
112
+ throw new SolanaError(SOLANA_ERROR__CODECS__ENCODER_DECODER_FIXED_SIZE_MISMATCH, {
113
+ decoderFixedSize: decoder.fixedSize,
114
+ encoderFixedSize: encoder.fixedSize,
115
+ });
116
+ }
117
+
118
+ if (!isFixedSize(encoder) && !isFixedSize(decoder) && encoder.maxSize !== decoder.maxSize) {
119
+ throw new SolanaError(SOLANA_ERROR__CODECS__ENCODER_DECODER_MAX_SIZE_MISMATCH, {
120
+ decoderMaxSize: decoder.maxSize,
121
+ encoderMaxSize: encoder.maxSize,
122
+ });
123
+ }
124
+
125
+ return {
126
+ ...decoder,
127
+ ...encoder,
128
+ decode: decoder.decode,
129
+ encode: encoder.encode,
130
+ read: decoder.read,
131
+ write: encoder.write,
132
+ };
133
+ }
@@ -0,0 +1,45 @@
1
+ import { SOLANA_ERROR__CODECS__EXPECTED_DECODER_TO_CONSUME_ENTIRE_BYTE_ARRAY, SolanaError } from '@solana/errors';
2
+
3
+ import { createDecoder, Decoder } from './codec';
4
+
5
+ /**
6
+ * Create a {@link Decoder} that asserts that the bytes provided to `decode` or `read` are fully consumed by the inner decoder
7
+ * @param decoder A decoder to wrap
8
+ * @returns A new decoder that will throw if provided with a byte array that it does not fully consume
9
+ *
10
+ * @typeParam T - The type of the decoder
11
+ *
12
+ * @remarks
13
+ * Note that this compares the offset after encoding to the length of the input byte array
14
+ *
15
+ * The `offset` parameter to `decode` and `read` is still considered, and will affect the new offset that is compared to the byte array length
16
+ *
17
+ * The error that is thrown by the returned decoder is a {@link SolanaError} with the code `SOLANA_ERROR__CODECS__EXPECTED_DECODER_TO_CONSUME_ENTIRE_BYTE_ARRAY`
18
+ *
19
+ * @example
20
+ * Create a decoder that decodes a `u32` (4 bytes) and ensures the entire byte array is consumed
21
+ * ```ts
22
+ * const decoder = createDecoderThatUsesExactByteArray(getU32Decoder());
23
+ * decoder.decode(new Uint8Array([0, 0, 0, 0])); // 0
24
+ * decoder.decode(new Uint8Array([0, 0, 0, 0, 0])); // throws
25
+ *
26
+ * // with an offset
27
+ * decoder.decode(new Uint8Array([0, 0, 0, 0, 0]), 1); // 0
28
+ * decoder.decode(new Uint8Array([0, 0, 0, 0, 0, 0]), 1); // throws
29
+ * ```
30
+ */
31
+ export function createDecoderThatConsumesEntireByteArray<T>(decoder: Decoder<T>): Decoder<T> {
32
+ return createDecoder({
33
+ ...decoder,
34
+ read(bytes, offset) {
35
+ const [value, newOffset] = decoder.read(bytes, offset);
36
+ if (bytes.length > newOffset) {
37
+ throw new SolanaError(SOLANA_ERROR__CODECS__EXPECTED_DECODER_TO_CONSUME_ENTIRE_BYTE_ARRAY, {
38
+ expectedLength: newOffset,
39
+ numExcessBytes: bytes.length - newOffset,
40
+ });
41
+ }
42
+ return [value, newOffset];
43
+ },
44
+ });
45
+ }
@@ -0,0 +1,170 @@
1
+ import { assertByteArrayHasEnoughBytesForCodec } from './assertions';
2
+ import { fixBytes } from './bytes';
3
+ import {
4
+ Codec,
5
+ createDecoder,
6
+ createEncoder,
7
+ Decoder,
8
+ Encoder,
9
+ FixedSizeCodec,
10
+ FixedSizeDecoder,
11
+ FixedSizeEncoder,
12
+ isFixedSize,
13
+ Offset,
14
+ } from './codec';
15
+ import { combineCodec } from './combine-codec';
16
+
17
+ /**
18
+ * Creates a fixed-size encoder from a given encoder.
19
+ *
20
+ * The resulting encoder ensures that encoded values always have the specified number of bytes.
21
+ * If the original encoded value is larger than `fixedBytes`, it is truncated.
22
+ * If it is smaller, it is padded with trailing zeroes.
23
+ *
24
+ * For more details, see {@link fixCodecSize}.
25
+ *
26
+ * @typeParam TFrom - The type of the value to encode.
27
+ * @typeParam TSize - The fixed size of the encoded value in bytes.
28
+ *
29
+ * @param encoder - The encoder to wrap into a fixed-size encoder.
30
+ * @param fixedBytes - The fixed number of bytes to write.
31
+ * @returns A `FixedSizeEncoder` that ensures a consistent output size.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * const encoder = fixEncoderSize(getUtf8Encoder(), 4);
36
+ * encoder.encode("Hello"); // 0x48656c6c (truncated)
37
+ * encoder.encode("Hi"); // 0x48690000 (padded)
38
+ * encoder.encode("Hiya"); // 0x48697961 (same length)
39
+ * ```
40
+ *
41
+ * @remarks
42
+ * If you need a full codec with both encoding and decoding, use {@link fixCodecSize}.
43
+ *
44
+ * @see {@link fixCodecSize}
45
+ * @see {@link fixDecoderSize}
46
+ */
47
+ export function fixEncoderSize<TFrom, TSize extends number>(
48
+ encoder: Encoder<TFrom>,
49
+ fixedBytes: TSize,
50
+ ): FixedSizeEncoder<TFrom, TSize> {
51
+ return createEncoder({
52
+ fixedSize: fixedBytes,
53
+ write: (value: TFrom, bytes: Uint8Array, offset: Offset) => {
54
+ // Here we exceptionally use the `encode` function instead of the `write`
55
+ // function as using the nested `write` function on a fixed-sized byte
56
+ // array may result in a out-of-bounds error on the nested encoder.
57
+ const variableByteArray = encoder.encode(value);
58
+ const fixedByteArray =
59
+ variableByteArray.length > fixedBytes ? variableByteArray.slice(0, fixedBytes) : variableByteArray;
60
+ bytes.set(fixedByteArray, offset);
61
+ return offset + fixedBytes;
62
+ },
63
+ });
64
+ }
65
+
66
+ /**
67
+ * Creates a fixed-size decoder from a given decoder.
68
+ *
69
+ * The resulting decoder always reads exactly `fixedBytes` bytes from the input.
70
+ * If the nested decoder is also fixed-size, the bytes are truncated or padded as needed.
71
+ *
72
+ * For more details, see {@link fixCodecSize}.
73
+ *
74
+ * @typeParam TTo - The type of the decoded value.
75
+ * @typeParam TSize - The fixed size of the encoded value in bytes.
76
+ *
77
+ * @param decoder - The decoder to wrap into a fixed-size decoder.
78
+ * @param fixedBytes - The fixed number of bytes to read.
79
+ * @returns A `FixedSizeDecoder` that ensures a consistent input size.
80
+ *
81
+ * @example
82
+ * ```ts
83
+ * const decoder = fixDecoderSize(getUtf8Decoder(), 4);
84
+ * decoder.decode(new Uint8Array([72, 101, 108, 108, 111])); // "Hell" (truncated)
85
+ * decoder.decode(new Uint8Array([72, 105, 0, 0])); // "Hi" (zeroes ignored)
86
+ * decoder.decode(new Uint8Array([72, 105, 121, 97])); // "Hiya" (same length)
87
+ * ```
88
+ *
89
+ * @remarks
90
+ * If you need a full codec with both encoding and decoding, use {@link fixCodecSize}.
91
+ *
92
+ * @see {@link fixCodecSize}
93
+ * @see {@link fixEncoderSize}
94
+ */
95
+ export function fixDecoderSize<TTo, TSize extends number>(
96
+ decoder: Decoder<TTo>,
97
+ fixedBytes: TSize,
98
+ ): FixedSizeDecoder<TTo, TSize> {
99
+ return createDecoder({
100
+ fixedSize: fixedBytes,
101
+ read: (bytes, offset) => {
102
+ assertByteArrayHasEnoughBytesForCodec('fixCodecSize', fixedBytes, bytes, offset);
103
+ // Slice the byte array to the fixed size if necessary.
104
+ if (offset > 0 || bytes.length > fixedBytes) {
105
+ bytes = bytes.slice(offset, offset + fixedBytes);
106
+ }
107
+ // If the nested decoder is fixed-size, pad and truncate the byte array accordingly.
108
+ if (isFixedSize(decoder)) {
109
+ bytes = fixBytes(bytes, decoder.fixedSize);
110
+ }
111
+ // Decode the value using the nested decoder.
112
+ const [value] = decoder.read(bytes, 0);
113
+ return [value, offset + fixedBytes];
114
+ },
115
+ });
116
+ }
117
+
118
+ /**
119
+ * Creates a fixed-size codec from a given codec.
120
+ *
121
+ * The resulting codec ensures that both encoding and decoding operate on a fixed number of bytes.
122
+ * When encoding:
123
+ * - If the encoded value is larger than `fixedBytes`, it is truncated.
124
+ * - If it is smaller, it is padded with trailing zeroes.
125
+ * - If it is exactly `fixedBytes`, it remains unchanged.
126
+ *
127
+ * When decoding:
128
+ * - Exactly `fixedBytes` bytes are read from the input.
129
+ * - If the nested decoder has a smaller fixed size, bytes are truncated or padded as necessary.
130
+ *
131
+ * @typeParam TFrom - The type of the value to encode.
132
+ * @typeParam TTo - The type of the decoded value.
133
+ * @typeParam TSize - The fixed size of the encoded value in bytes.
134
+ *
135
+ * @param codec - The codec to wrap into a fixed-size codec.
136
+ * @param fixedBytes - The fixed number of bytes to read/write.
137
+ * @returns A `FixedSizeCodec` that ensures both encoding and decoding conform to a fixed size.
138
+ *
139
+ * @example
140
+ * ```ts
141
+ * const codec = fixCodecSize(getUtf8Codec(), 4);
142
+ *
143
+ * const bytes1 = codec.encode("Hello"); // 0x48656c6c (truncated)
144
+ * const value1 = codec.decode(bytes1); // "Hell"
145
+ *
146
+ * const bytes2 = codec.encode("Hi"); // 0x48690000 (padded)
147
+ * const value2 = codec.decode(bytes2); // "Hi"
148
+ *
149
+ * const bytes3 = codec.encode("Hiya"); // 0x48697961 (same length)
150
+ * const value3 = codec.decode(bytes3); // "Hiya"
151
+ * ```
152
+ *
153
+ * @remarks
154
+ * If you only need to enforce a fixed size for encoding, use {@link fixEncoderSize}.
155
+ * If you only need to enforce a fixed size for decoding, use {@link fixDecoderSize}.
156
+ *
157
+ * ```ts
158
+ * const bytes = fixEncoderSize(getUtf8Encoder(), 4).encode("Hiya");
159
+ * const value = fixDecoderSize(getUtf8Decoder(), 4).decode(bytes);
160
+ * ```
161
+ *
162
+ * @see {@link fixEncoderSize}
163
+ * @see {@link fixDecoderSize}
164
+ */
165
+ export function fixCodecSize<TFrom, TTo extends TFrom, TSize extends number>(
166
+ codec: Codec<TFrom, TTo>,
167
+ fixedBytes: TSize,
168
+ ): FixedSizeCodec<TFrom, TTo, TSize> {
169
+ return combineCodec(fixEncoderSize(codec, fixedBytes), fixDecoderSize(codec, fixedBytes));
170
+ }