@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solana/codecs-core",
3
- "version": "6.3.1",
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.1"
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
+ }