@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solana/codecs-core",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.2-canary-20260313112147",
|
|
4
4
|
"description": "Core types and helpers for encoding and decoding byte arrays on Solana",
|
|
5
5
|
"homepage": "https://www.solanakit.com/api#solanacodecs-core",
|
|
6
6
|
"exports": {
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
"types": "./dist/types/index.d.ts",
|
|
34
34
|
"type": "commonjs",
|
|
35
35
|
"files": [
|
|
36
|
-
"./dist/"
|
|
36
|
+
"./dist/",
|
|
37
|
+
"./src/"
|
|
37
38
|
],
|
|
38
39
|
"sideEffects": false,
|
|
39
40
|
"keywords": [
|
|
@@ -55,7 +56,7 @@
|
|
|
55
56
|
"maintained node versions"
|
|
56
57
|
],
|
|
57
58
|
"dependencies": {
|
|
58
|
-
"@solana/errors": "6.3.
|
|
59
|
+
"@solana/errors": "6.3.2-canary-20260313112147"
|
|
59
60
|
},
|
|
60
61
|
"peerDependencies": {
|
|
61
62
|
"typescript": "^5.0.0"
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SOLANA_ERROR__CODECS__ENCODED_BYTES_MUST_NOT_INCLUDE_SENTINEL,
|
|
3
|
+
SOLANA_ERROR__CODECS__SENTINEL_MISSING_IN_DECODED_BYTES,
|
|
4
|
+
SolanaError,
|
|
5
|
+
} from '@solana/errors';
|
|
6
|
+
|
|
7
|
+
import { containsBytes } from './bytes';
|
|
8
|
+
import {
|
|
9
|
+
Codec,
|
|
10
|
+
createDecoder,
|
|
11
|
+
createEncoder,
|
|
12
|
+
Decoder,
|
|
13
|
+
Encoder,
|
|
14
|
+
FixedSizeCodec,
|
|
15
|
+
FixedSizeDecoder,
|
|
16
|
+
FixedSizeEncoder,
|
|
17
|
+
isFixedSize,
|
|
18
|
+
VariableSizeCodec,
|
|
19
|
+
VariableSizeDecoder,
|
|
20
|
+
VariableSizeEncoder,
|
|
21
|
+
} from './codec';
|
|
22
|
+
import { combineCodec } from './combine-codec';
|
|
23
|
+
import { ReadonlyUint8Array } from './readonly-uint8array';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Creates an encoder that writes a `Uint8Array` sentinel after the encoded value.
|
|
27
|
+
* This is useful to delimit the encoded value when being read by a decoder.
|
|
28
|
+
*
|
|
29
|
+
* See {@link addCodecSentinel} for more information.
|
|
30
|
+
*
|
|
31
|
+
* @typeParam TFrom - The type of the value to encode.
|
|
32
|
+
*
|
|
33
|
+
* @see {@link addCodecSentinel}
|
|
34
|
+
*/
|
|
35
|
+
export function addEncoderSentinel<TFrom>(
|
|
36
|
+
encoder: FixedSizeEncoder<TFrom>,
|
|
37
|
+
sentinel: ReadonlyUint8Array,
|
|
38
|
+
): FixedSizeEncoder<TFrom>;
|
|
39
|
+
export function addEncoderSentinel<TFrom>(
|
|
40
|
+
encoder: Encoder<TFrom>,
|
|
41
|
+
sentinel: ReadonlyUint8Array,
|
|
42
|
+
): VariableSizeEncoder<TFrom>;
|
|
43
|
+
export function addEncoderSentinel<TFrom>(encoder: Encoder<TFrom>, sentinel: ReadonlyUint8Array): Encoder<TFrom> {
|
|
44
|
+
const write = ((value, bytes, offset) => {
|
|
45
|
+
// Here we exceptionally use the `encode` function instead of the `write`
|
|
46
|
+
// function to contain the content of the encoder within its own bounds
|
|
47
|
+
// and to avoid writing the sentinel as part of the encoded value.
|
|
48
|
+
const encoderBytes = encoder.encode(value);
|
|
49
|
+
if (findSentinelIndex(encoderBytes, sentinel) >= 0) {
|
|
50
|
+
throw new SolanaError(SOLANA_ERROR__CODECS__ENCODED_BYTES_MUST_NOT_INCLUDE_SENTINEL, {
|
|
51
|
+
encodedBytes: encoderBytes,
|
|
52
|
+
hexEncodedBytes: hexBytes(encoderBytes),
|
|
53
|
+
hexSentinel: hexBytes(sentinel),
|
|
54
|
+
sentinel,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
bytes.set(encoderBytes, offset);
|
|
58
|
+
offset += encoderBytes.length;
|
|
59
|
+
bytes.set(sentinel, offset);
|
|
60
|
+
offset += sentinel.length;
|
|
61
|
+
return offset;
|
|
62
|
+
}) as Encoder<TFrom>['write'];
|
|
63
|
+
|
|
64
|
+
if (isFixedSize(encoder)) {
|
|
65
|
+
return createEncoder({ ...encoder, fixedSize: encoder.fixedSize + sentinel.length, write });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return createEncoder({
|
|
69
|
+
...encoder,
|
|
70
|
+
...(encoder.maxSize != null ? { maxSize: encoder.maxSize + sentinel.length } : {}),
|
|
71
|
+
getSizeFromValue: value => encoder.getSizeFromValue(value) + sentinel.length,
|
|
72
|
+
write,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Creates a decoder that continues reading until
|
|
78
|
+
* a given `Uint8Array` sentinel is found.
|
|
79
|
+
*
|
|
80
|
+
* See {@link addCodecSentinel} for more information.
|
|
81
|
+
*
|
|
82
|
+
* @typeParam TTo - The type of the decoded value.
|
|
83
|
+
*
|
|
84
|
+
* @see {@link addCodecSentinel}
|
|
85
|
+
*/
|
|
86
|
+
export function addDecoderSentinel<TTo>(
|
|
87
|
+
decoder: FixedSizeDecoder<TTo>,
|
|
88
|
+
sentinel: ReadonlyUint8Array,
|
|
89
|
+
): FixedSizeDecoder<TTo>;
|
|
90
|
+
export function addDecoderSentinel<TTo>(decoder: Decoder<TTo>, sentinel: ReadonlyUint8Array): VariableSizeDecoder<TTo>;
|
|
91
|
+
export function addDecoderSentinel<TTo>(decoder: Decoder<TTo>, sentinel: ReadonlyUint8Array): Decoder<TTo> {
|
|
92
|
+
const read = ((bytes, offset) => {
|
|
93
|
+
const candidateBytes = offset === 0 ? bytes : bytes.slice(offset);
|
|
94
|
+
const sentinelIndex = findSentinelIndex(candidateBytes, sentinel);
|
|
95
|
+
if (sentinelIndex === -1) {
|
|
96
|
+
throw new SolanaError(SOLANA_ERROR__CODECS__SENTINEL_MISSING_IN_DECODED_BYTES, {
|
|
97
|
+
decodedBytes: candidateBytes,
|
|
98
|
+
hexDecodedBytes: hexBytes(candidateBytes),
|
|
99
|
+
hexSentinel: hexBytes(sentinel),
|
|
100
|
+
sentinel,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
const preSentinelBytes = candidateBytes.slice(0, sentinelIndex);
|
|
104
|
+
// Here we exceptionally use the `decode` function instead of the `read`
|
|
105
|
+
// function to contain the content of the decoder within its own bounds
|
|
106
|
+
// and ensure that the sentinel is not part of the decoded value.
|
|
107
|
+
return [decoder.decode(preSentinelBytes), offset + preSentinelBytes.length + sentinel.length];
|
|
108
|
+
}) as Decoder<TTo>['read'];
|
|
109
|
+
|
|
110
|
+
if (isFixedSize(decoder)) {
|
|
111
|
+
return createDecoder({ ...decoder, fixedSize: decoder.fixedSize + sentinel.length, read });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return createDecoder({
|
|
115
|
+
...decoder,
|
|
116
|
+
...(decoder.maxSize != null ? { maxSize: decoder.maxSize + sentinel.length } : {}),
|
|
117
|
+
read,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Creates a Codec that writes a given `Uint8Array` sentinel after the encoded
|
|
123
|
+
* value and, when decoding, continues reading until the sentinel is found.
|
|
124
|
+
*
|
|
125
|
+
* This sets a limit on variable-size codecs and tells us when to stop decoding.
|
|
126
|
+
*
|
|
127
|
+
* @typeParam TFrom - The type of the value to encode.
|
|
128
|
+
* @typeParam TTo - The type of the decoded value.
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```ts
|
|
132
|
+
* const codec = addCodecSentinel(getUtf8Codec(), new Uint8Array([255, 255]));
|
|
133
|
+
* codec.encode('hello');
|
|
134
|
+
* // 0x68656c6c6fffff
|
|
135
|
+
* // | └-- Our sentinel.
|
|
136
|
+
* // └-- Our encoded string.
|
|
137
|
+
* ```
|
|
138
|
+
*
|
|
139
|
+
* @remarks
|
|
140
|
+
* Note that the sentinel _must not_ be present in the encoded data and
|
|
141
|
+
* _must_ be present in the decoded data for this to work.
|
|
142
|
+
* If this is not the case, dedicated errors will be thrown.
|
|
143
|
+
*
|
|
144
|
+
* ```ts
|
|
145
|
+
* const sentinel = new Uint8Array([108, 108]); // 'll'
|
|
146
|
+
* const codec = addCodecSentinel(getUtf8Codec(), sentinel);
|
|
147
|
+
*
|
|
148
|
+
* codec.encode('hello'); // Throws: sentinel is in encoded data.
|
|
149
|
+
* codec.decode(new Uint8Array([1, 2, 3])); // Throws: sentinel missing in decoded data.
|
|
150
|
+
* ```
|
|
151
|
+
*
|
|
152
|
+
* Separate {@link addEncoderSentinel} and {@link addDecoderSentinel} functions are also available.
|
|
153
|
+
*
|
|
154
|
+
* ```ts
|
|
155
|
+
* const bytes = addEncoderSentinel(getUtf8Encoder(), sentinel).encode('hello');
|
|
156
|
+
* const value = addDecoderSentinel(getUtf8Decoder(), sentinel).decode(bytes);
|
|
157
|
+
* ```
|
|
158
|
+
*
|
|
159
|
+
* @see {@link addEncoderSentinel}
|
|
160
|
+
* @see {@link addDecoderSentinel}
|
|
161
|
+
*/
|
|
162
|
+
export function addCodecSentinel<TFrom, TTo extends TFrom>(
|
|
163
|
+
codec: FixedSizeCodec<TFrom, TTo>,
|
|
164
|
+
sentinel: ReadonlyUint8Array,
|
|
165
|
+
): FixedSizeCodec<TFrom, TTo>;
|
|
166
|
+
export function addCodecSentinel<TFrom, TTo extends TFrom>(
|
|
167
|
+
codec: Codec<TFrom, TTo>,
|
|
168
|
+
sentinel: ReadonlyUint8Array,
|
|
169
|
+
): VariableSizeCodec<TFrom, TTo>;
|
|
170
|
+
export function addCodecSentinel<TFrom, TTo extends TFrom>(
|
|
171
|
+
codec: Codec<TFrom, TTo>,
|
|
172
|
+
sentinel: ReadonlyUint8Array,
|
|
173
|
+
): Codec<TFrom, TTo> {
|
|
174
|
+
return combineCodec(addEncoderSentinel(codec, sentinel), addDecoderSentinel(codec, sentinel));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function findSentinelIndex(bytes: ReadonlyUint8Array, sentinel: ReadonlyUint8Array) {
|
|
178
|
+
return bytes.findIndex((byte, index, arr) => {
|
|
179
|
+
if (sentinel.length === 1) return byte === sentinel[0];
|
|
180
|
+
return containsBytes(arr, sentinel, index);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function hexBytes(bytes: ReadonlyUint8Array): string {
|
|
185
|
+
return bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '');
|
|
186
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { assertByteArrayHasEnoughBytesForCodec } from './assertions';
|
|
2
|
+
import {
|
|
3
|
+
Codec,
|
|
4
|
+
createDecoder,
|
|
5
|
+
createEncoder,
|
|
6
|
+
Decoder,
|
|
7
|
+
Encoder,
|
|
8
|
+
FixedSizeCodec,
|
|
9
|
+
FixedSizeDecoder,
|
|
10
|
+
FixedSizeEncoder,
|
|
11
|
+
getEncodedSize,
|
|
12
|
+
isFixedSize,
|
|
13
|
+
VariableSizeCodec,
|
|
14
|
+
VariableSizeDecoder,
|
|
15
|
+
VariableSizeEncoder,
|
|
16
|
+
} from './codec';
|
|
17
|
+
import { combineCodec } from './combine-codec';
|
|
18
|
+
|
|
19
|
+
type NumberEncoder = Encoder<bigint | number> | Encoder<number>;
|
|
20
|
+
type FixedSizeNumberEncoder<TSize extends number = number> =
|
|
21
|
+
| FixedSizeEncoder<bigint | number, TSize>
|
|
22
|
+
| FixedSizeEncoder<number, TSize>;
|
|
23
|
+
type NumberDecoder = Decoder<bigint> | Decoder<number>;
|
|
24
|
+
type FixedSizeNumberDecoder<TSize extends number = number> =
|
|
25
|
+
| FixedSizeDecoder<bigint, TSize>
|
|
26
|
+
| FixedSizeDecoder<number, TSize>;
|
|
27
|
+
type NumberCodec = Codec<bigint | number, bigint> | Codec<number>;
|
|
28
|
+
type FixedSizeNumberCodec<TSize extends number = number> =
|
|
29
|
+
| FixedSizeCodec<bigint | number, bigint, TSize>
|
|
30
|
+
| FixedSizeCodec<number, number, TSize>;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Stores the size of the `encoder` in bytes as a prefix using the `prefix` encoder.
|
|
34
|
+
*
|
|
35
|
+
* See {@link addCodecSizePrefix} for more information.
|
|
36
|
+
*
|
|
37
|
+
* @typeParam TFrom - The type of the value to encode.
|
|
38
|
+
*
|
|
39
|
+
* @see {@link addCodecSizePrefix}
|
|
40
|
+
*/
|
|
41
|
+
export function addEncoderSizePrefix<TFrom>(
|
|
42
|
+
encoder: FixedSizeEncoder<TFrom>,
|
|
43
|
+
prefix: FixedSizeNumberEncoder,
|
|
44
|
+
): FixedSizeEncoder<TFrom>;
|
|
45
|
+
export function addEncoderSizePrefix<TFrom>(encoder: Encoder<TFrom>, prefix: NumberEncoder): VariableSizeEncoder<TFrom>;
|
|
46
|
+
export function addEncoderSizePrefix<TFrom>(encoder: Encoder<TFrom>, prefix: NumberEncoder): Encoder<TFrom> {
|
|
47
|
+
const write = ((value, bytes, offset) => {
|
|
48
|
+
// Here we exceptionally use the `encode` function instead of the `write`
|
|
49
|
+
// function to contain the content of the encoder within its own bounds.
|
|
50
|
+
const encoderBytes = encoder.encode(value);
|
|
51
|
+
offset = prefix.write(encoderBytes.length, bytes, offset);
|
|
52
|
+
bytes.set(encoderBytes, offset);
|
|
53
|
+
return offset + encoderBytes.length;
|
|
54
|
+
}) as Encoder<TFrom>['write'];
|
|
55
|
+
|
|
56
|
+
if (isFixedSize(prefix) && isFixedSize(encoder)) {
|
|
57
|
+
return createEncoder({ ...encoder, fixedSize: prefix.fixedSize + encoder.fixedSize, write });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const prefixMaxSize = isFixedSize(prefix) ? prefix.fixedSize : (prefix.maxSize ?? null);
|
|
61
|
+
const encoderMaxSize = isFixedSize(encoder) ? encoder.fixedSize : (encoder.maxSize ?? null);
|
|
62
|
+
const maxSize = prefixMaxSize !== null && encoderMaxSize !== null ? prefixMaxSize + encoderMaxSize : null;
|
|
63
|
+
|
|
64
|
+
return createEncoder({
|
|
65
|
+
...encoder,
|
|
66
|
+
...(maxSize !== null ? { maxSize } : {}),
|
|
67
|
+
getSizeFromValue: value => {
|
|
68
|
+
const encoderSize = getEncodedSize(value, encoder);
|
|
69
|
+
return getEncodedSize(encoderSize, prefix) + encoderSize;
|
|
70
|
+
},
|
|
71
|
+
write,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Bounds the size of the nested `decoder` by reading its encoded `prefix`.
|
|
77
|
+
*
|
|
78
|
+
* See {@link addCodecSizePrefix} for more information.
|
|
79
|
+
*
|
|
80
|
+
* @typeParam TTo - The type of the decoded value.
|
|
81
|
+
*
|
|
82
|
+
* @see {@link addCodecSizePrefix}
|
|
83
|
+
*/
|
|
84
|
+
export function addDecoderSizePrefix<TTo>(
|
|
85
|
+
decoder: FixedSizeDecoder<TTo>,
|
|
86
|
+
prefix: FixedSizeNumberDecoder,
|
|
87
|
+
): FixedSizeDecoder<TTo>;
|
|
88
|
+
export function addDecoderSizePrefix<TTo>(decoder: Decoder<TTo>, prefix: NumberDecoder): VariableSizeDecoder<TTo>;
|
|
89
|
+
export function addDecoderSizePrefix<TTo>(decoder: Decoder<TTo>, prefix: NumberDecoder): Decoder<TTo> {
|
|
90
|
+
const read = ((bytes, offset) => {
|
|
91
|
+
const [bigintSize, decoderOffset] = prefix.read(bytes, offset);
|
|
92
|
+
const size = Number(bigintSize);
|
|
93
|
+
offset = decoderOffset;
|
|
94
|
+
// Slice the byte array to the contained size if necessary.
|
|
95
|
+
if (offset > 0 || bytes.length > size) {
|
|
96
|
+
bytes = bytes.slice(offset, offset + size);
|
|
97
|
+
}
|
|
98
|
+
assertByteArrayHasEnoughBytesForCodec('addDecoderSizePrefix', size, bytes);
|
|
99
|
+
// Here we exceptionally use the `decode` function instead of the `read`
|
|
100
|
+
// function to contain the content of the decoder within its own bounds.
|
|
101
|
+
return [decoder.decode(bytes), offset + size];
|
|
102
|
+
}) as Decoder<TTo>['read'];
|
|
103
|
+
|
|
104
|
+
if (isFixedSize(prefix) && isFixedSize(decoder)) {
|
|
105
|
+
return createDecoder({ ...decoder, fixedSize: prefix.fixedSize + decoder.fixedSize, read });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const prefixMaxSize = isFixedSize(prefix) ? prefix.fixedSize : (prefix.maxSize ?? null);
|
|
109
|
+
const decoderMaxSize = isFixedSize(decoder) ? decoder.fixedSize : (decoder.maxSize ?? null);
|
|
110
|
+
const maxSize = prefixMaxSize !== null && decoderMaxSize !== null ? prefixMaxSize + decoderMaxSize : null;
|
|
111
|
+
return createDecoder({ ...decoder, ...(maxSize !== null ? { maxSize } : {}), read });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Stores the byte size of any given codec as an encoded number prefix.
|
|
116
|
+
*
|
|
117
|
+
* This sets a limit on variable-size codecs and tells us when to stop decoding.
|
|
118
|
+
* When encoding, the size of the encoded data is stored before the encoded data itself.
|
|
119
|
+
* When decoding, the size is read first to know how many bytes to read next.
|
|
120
|
+
*
|
|
121
|
+
* @typeParam TFrom - The type of the value to encode.
|
|
122
|
+
* @typeParam TTo - The type of the decoded value.
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* For example, say we want to bound a variable-size base-58 string using a `u32` size prefix.
|
|
126
|
+
* Here’s how you can use the `addCodecSizePrefix` function to achieve that.
|
|
127
|
+
*
|
|
128
|
+
* ```ts
|
|
129
|
+
* const getU32Base58Codec = () => addCodecSizePrefix(getBase58Codec(), getU32Codec());
|
|
130
|
+
*
|
|
131
|
+
* getU32Base58Codec().encode('hello world');
|
|
132
|
+
* // 0x0b00000068656c6c6f20776f726c64
|
|
133
|
+
* // | └-- Our encoded base-58 string.
|
|
134
|
+
* // └-- Our encoded u32 size prefix.
|
|
135
|
+
* ```
|
|
136
|
+
*
|
|
137
|
+
* @remarks
|
|
138
|
+
* Separate {@link addEncoderSizePrefix} and {@link addDecoderSizePrefix} functions are also available.
|
|
139
|
+
*
|
|
140
|
+
* ```ts
|
|
141
|
+
* const bytes = addEncoderSizePrefix(getBase58Encoder(), getU32Encoder()).encode('hello');
|
|
142
|
+
* const value = addDecoderSizePrefix(getBase58Decoder(), getU32Decoder()).decode(bytes);
|
|
143
|
+
* ```
|
|
144
|
+
*
|
|
145
|
+
* @see {@link addEncoderSizePrefix}
|
|
146
|
+
* @see {@link addDecoderSizePrefix}
|
|
147
|
+
*/
|
|
148
|
+
export function addCodecSizePrefix<TFrom, TTo extends TFrom>(
|
|
149
|
+
codec: FixedSizeCodec<TFrom, TTo>,
|
|
150
|
+
prefix: FixedSizeNumberCodec,
|
|
151
|
+
): FixedSizeCodec<TFrom, TTo>;
|
|
152
|
+
export function addCodecSizePrefix<TFrom, TTo extends TFrom>(
|
|
153
|
+
codec: Codec<TFrom, TTo>,
|
|
154
|
+
prefix: NumberCodec,
|
|
155
|
+
): VariableSizeCodec<TFrom, TTo>;
|
|
156
|
+
export function addCodecSizePrefix<TFrom, TTo extends TFrom>(
|
|
157
|
+
codec: Codec<TFrom, TTo>,
|
|
158
|
+
prefix: NumberCodec,
|
|
159
|
+
): Codec<TFrom, TTo> {
|
|
160
|
+
return combineCodec(addEncoderSizePrefix(codec, prefix), addDecoderSizePrefix(codec, prefix));
|
|
161
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ReadonlyUint8Array } from './readonly-uint8array';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Converts a `Uint8Array` to an `ArrayBuffer`. If the underlying buffer is a `SharedArrayBuffer`,
|
|
5
|
+
* it will be copied to a non-shared buffer, for safety.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* Source: https://stackoverflow.com/questions/37228285/uint8array-to-arraybuffer
|
|
9
|
+
*/
|
|
10
|
+
export function toArrayBuffer(bytes: ReadonlyUint8Array | Uint8Array, offset?: number, length?: number): ArrayBuffer {
|
|
11
|
+
const bytesOffset = bytes.byteOffset + (offset ?? 0);
|
|
12
|
+
const bytesLength = length ?? bytes.byteLength;
|
|
13
|
+
let buffer: ArrayBuffer;
|
|
14
|
+
if (typeof SharedArrayBuffer === 'undefined') {
|
|
15
|
+
buffer = bytes.buffer as ArrayBuffer;
|
|
16
|
+
} else if (bytes.buffer instanceof SharedArrayBuffer) {
|
|
17
|
+
buffer = new ArrayBuffer(bytes.length);
|
|
18
|
+
new Uint8Array(buffer).set(new Uint8Array(bytes));
|
|
19
|
+
} else {
|
|
20
|
+
buffer = bytes.buffer;
|
|
21
|
+
}
|
|
22
|
+
return (bytesOffset === 0 || bytesOffset === -bytes.byteLength) && bytesLength === bytes.byteLength
|
|
23
|
+
? buffer
|
|
24
|
+
: buffer.slice(bytesOffset, bytesOffset + bytesLength);
|
|
25
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SOLANA_ERROR__CODECS__CANNOT_DECODE_EMPTY_BYTE_ARRAY,
|
|
3
|
+
SOLANA_ERROR__CODECS__INVALID_BYTE_LENGTH,
|
|
4
|
+
SOLANA_ERROR__CODECS__OFFSET_OUT_OF_RANGE,
|
|
5
|
+
SolanaError,
|
|
6
|
+
} from '@solana/errors';
|
|
7
|
+
|
|
8
|
+
import { ReadonlyUint8Array } from './readonly-uint8array';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Asserts that a given byte array is not empty (after the optional provided offset).
|
|
12
|
+
*
|
|
13
|
+
* Returns void if the byte array is not empty but throws a {@link SolanaError} otherwise.
|
|
14
|
+
*
|
|
15
|
+
* @param codecDescription - A description of the codec used by the assertion error.
|
|
16
|
+
* @param bytes - The byte array to check.
|
|
17
|
+
* @param offset - The offset from which to start checking the byte array.
|
|
18
|
+
* If provided, the byte array is considered empty if it has no bytes after the offset.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* const bytes = new Uint8Array([0x01, 0x02, 0x03]);
|
|
23
|
+
* assertByteArrayIsNotEmptyForCodec('myCodec', bytes); // OK
|
|
24
|
+
* assertByteArrayIsNotEmptyForCodec('myCodec', bytes, 1); // OK
|
|
25
|
+
* assertByteArrayIsNotEmptyForCodec('myCodec', bytes, 3); // Throws
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export function assertByteArrayIsNotEmptyForCodec(
|
|
29
|
+
codecDescription: string,
|
|
30
|
+
bytes: ReadonlyUint8Array | Uint8Array,
|
|
31
|
+
offset = 0,
|
|
32
|
+
) {
|
|
33
|
+
if (bytes.length - offset <= 0) {
|
|
34
|
+
throw new SolanaError(SOLANA_ERROR__CODECS__CANNOT_DECODE_EMPTY_BYTE_ARRAY, {
|
|
35
|
+
codecDescription,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Asserts that a given byte array has enough bytes to decode
|
|
42
|
+
* (after the optional provided offset).
|
|
43
|
+
*
|
|
44
|
+
* Returns void if the byte array has at least the expected number
|
|
45
|
+
* of bytes but throws a {@link SolanaError} otherwise.
|
|
46
|
+
*
|
|
47
|
+
* @param codecDescription - A description of the codec used by the assertion error.
|
|
48
|
+
* @param expected - The minimum number of bytes expected in the byte array.
|
|
49
|
+
* @param bytes - The byte array to check.
|
|
50
|
+
* @param offset - The offset from which to start checking the byte array.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* const bytes = new Uint8Array([0x01, 0x02, 0x03]);
|
|
55
|
+
* assertByteArrayHasEnoughBytesForCodec('myCodec', 3, bytes); // OK
|
|
56
|
+
* assertByteArrayHasEnoughBytesForCodec('myCodec', 4, bytes); // Throws
|
|
57
|
+
* assertByteArrayHasEnoughBytesForCodec('myCodec', 2, bytes, 1); // OK
|
|
58
|
+
* assertByteArrayHasEnoughBytesForCodec('myCodec', 3, bytes, 1); // Throws
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export function assertByteArrayHasEnoughBytesForCodec(
|
|
62
|
+
codecDescription: string,
|
|
63
|
+
expected: number,
|
|
64
|
+
bytes: ReadonlyUint8Array | Uint8Array,
|
|
65
|
+
offset = 0,
|
|
66
|
+
) {
|
|
67
|
+
const bytesLength = bytes.length - offset;
|
|
68
|
+
if (bytesLength < expected) {
|
|
69
|
+
throw new SolanaError(SOLANA_ERROR__CODECS__INVALID_BYTE_LENGTH, {
|
|
70
|
+
bytesLength,
|
|
71
|
+
codecDescription,
|
|
72
|
+
expected,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Asserts that a given offset is within the byte array bounds.
|
|
79
|
+
* This range is between 0 and the byte array length and is inclusive.
|
|
80
|
+
* An offset equals to the byte array length is considered a valid offset
|
|
81
|
+
* as it allows the post-offset of codecs to signal the end of the byte array.
|
|
82
|
+
*
|
|
83
|
+
* @param codecDescription - A description of the codec used by the assertion error.
|
|
84
|
+
* @param offset - The offset to check.
|
|
85
|
+
* @param bytesLength - The length of the byte array from which the offset should be within bounds.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```ts
|
|
89
|
+
* const bytes = new Uint8Array([0x01, 0x02, 0x03]);
|
|
90
|
+
* assertByteArrayOffsetIsNotOutOfRange('myCodec', 0, bytes.length); // OK
|
|
91
|
+
* assertByteArrayOffsetIsNotOutOfRange('myCodec', 3, bytes.length); // OK
|
|
92
|
+
* assertByteArrayOffsetIsNotOutOfRange('myCodec', 4, bytes.length); // Throws
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export function assertByteArrayOffsetIsNotOutOfRange(codecDescription: string, offset: number, bytesLength: number) {
|
|
96
|
+
if (offset < 0 || offset > bytesLength) {
|
|
97
|
+
throw new SolanaError(SOLANA_ERROR__CODECS__OFFSET_OUT_OF_RANGE, {
|
|
98
|
+
bytesLength,
|
|
99
|
+
codecDescription,
|
|
100
|
+
offset,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
package/src/bytes.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { ReadonlyUint8Array } from './readonly-uint8array';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Concatenates an array of `Uint8Array`s into a single `Uint8Array`.
|
|
5
|
+
* Reuses the original byte array when applicable.
|
|
6
|
+
*
|
|
7
|
+
* @param byteArrays - The array of byte arrays to concatenate.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* const bytes1 = new Uint8Array([0x01, 0x02]);
|
|
12
|
+
* const bytes2 = new Uint8Array([]);
|
|
13
|
+
* const bytes3 = new Uint8Array([0x03, 0x04]);
|
|
14
|
+
* const bytes = mergeBytes([bytes1, bytes2, bytes3]);
|
|
15
|
+
* // ^ [0x01, 0x02, 0x03, 0x04]
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export const mergeBytes = (byteArrays: Uint8Array[]): Uint8Array => {
|
|
19
|
+
const nonEmptyByteArrays = byteArrays.filter(arr => arr.length);
|
|
20
|
+
if (nonEmptyByteArrays.length === 0) {
|
|
21
|
+
return byteArrays.length ? byteArrays[0] : new Uint8Array();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (nonEmptyByteArrays.length === 1) {
|
|
25
|
+
return nonEmptyByteArrays[0];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const totalLength = nonEmptyByteArrays.reduce((total, arr) => total + arr.length, 0);
|
|
29
|
+
const result = new Uint8Array(totalLength);
|
|
30
|
+
let offset = 0;
|
|
31
|
+
nonEmptyByteArrays.forEach(arr => {
|
|
32
|
+
result.set(arr, offset);
|
|
33
|
+
offset += arr.length;
|
|
34
|
+
});
|
|
35
|
+
return result;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Pads a `Uint8Array` with zeroes to the specified length.
|
|
40
|
+
* If the array is longer than the specified length, it is returned as-is.
|
|
41
|
+
*
|
|
42
|
+
* @param bytes - The byte array to pad.
|
|
43
|
+
* @param length - The desired length of the byte array.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* Adds zeroes to the end of the byte array to reach the desired length.
|
|
47
|
+
* ```ts
|
|
48
|
+
* const bytes = new Uint8Array([0x01, 0x02]);
|
|
49
|
+
* const paddedBytes = padBytes(bytes, 4);
|
|
50
|
+
* // ^ [0x01, 0x02, 0x00, 0x00]
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* Returns the original byte array if it is already at the desired length.
|
|
55
|
+
* ```ts
|
|
56
|
+
* const bytes = new Uint8Array([0x01, 0x02]);
|
|
57
|
+
* const paddedBytes = padBytes(bytes, 2);
|
|
58
|
+
* // bytes === paddedBytes
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export function padBytes(bytes: Uint8Array, length: number): Uint8Array;
|
|
62
|
+
export function padBytes(bytes: ReadonlyUint8Array, length: number): ReadonlyUint8Array;
|
|
63
|
+
export function padBytes(bytes: ReadonlyUint8Array, length: number): ReadonlyUint8Array {
|
|
64
|
+
if (bytes.length >= length) return bytes;
|
|
65
|
+
const paddedBytes = new Uint8Array(length).fill(0);
|
|
66
|
+
paddedBytes.set(bytes);
|
|
67
|
+
return paddedBytes;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Fixes a `Uint8Array` to the specified length.
|
|
72
|
+
* If the array is longer than the specified length, it is truncated.
|
|
73
|
+
* If the array is shorter than the specified length, it is padded with zeroes.
|
|
74
|
+
*
|
|
75
|
+
* @param bytes - The byte array to truncate or pad.
|
|
76
|
+
* @param length - The desired length of the byte array.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* Truncates the byte array to the desired length.
|
|
80
|
+
* ```ts
|
|
81
|
+
* const bytes = new Uint8Array([0x01, 0x02, 0x03, 0x04]);
|
|
82
|
+
* const fixedBytes = fixBytes(bytes, 2);
|
|
83
|
+
* // ^ [0x01, 0x02]
|
|
84
|
+
* ```
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* Adds zeroes to the end of the byte array to reach the desired length.
|
|
88
|
+
* ```ts
|
|
89
|
+
* const bytes = new Uint8Array([0x01, 0x02]);
|
|
90
|
+
* const fixedBytes = fixBytes(bytes, 4);
|
|
91
|
+
* // ^ [0x01, 0x02, 0x00, 0x00]
|
|
92
|
+
* ```
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* Returns the original byte array if it is already at the desired length.
|
|
96
|
+
* ```ts
|
|
97
|
+
* const bytes = new Uint8Array([0x01, 0x02]);
|
|
98
|
+
* const fixedBytes = fixBytes(bytes, 2);
|
|
99
|
+
* // bytes === fixedBytes
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
export const fixBytes = (bytes: ReadonlyUint8Array | Uint8Array, length: number): ReadonlyUint8Array | Uint8Array =>
|
|
103
|
+
padBytes(bytes.length <= length ? bytes : bytes.slice(0, length), length);
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Returns true if and only if the provided `data` byte array contains
|
|
107
|
+
* the provided `bytes` byte array at the specified `offset`.
|
|
108
|
+
*
|
|
109
|
+
* @param data - The byte sequence to search for.
|
|
110
|
+
* @param bytes - The byte array in which to search for `data`.
|
|
111
|
+
* @param offset - The position in `bytes` where the search begins.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```ts
|
|
115
|
+
* const bytes = new Uint8Array([0x01, 0x02, 0x03, 0x04]);
|
|
116
|
+
* const data = new Uint8Array([0x02, 0x03]);
|
|
117
|
+
* containsBytes(bytes, data, 1); // true
|
|
118
|
+
* containsBytes(bytes, data, 2); // false
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
export function containsBytes(
|
|
122
|
+
data: ReadonlyUint8Array | Uint8Array,
|
|
123
|
+
bytes: ReadonlyUint8Array | Uint8Array,
|
|
124
|
+
offset: number,
|
|
125
|
+
): boolean {
|
|
126
|
+
const slice = offset === 0 && data.length === bytes.length ? data : data.slice(offset, offset + bytes.length);
|
|
127
|
+
return bytesEqual(slice, bytes);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Returns true if and only if the provided `bytes1` and `bytes2` byte arrays are equal.
|
|
132
|
+
*
|
|
133
|
+
* @param bytes1 - The first byte array to compare.
|
|
134
|
+
* @param bytes2 - The second byte array to compare.
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* ```ts
|
|
138
|
+
* const bytes1 = new Uint8Array([0x01, 0x02, 0x03, 0x04]);
|
|
139
|
+
* const bytes2 = new Uint8Array([0x01, 0x02, 0x03, 0x04]);
|
|
140
|
+
* bytesEqual(bytes1, bytes2); // true
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
export function bytesEqual(bytes1: ReadonlyUint8Array | Uint8Array, bytes2: ReadonlyUint8Array | Uint8Array): boolean {
|
|
144
|
+
return bytes1.length === bytes2.length && bytes1.every((value, index) => value === bytes2[index]);
|
|
145
|
+
}
|