@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.
- package/package.json +4 -3
- package/src/add-codec-sentinel.ts +186 -0
- package/src/add-codec-size-prefix.ts +161 -0
- package/src/array-buffers.ts +25 -0
- package/src/assertions.ts +103 -0
- package/src/bytes.ts +145 -0
- package/src/codec.ts +925 -0
- package/src/combine-codec.ts +133 -0
- package/src/decoder-entire-byte-array.ts +45 -0
- package/src/fix-codec-size.ts +170 -0
- package/src/index.ts +668 -0
- package/src/offset-codec.ts +379 -0
- package/src/pad-codec.ts +197 -0
- package/src/readonly-uint8array.ts +21 -0
- package/src/resize-codec.ts +209 -0
- package/src/reverse-codec.ts +159 -0
- package/src/transform-codec.ts +208 -0
|
@@ -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
|
+
}
|