@solana/codecs-data-structures 8.3.0 → 8.4.0-canary-20260918134912

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/README.md CHANGED
@@ -33,11 +33,12 @@ getArrayCodec(getU8Codec()).encode([1, 2, 3]);
33
33
  // └-- 4-byte prefix telling us to read 3 items.
34
34
  ```
35
35
 
36
- However, you may use the `size` option to configure this behaviour. It can be one of the following three strategies:
36
+ However, you may use the `size` option to configure this behaviour. It can be one of the following four strategies:
37
37
 
38
38
  - `Codec<number>`: When a number codec is provided, that codec will be used to encode and decode the size prefix.
39
39
  - `number`: When a number is provided, the codec will expect a fixed number of items in the array. An error will be thrown when trying to encode an array of a different length.
40
- - `"remainder"`: When the string `"remainder"` is passed as a size, the codec will use the remainder of the bytes to encode/decode its items. This means the size is not stored or known in advance but simply inferred from the rest of the buffer. For instance, if we have an array of `u16` numbers and 10 bytes remaining, we know there are 5 items in this array.
40
+ - `"remainder"`: When the string `"remainder"` is passed as a size, the codec will use the remainder of the bytes to encode/decode its items. This means the size is not stored or known in advance but simply inferred from the rest of the buffer. For instance, if we have an array of `u16` numbers and 10 bytes remaining, we know there are 5 items in this array.
41
+ - A sentinel object: When an object of the form `{ __kind: 'sentinel', sentinel, strategy? }` is provided, the array ends as soon as the bytes at the next item position match the given `sentinel`. The sentinel is only compared at item boundaries and is never searched for within an item, so — unlike `addCodecSentinel` — its bytes may occur _inside_ an item without terminating the array. The optional `strategy` (`"required"` by default, or `"optional"` / `"omitted"`) controls whether the sentinel is written when encoding and required when decoding.
41
42
 
42
43
  ```ts
43
44
  getArrayCodec(getU8Codec(), { size: getU16Codec() }).encode([1, 2, 3]);
@@ -52,8 +53,24 @@ getArrayCodec(getU8Codec(), { size: 3 }).encode([1, 2, 3]);
52
53
  getArrayCodec(getU8Codec(), { size: 'remainder' }).encode([1, 2, 3]);
53
54
  // 0x010203
54
55
  // └-- 3 items of 1 byte each. The size is inferred from the remainder of the bytes.
56
+
57
+ getArrayCodec(getU8Codec(), { size: { __kind: 'sentinel', sentinel: new Uint8Array([0]) } }).encode([1, 2, 3]);
58
+ // 0x01020300
59
+ // | └-- The sentinel that marks the end of the array.
60
+ // └-- 3 items of 1 byte each.
55
61
  ```
56
62
 
63
+ The sentinel strategy accepts three variants:
64
+
65
+ - `"required"` (default): the sentinel is written after the last item and must be present when decoding. Reaching the end of the byte array without it throws.
66
+ - `"optional"`: the sentinel is written after the last item, but decoding also stops at the end of the byte array. Use this to tolerate tightly sized or legacy data that lacks the sentinel.
67
+ - `"omitted"`: the sentinel is never written; decoding stops at the end of the byte array (and consumes the sentinel if one happens to be present). Only meaningful when the collection is followed by unused space or the end of the byte array.
68
+
69
+ Because the sentinel is only compared at the start of the next item slot, two invariants must hold for the array to round-trip correctly. The codec does **not** enforce them; it is your responsibility to guarantee them:
70
+
71
+ 1. **No item may _begin_ with the sentinel's bytes.** A valid item that starts with the sentinel is indistinguishable from the terminator, so decoding would stop early at that item. The sentinel may still appear _inside_ an item, just never at its start. For example, a single `0xff` byte is a poor sentinel for a list of public keys, since roughly one key in 256 starts with `0xff`; a sentinel as wide as an item (such as the all-zero public key) avoids this because only that exact key can match the terminator.
72
+ 2. **Under `"optional"` and `"omitted"`, the sentinel must be no wider than the smallest possible item.** Otherwise a trailing region shorter than the sentinel but large enough to hold a valid item would be skipped, since decoding stops as soon as fewer bytes than the sentinel remain. This cannot arise under `"required"` because a terminator is always written.
73
+
57
74
  When the size is stored as a prefix, decoding an exhausted byte array yields an empty array instead of failing. This allows arrays to be appended to existing data layouts without breaking the decoding of older data. Use the `requireSizePrefix` option to throw instead.
58
75
 
59
76
  ```ts
@@ -83,6 +100,7 @@ Just like the array codec, it uses a `u32` size prefix by default but can be con
83
100
  getSetCodec(getU8Codec(), { size: getU16Codec() }).encode(new Set([1, 2, 3]));
84
101
  getSetCodec(getU8Codec(), { size: 3 }).encode(new Set([1, 2, 3]));
85
102
  getSetCodec(getU8Codec(), { size: 'remainder' }).encode(new Set([1, 2, 3]));
103
+ getSetCodec(getU8Codec(), { size: { __kind: 'sentinel', sentinel: new Uint8Array([0]) } }).encode(new Set([1, 2, 3]));
86
104
  ```
87
105
 
88
106
  Separate `getSetEncoder` and `getSetDecoder` functions are also available.
@@ -127,6 +145,7 @@ However, it can be configured using the `size` and `requireSizePrefix` options.
127
145
  getMapCodec(keyCodec, valueCodec, { size: getU16Codec() }).encode(myMap);
128
146
  getMapCodec(keyCodec, valueCodec, { size: 3 }).encode(myMap);
129
147
  getMapCodec(keyCodec, valueCodec, { size: 'remainder' }).encode(myMap);
148
+ getMapCodec(keyCodec, valueCodec, { size: { __kind: 'sentinel', sentinel: new Uint8Array([0]) } }).encode(myMap);
130
149
  ```
131
150
 
132
151
  Separate `getMapEncoder` and `getMapDecoder` functions are also available.
@@ -33,13 +33,15 @@ function getMaxSize(codec) {
33
33
  // src/array.ts
34
34
  function getArrayEncoder(item, config = {}) {
35
35
  const size = config.size ?? codecsNumbers.getU32Encoder();
36
+ assertValidSize(size);
36
37
  const fixedSize = computeArrayLikeCodecSize(size, getFixedSize(item));
37
38
  const maxSize = computeArrayLikeCodecSize(size, getMaxSize(item)) ?? void 0;
38
39
  return codecsCore.createEncoder({
39
40
  ...fixedSize !== null ? { fixedSize } : {
40
41
  getSizeFromValue: (array) => {
41
- const prefixSize = typeof size === "object" ? codecsCore.getEncodedSize(array.length, size) : 0;
42
- return prefixSize + [...array].reduce((all, value) => all + codecsCore.getEncodedSize(value, item), 0);
42
+ const prefixSize = isPrefixSize(size) ? codecsCore.getEncodedSize(array.length, size) : 0;
43
+ const suffixSize = isSentinelSize(size) && size.strategy !== "omitted" ? size.sentinel.length : 0;
44
+ return prefixSize + suffixSize + [...array].reduce((all, value) => all + codecsCore.getEncodedSize(value, item), 0);
43
45
  },
44
46
  maxSize
45
47
  },
@@ -47,18 +49,23 @@ function getArrayEncoder(item, config = {}) {
47
49
  if (typeof size === "number") {
48
50
  assertValidNumberOfItemsForCodec(config.description ?? "array", size, array.length);
49
51
  }
50
- if (typeof size === "object") {
52
+ if (isPrefixSize(size)) {
51
53
  offset = size.write(array.length, bytes, offset);
52
54
  }
53
55
  array.forEach((value) => {
54
56
  offset = item.write(value, bytes, offset);
55
57
  });
58
+ if (isSentinelSize(size) && size.strategy !== "omitted") {
59
+ bytes.set(size.sentinel, offset);
60
+ offset += size.sentinel.length;
61
+ }
56
62
  return offset;
57
63
  }
58
64
  });
59
65
  }
60
66
  function getArrayDecoder(item, config = {}) {
61
67
  const size = config.size ?? codecsNumbers.getU32Decoder();
68
+ assertValidSize(size);
62
69
  const itemSize = getFixedSize(item);
63
70
  const fixedSize = computeArrayLikeCodecSize(size, itemSize);
64
71
  const maxSize = computeArrayLikeCodecSize(size, getMaxSize(item)) ?? void 0;
@@ -66,7 +73,7 @@ function getArrayDecoder(item, config = {}) {
66
73
  ...fixedSize !== null ? { fixedSize } : { maxSize },
67
74
  read: (bytes, offset) => {
68
75
  const array = [];
69
- if (typeof size === "object" && !config.requireSizePrefix && offset >= bytes.length) {
76
+ if (isPrefixSize(size) && !config.requireSizePrefix && offset >= bytes.length) {
70
77
  return [array, offset];
71
78
  }
72
79
  if (size === "remainder") {
@@ -77,6 +84,29 @@ function getArrayDecoder(item, config = {}) {
77
84
  }
78
85
  return [array, offset];
79
86
  }
87
+ if (isSentinelSize(size)) {
88
+ const { sentinel, strategy = "required" } = size;
89
+ while (true) {
90
+ if (offset + sentinel.length > bytes.length) {
91
+ if (strategy === "required") {
92
+ throw new errors.SolanaError(errors.SOLANA_ERROR__CODECS__SENTINEL_MISSING_AT_END_OF_BYTES, {
93
+ codecDescription: config.description ?? "array",
94
+ hexSentinel: hexBytes(sentinel),
95
+ sentinel
96
+ });
97
+ }
98
+ break;
99
+ }
100
+ if (codecsCore.containsBytes(bytes, sentinel, offset)) {
101
+ offset += sentinel.length;
102
+ break;
103
+ }
104
+ const [value, newOffset2] = item.read(bytes, offset);
105
+ offset = newOffset2;
106
+ array.push(value);
107
+ }
108
+ return [array, offset];
109
+ }
80
110
  const [resolvedSize, newOffset] = typeof size === "number" ? [size, offset] : size.read(bytes, offset);
81
111
  offset = newOffset;
82
112
  for (let i = 0; i < resolvedSize; i += 1) {
@@ -96,6 +126,20 @@ function computeArrayLikeCodecSize(size, itemSize) {
96
126
  if (size === 0) return 0;
97
127
  return itemSize === null ? null : itemSize * size;
98
128
  }
129
+ function isPrefixSize(size) {
130
+ return typeof size === "object" && size !== null && !isSentinelSize(size);
131
+ }
132
+ function isSentinelSize(size) {
133
+ return typeof size === "object" && size !== null && "__kind" in size && size.__kind === "sentinel";
134
+ }
135
+ function assertValidSize(size) {
136
+ if (isSentinelSize(size) && size.sentinel.length === 0) {
137
+ throw new errors.SolanaError(errors.SOLANA_ERROR__CODECS__SENTINEL_MUST_NOT_BE_EMPTY);
138
+ }
139
+ }
140
+ function hexBytes(bytes) {
141
+ return bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, "0"), "");
142
+ }
99
143
  function getBitArrayEncoder(size, config = {}) {
100
144
  const parsedConfig = typeof config === "boolean" ? { backward: config } : config;
101
145
  const backward = parsedConfig.backward ?? false;