@sofa-buffers/corelib 0.8.0 → 0.10.0
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/CHANGELOG.md +54 -1
- package/README.md +55 -0
- package/dist/index.cjs +262 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +204 -26
- package/dist/index.d.ts +204 -26
- package/dist/index.global.js +262 -47
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +262 -47
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,10 +6,63 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
While the version is below `1.0.0`, breaking changes bump the **minor** version.
|
|
8
8
|
|
|
9
|
-
## [
|
|
9
|
+
## [0.10.0] - 2026-08-01
|
|
10
|
+
|
|
11
|
+
> The breaking entries below make the next release a **minor** bump (the first
|
|
12
|
+
> since `0.2.0`), per the pre-`1.0.0` rule above — never a patch. The published
|
|
13
|
+
> package is `@sofa-buffers/corelib`; the git tag is the source of truth for the
|
|
14
|
+
> version number, and `package.json` stays at `0.0.0-dev`.
|
|
10
15
|
|
|
11
16
|
### Changed
|
|
12
17
|
|
|
18
|
+
- **BREAKING (encode API) — an all-default sequence is now *omitted*, not framed
|
|
19
|
+
empty (MESSAGE_SPEC §2, CORELIB_PLAN §6).** A sequence-typed **field** whose
|
|
20
|
+
value equals its declared default carries no information, so it no longer
|
|
21
|
+
reaches the wire at all, where it previously appeared as the two-byte empty
|
|
22
|
+
frame `0E 07`. An all-default message is now the **empty byte string**. A
|
|
23
|
+
wrapper-array **element** is the exception and keeps its frame: element
|
|
24
|
+
presence is what carries a dynamic array's length (§5.1), so dropping one
|
|
25
|
+
would change the decoded value, not just the bytes.
|
|
26
|
+
|
|
27
|
+
Deciding this without buffering the sub-message means the sequence header has
|
|
28
|
+
to be held back until the sequence proves it has content, which changes the
|
|
29
|
+
encoder's public sequence API:
|
|
30
|
+
|
|
31
|
+
| before | after |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `writeSequenceBegin(id)` — **removed** | `writeSequenceBeginLazy(id)` — opens the scope and holds the header back; writes no byte |
|
|
34
|
+
| `writeSequenceEnd()` | `writeSequenceEnd()` — drops the frame (header *and* end marker) if the sequence got no content |
|
|
35
|
+
| — | `writeSequenceEndKeep()` — new; emits the held-back headers plus the end marker, so a contentless sequence still reaches the wire as `begin` + `end` |
|
|
36
|
+
|
|
37
|
+
**Migration.** Replace every `writeSequenceBegin` with
|
|
38
|
+
`writeSequenceBeginLazy`. Then pick the closer *statically*, by the position in
|
|
39
|
+
the schema — it is a property of the position, not of the value:
|
|
40
|
+
`writeSequenceEnd` for a `struct`/`union` field and for an array-field wrapper;
|
|
41
|
+
`writeSequenceEndKeep` for a wrapper-array element, and for an array field
|
|
42
|
+
already known to differ from a **non-empty** declared default. When in doubt
|
|
43
|
+
`writeSequenceEndKeep` is the safe choice: the failure directions are not
|
|
44
|
+
symmetric — a needless `endKeep` costs one non-canonical empty frame that a
|
|
45
|
+
decoder normalizes away, while a wrong `end` silently changes an array's
|
|
46
|
+
length. Code that transcodes or replays raw bytes (rather than encoding a
|
|
47
|
+
schema value) wants `writeSequenceEndKeep` throughout, so its output reproduces
|
|
48
|
+
its input frame for frame.
|
|
49
|
+
|
|
50
|
+
**Decoding is unaffected**, in both directions: an empty frame remains valid
|
|
51
|
+
input that the message layer normalizes to the default, and an omitted
|
|
52
|
+
sequence field was already reconstructed from the schema default. Old and new
|
|
53
|
+
encoders therefore interoperate; they disagree only about which encoding is
|
|
54
|
+
canonical. Every non-sequence byte is unchanged — the shared
|
|
55
|
+
`assets/test_vectors.json` is re-synced and every `serialized` hex is
|
|
56
|
+
byte-identical; the vectors' separate `serialized_sparse` column is the new
|
|
57
|
+
canonical form, and is exercised by the generator's conformance drivers (a
|
|
58
|
+
corelib has no message layer and cannot produce it).
|
|
59
|
+
|
|
60
|
+
The hold-back run is bounded only by `MAX_DEPTH`: this port can allocate, so
|
|
61
|
+
it holds back to the full nesting depth and is canonical at every depth
|
|
62
|
+
(CORELIB_PLAN §6, "How deep the hold-back reaches"). Held-back ids are encoder
|
|
63
|
+
state and never buffer content, so a flush cannot split a run and a buffer
|
|
64
|
+
smaller than the message still produces the one-shot bytes.
|
|
65
|
+
|
|
13
66
|
- **Strict UTF-8 for `string` fields (corelib-ts#85, MESSAGE_SPEC §8,
|
|
14
67
|
CORELIB_PLAN §6.4).** JavaScript strings are a Unicode string type, so the
|
|
15
68
|
corelib transcodes `string` payloads at the boundary and is now **always
|
package/README.md
CHANGED
|
@@ -104,6 +104,61 @@ for (let i = 0; i < 1000; i++) os.writeUnsigned(i, BigInt(i));
|
|
|
104
104
|
os.flush(); // push the tail
|
|
105
105
|
```
|
|
106
106
|
|
|
107
|
+
### Nested sequences
|
|
108
|
+
|
|
109
|
+
A nested message is a *sequence*: a fresh id scope between a begin header and the
|
|
110
|
+
`0x07` end marker. MESSAGE_SPEC §2 omits a sequence-typed **field** whose value
|
|
111
|
+
equals its declared default, so the encoder holds the begin header back until the
|
|
112
|
+
sequence proves it has content — no buffering of the sub-message, and nothing to
|
|
113
|
+
compare byte images against:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
const os = new OStream();
|
|
117
|
+
os.writeUnsigned(1, 42);
|
|
118
|
+
os.writeSequenceBeginLazy(2); // a nested field...
|
|
119
|
+
os.writeSequenceEnd(); // ...that got no content: header and end both vanish
|
|
120
|
+
os.writeSequenceBeginLazy(3);
|
|
121
|
+
os.writeString(1, "hi"); // content — commits the held-back header first
|
|
122
|
+
os.writeSequenceEnd();
|
|
123
|
+
os.bytes(); // 08 2a 1e 0a 12 68 69 07 (field 2 is not on the wire)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Which closer to use is decided **statically**, by the position in the schema, not
|
|
127
|
+
by the value:
|
|
128
|
+
|
|
129
|
+
| position | closer |
|
|
130
|
+
|---|---|
|
|
131
|
+
| `struct` / `union` field, array-field wrapper | `writeSequenceEnd()` — drops a contentless frame |
|
|
132
|
+
| wrapper-array **element**, or an array field differing from a non-empty declared default | `writeSequenceEndKeep()` — always emits `begin` + `end` |
|
|
133
|
+
|
|
134
|
+
An element keeps its frame because element presence is what carries a dynamic
|
|
135
|
+
array's length (highest present id + 1, §5.1); dropping an all-default element
|
|
136
|
+
would change the decoded length, not just the bytes. The failure directions are
|
|
137
|
+
not symmetric, so `writeSequenceEndKeep()` is the safe choice when in doubt: a
|
|
138
|
+
needless one costs a non-canonical empty frame that a decoder normalizes away,
|
|
139
|
+
while a wrong `writeSequenceEnd()` shortens an array. Raw transcoding — replaying
|
|
140
|
+
bytes rather than encoding a schema value — should use `writeSequenceEndKeep()`
|
|
141
|
+
throughout, so the output reproduces the input frame for frame.
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
const os = new OStream();
|
|
145
|
+
os.writeSequenceBeginLazy(4); // the wrapper array
|
|
146
|
+
os.writeSequenceBeginLazy(0); // element 0 — has content
|
|
147
|
+
os.writeUnsigned(0, 7);
|
|
148
|
+
os.writeSequenceEndKeep();
|
|
149
|
+
os.writeSequenceBeginLazy(1); // element 1 — all-default, but still present
|
|
150
|
+
os.writeSequenceEndKeep(); // ...so its frame stays: the array has length 2
|
|
151
|
+
os.writeSequenceEnd();
|
|
152
|
+
os.bytes(); // 26 06 00 07 07 0e 07 07
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Decoding is unaffected by the distinction: an empty frame is valid input that the
|
|
156
|
+
message layer normalizes to the default, and an absent sequence field is
|
|
157
|
+
reconstructed from the schema default. Nesting is capped at `MAX_DEPTH` (255) on
|
|
158
|
+
both sides; the encoder holds headers back to that full depth, so its output is
|
|
159
|
+
canonical however deep a message nests. A held-back header is encoder state, never
|
|
160
|
+
buffer content, so streaming through a small buffer produces the same bytes.
|
|
161
|
+
|
|
107
162
|
### Deserialize
|
|
108
163
|
|
|
109
164
|
`decode()` walks a whole buffer and calls one optional `Visitor` method per field;
|
package/dist/index.cjs
CHANGED
|
@@ -97,8 +97,6 @@ var DecodeStatus = {
|
|
|
97
97
|
var SofabErrorCode = {
|
|
98
98
|
/** A caller argument was invalid (e.g. id out of range, empty array). */
|
|
99
99
|
Argument: "ARGUMENT",
|
|
100
|
-
/** The API was used incorrectly (e.g. unbalanced sequence end). */
|
|
101
|
-
Usage: "USAGE",
|
|
102
100
|
/** The output buffer is full and no flush sink was provided. */
|
|
103
101
|
BufferFull: "BUFFER_FULL",
|
|
104
102
|
/** The input being decoded is malformed regardless of what follows (`INVALID`). */
|
|
@@ -133,9 +131,6 @@ var SofabError = class _SofabError extends Error {
|
|
|
133
131
|
function argumentError(message) {
|
|
134
132
|
return new SofabError(SofabErrorCode.Argument, message);
|
|
135
133
|
}
|
|
136
|
-
function usageError(message) {
|
|
137
|
-
return new SofabError(SofabErrorCode.Usage, message);
|
|
138
|
-
}
|
|
139
134
|
function bufferFullError(message) {
|
|
140
135
|
return new SofabError(SofabErrorCode.BufferFull, message);
|
|
141
136
|
}
|
|
@@ -449,6 +444,26 @@ var SIGNED_FAST_MAX2 = 4503599627370496;
|
|
|
449
444
|
var OStream = class {
|
|
450
445
|
constructor(buffer, offset = 0, flush) {
|
|
451
446
|
this.depth = 0;
|
|
447
|
+
/**
|
|
448
|
+
* Ids of the innermost open sequences whose header has not been written yet
|
|
449
|
+
* (MESSAGE_SPEC §2 lazy framing, {@link OStream.writeSequenceBeginLazy}).
|
|
450
|
+
* Always a contiguous suffix of the open sequences: writing any field commits
|
|
451
|
+
* the whole run at once, so {@link OStream.writeSequenceEnd} can drop the
|
|
452
|
+
* innermost one by dropping the last entry. Held-back ids are encoder state,
|
|
453
|
+
* never buffer content, so a flush can never split a run.
|
|
454
|
+
*
|
|
455
|
+
* Storage plus an explicit count rather than `push`/`pop`, so the slots are
|
|
456
|
+
* reused across messages (no allocation on a pooled encoder) and so
|
|
457
|
+
* {@link OStream.commitPending} can zero the count *before* it writes.
|
|
458
|
+
*
|
|
459
|
+
* The array grows on demand and is bounded only by `MAX_DEPTH` — there is no
|
|
460
|
+
* fixed hold-back window and hence no eager-framing fallback, which is what
|
|
461
|
+
* CORELIB_PLAN §6 ("How deep the hold-back reaches") demands of an
|
|
462
|
+
* implementation that can allocate: canonical output at *every* depth.
|
|
463
|
+
*/
|
|
464
|
+
this.pending = [];
|
|
465
|
+
/** Valid entries in {@link OStream.pending}. */
|
|
466
|
+
this.nPending = 0;
|
|
452
467
|
this.kernel = getKernel();
|
|
453
468
|
if (buffer === void 0) {
|
|
454
469
|
this.buf = new Uint8Array(DEFAULT_CAPACITY);
|
|
@@ -510,6 +525,7 @@ var OStream = class {
|
|
|
510
525
|
reset() {
|
|
511
526
|
this.pos = this.start;
|
|
512
527
|
this.depth = 0;
|
|
528
|
+
this.nPending = 0;
|
|
513
529
|
}
|
|
514
530
|
// --- scalars ------------------------------------------------------------
|
|
515
531
|
/** Write an unsigned integer field. */
|
|
@@ -660,6 +676,23 @@ var OStream = class {
|
|
|
660
676
|
}
|
|
661
677
|
}
|
|
662
678
|
}
|
|
679
|
+
/**
|
|
680
|
+
* Write an fp32 array from its raw little-endian element payload. The bytes
|
|
681
|
+
* are emitted verbatim — no per-element `setFloat32` — so a signaling NaN
|
|
682
|
+
* survives bit-for-bit (§4.6), which {@link writeFp32Array} cannot guarantee
|
|
683
|
+
* because it re-quantizes each JS `number`. `payload.length` must be a
|
|
684
|
+
* multiple of 4; the element count is `payload.length / 4`.
|
|
685
|
+
*/
|
|
686
|
+
writeFp32ArrayRaw(id, payload) {
|
|
687
|
+
if ((payload.length & 3) !== 0) {
|
|
688
|
+
throw argumentError(
|
|
689
|
+
`fp32 array payload length ${payload.length} is not a multiple of 4`
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
this.arrayHead(id, WireType.ArrayFixlen, payload.length >> 2);
|
|
693
|
+
this.putVarintNum(4 * 8 + FixlenSubtype.Fp32);
|
|
694
|
+
this.writeRaw(payload);
|
|
695
|
+
}
|
|
663
696
|
/** Write an array of IEEE-754 64-bit doubles. */
|
|
664
697
|
writeFp64Array(id, values) {
|
|
665
698
|
this.arrayHead(id, WireType.ArrayFixlen, values.length);
|
|
@@ -675,20 +708,82 @@ var OStream = class {
|
|
|
675
708
|
}
|
|
676
709
|
}
|
|
677
710
|
// --- sequences ----------------------------------------------------------
|
|
678
|
-
/**
|
|
679
|
-
|
|
711
|
+
/**
|
|
712
|
+
* Open a nested sequence (a fresh id scope) whose header is **held back**
|
|
713
|
+
* until the sequence turns out to have content.
|
|
714
|
+
*
|
|
715
|
+
* MESSAGE_SPEC §2 omits a sequence-typed field whose value equals its declared
|
|
716
|
+
* default, and "not one child was written" is exactly that condition —
|
|
717
|
+
* evaluated per child field, recursively, for free, because the message layer
|
|
718
|
+
* already omits every child equal to its default. A sequence closed with
|
|
719
|
+
* nothing in it therefore emits **nothing** instead of a two-byte empty frame,
|
|
720
|
+
* and an all-default message becomes the empty byte string. No byte image is
|
|
721
|
+
* ever compared, so in-memory layout never enters the decision.
|
|
722
|
+
*
|
|
723
|
+
* This is the only way to open a sequence. How it closes decides whether a
|
|
724
|
+
* contentless one survives: {@link OStream.writeSequenceEnd} drops it,
|
|
725
|
+
* {@link OStream.writeSequenceEndKeep} forces the frame out.
|
|
726
|
+
*/
|
|
727
|
+
writeSequenceBeginLazy(id) {
|
|
680
728
|
if (this.depth >= MAX_DEPTH) {
|
|
681
|
-
throw
|
|
729
|
+
throw argumentError(`nesting exceeds MAX_DEPTH (${MAX_DEPTH})`);
|
|
682
730
|
}
|
|
683
|
-
|
|
731
|
+
if (id < 0 || id > ID_MAX || !Number.isInteger(id)) {
|
|
732
|
+
throw argumentError(`field id ${id} out of range 0..${ID_MAX}`);
|
|
733
|
+
}
|
|
734
|
+
this.pending[this.nPending++] = id;
|
|
684
735
|
this.depth++;
|
|
685
736
|
}
|
|
686
|
-
/**
|
|
737
|
+
/**
|
|
738
|
+
* Close the current sequence, letting it **vanish** if it received no content.
|
|
739
|
+
*
|
|
740
|
+
* Use it wherever absence encodes the same value as an empty frame: a
|
|
741
|
+
* `struct`/`union` field, and an array field whose declared `default` is the
|
|
742
|
+
* empty collection (MESSAGE_SPEC §2). Where the frame must be visible, close
|
|
743
|
+
* with {@link OStream.writeSequenceEndKeep} instead.
|
|
744
|
+
*
|
|
745
|
+
* An end with no matching begin is not rejected: the encoder writes what it is
|
|
746
|
+
* told, and the resulting bytes are then malformed, which is the decoder's
|
|
747
|
+
* verdict to make. No other port refuses it. The depth counter stops at zero
|
|
748
|
+
* so the MAX_DEPTH check on begin cannot be fooled by an underflow.
|
|
749
|
+
*/
|
|
687
750
|
writeSequenceEnd() {
|
|
688
|
-
if (this.
|
|
751
|
+
if (this.nPending !== 0) {
|
|
752
|
+
this.nPending--;
|
|
753
|
+
if (this.depth > 0) this.depth--;
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
689
756
|
this.ensure(1);
|
|
690
757
|
this.buf[this.pos++] = WireType.SequenceEnd;
|
|
691
|
-
this.depth--;
|
|
758
|
+
if (this.depth > 0) this.depth--;
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Close the current sequence, **keeping** its frame even when it received no
|
|
762
|
+
* content.
|
|
763
|
+
*
|
|
764
|
+
* Behaves like a write: it first emits any held-back headers — this frame's
|
|
765
|
+
* and every enclosing one's — and then the end marker, so an empty sequence
|
|
766
|
+
* reaches the wire as `begin` + `end`.
|
|
767
|
+
*
|
|
768
|
+
* Required wherever the frame carries information beyond its contents:
|
|
769
|
+
* - a **wrapper-array element** (`struct`/`union`/nested row): element
|
|
770
|
+
* presence is what carries a dynamic array's length — *highest present id +
|
|
771
|
+
* 1* (MESSAGE_SPEC §5.1) — so dropping an all-default element would change
|
|
772
|
+
* the decoded length, not just the bytes;
|
|
773
|
+
* - an array field already known to **differ from a non-empty declared
|
|
774
|
+
* `default`**: absence would reconstruct that default, so the empty frame is
|
|
775
|
+
* the only encoding of "explicitly empty" (§2, §3).
|
|
776
|
+
*
|
|
777
|
+
* The two failure directions are not symmetric, which is why this is the safe
|
|
778
|
+
* choice when in doubt: using it where {@link OStream.writeSequenceEnd} would
|
|
779
|
+
* do costs one non-canonical empty frame that a decoder normalizes away, while
|
|
780
|
+
* the reverse silently changes an array's length.
|
|
781
|
+
*/
|
|
782
|
+
writeSequenceEndKeep() {
|
|
783
|
+
if (this.nPending !== 0) this.commitPending();
|
|
784
|
+
this.ensure(1);
|
|
785
|
+
this.buf[this.pos++] = WireType.SequenceEnd;
|
|
786
|
+
if (this.depth > 0) this.depth--;
|
|
692
787
|
}
|
|
693
788
|
// --- internals ----------------------------------------------------------
|
|
694
789
|
/** Ensure exactly `value`'s varint size, then write it (bigint path). */
|
|
@@ -701,12 +796,44 @@ var OStream = class {
|
|
|
701
796
|
this.ensure(varintSizeNum(value));
|
|
702
797
|
this.pos = encodeVarintNum(value, this.buf, this.pos);
|
|
703
798
|
}
|
|
799
|
+
/**
|
|
800
|
+
* Write a field header, the `(id << 3) | wireType` tag, as a varint.
|
|
801
|
+
*
|
|
802
|
+
* This is the single choke point every field write passes through — the
|
|
803
|
+
* scalar, fixlen, float, string, blob and both array writers all reach the
|
|
804
|
+
* wire through `header` / `fixlenHead` / `arrayHead`, and `fixlenHead` and
|
|
805
|
+
* `arrayHead` are themselves nothing but `header` plus a follow-up varint. So
|
|
806
|
+
* this is also where a held-back sequence run is committed: the field about to
|
|
807
|
+
* be written is content, which means every enclosing sequence is non-default
|
|
808
|
+
* and must be framed after all (MESSAGE_SPEC §2).
|
|
809
|
+
*
|
|
810
|
+
* The only writers that do *not* pass through here are the two sequence
|
|
811
|
+
* closers, which must not commit ({@link OStream.writeSequenceEnd}) or commit
|
|
812
|
+
* explicitly ({@link OStream.writeSequenceEndKeep}), and
|
|
813
|
+
* {@link OStream.writeSequenceBeginLazy}, which writes no byte at all.
|
|
814
|
+
*/
|
|
704
815
|
header(id, type) {
|
|
705
816
|
if (id < 0 || id > ID_MAX || !Number.isInteger(id)) {
|
|
706
817
|
throw argumentError(`field id ${id} out of range 0..${ID_MAX}`);
|
|
707
818
|
}
|
|
819
|
+
if (this.nPending !== 0) this.commitPending();
|
|
708
820
|
this.putVarintNum(id * 8 + type);
|
|
709
821
|
}
|
|
822
|
+
/**
|
|
823
|
+
* Write out the held-back sequence headers, **outermost first**, and clear the
|
|
824
|
+
* run. Runs at most once per non-default sequence, never per field — the cost
|
|
825
|
+
* on the hot path is the single `nPending` test in {@link header}.
|
|
826
|
+
*
|
|
827
|
+
* The count is zeroed before the first byte goes out, so a write re-entered
|
|
828
|
+
* from a flush sink cannot emit the same run twice.
|
|
829
|
+
*/
|
|
830
|
+
commitPending() {
|
|
831
|
+
const n = this.nPending;
|
|
832
|
+
this.nPending = 0;
|
|
833
|
+
for (let i = 0; i < n; i++) {
|
|
834
|
+
this.putVarintNum(this.pending[i] * 8 + WireType.SequenceStart);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
710
837
|
fixlenHead(id, length, subtype) {
|
|
711
838
|
this.header(id, WireType.Fixlen);
|
|
712
839
|
this.putVarintNum(length * 8 + subtype);
|
|
@@ -826,9 +953,14 @@ var FastDecoder = class {
|
|
|
826
953
|
if (sub === FixlenSubtype.Fp32 || sub === FixlenSubtype.Fp64) {
|
|
827
954
|
const want = sub === FixlenSubtype.Fp32 ? 4 : 8;
|
|
828
955
|
if (len !== want) throw invalidMsgError("fixlen float length mismatch");
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
956
|
+
if (sub === FixlenSubtype.Fp32) {
|
|
957
|
+
const p = this.p;
|
|
958
|
+
const value = this.readFp32();
|
|
959
|
+
top.fp32?.(id, value, top.fp32Raw ? this.buf.subarray(p, p + 4) : void 0);
|
|
960
|
+
} else {
|
|
961
|
+
const value = this.readFp64();
|
|
962
|
+
top.fp64?.(id, value);
|
|
963
|
+
}
|
|
832
964
|
} else {
|
|
833
965
|
const chunk = this.take(len);
|
|
834
966
|
if (sub === FixlenSubtype.String) top.string?.(id, len, 0, chunk);
|
|
@@ -867,9 +999,11 @@ var FastDecoder = class {
|
|
|
867
999
|
else throw invalidMsgError("invalid fixlen array element type");
|
|
868
1000
|
top.arrayBegin?.(id, kind, count);
|
|
869
1001
|
if (kind === ArrayKind.Fp32) {
|
|
1002
|
+
const wantRaw = top.fp32Raw === true;
|
|
870
1003
|
for (let i = 0; i < count; i++) {
|
|
1004
|
+
const p = this.p;
|
|
871
1005
|
const value = this.readFp32();
|
|
872
|
-
top.arrayFp32?.(id, i, value);
|
|
1006
|
+
top.arrayFp32?.(id, i, value, wantRaw ? this.buf.subarray(p, p + 4) : void 0);
|
|
873
1007
|
}
|
|
874
1008
|
} else {
|
|
875
1009
|
for (let i = 0; i < count; i++) {
|
|
@@ -1130,8 +1264,10 @@ var DecoderState = class {
|
|
|
1130
1264
|
i = this.fpStep(input, i);
|
|
1131
1265
|
if (this.have < this.need) return;
|
|
1132
1266
|
const value = this.fixSub === FixlenSubtype.Fp32 ? unpackFp32(this.scratch, 0) : unpackFp64(this.scratch, 0);
|
|
1133
|
-
if (this.fixSub === FixlenSubtype.Fp32)
|
|
1134
|
-
|
|
1267
|
+
if (this.fixSub === FixlenSubtype.Fp32) {
|
|
1268
|
+
const top = this.top();
|
|
1269
|
+
top.fp32?.(this.id, value, top.fp32Raw ? this.scratch.subarray(0, 4) : void 0);
|
|
1270
|
+
} else this.top().fp64?.(this.id, value);
|
|
1135
1271
|
this.state = 0 /* Header */;
|
|
1136
1272
|
break;
|
|
1137
1273
|
}
|
|
@@ -1213,8 +1349,10 @@ var DecoderState = class {
|
|
|
1213
1349
|
i = this.fpStep(input, i);
|
|
1214
1350
|
if (this.have < this.need) return;
|
|
1215
1351
|
const value = this.arrKind === ArrayKind.Fp32 ? unpackFp32(this.scratch, 0) : unpackFp64(this.scratch, 0);
|
|
1216
|
-
if (this.arrKind === ArrayKind.Fp32)
|
|
1217
|
-
|
|
1352
|
+
if (this.arrKind === ArrayKind.Fp32) {
|
|
1353
|
+
const top = this.top();
|
|
1354
|
+
top.arrayFp32?.(this.id, this.arrIndex, value, top.fp32Raw ? this.scratch.subarray(0, 4) : void 0);
|
|
1355
|
+
} else this.top().arrayFp64?.(this.id, this.arrIndex, value);
|
|
1218
1356
|
this.have = 0;
|
|
1219
1357
|
this.advanceArray();
|
|
1220
1358
|
break;
|
|
@@ -1509,7 +1647,12 @@ var Cursor = class {
|
|
|
1509
1647
|
}
|
|
1510
1648
|
const id = this.upper();
|
|
1511
1649
|
if (id > ID_MAX) throw invalidMsgError(`field id ${id} out of range`);
|
|
1512
|
-
if (wire === WireType.SequenceStart)
|
|
1650
|
+
if (wire === WireType.SequenceStart) {
|
|
1651
|
+
if (this.depth >= MAX_DEPTH) {
|
|
1652
|
+
throw invalidMsgError(`nesting exceeds MAX_DEPTH (${MAX_DEPTH})`);
|
|
1653
|
+
}
|
|
1654
|
+
this.depth++;
|
|
1655
|
+
}
|
|
1513
1656
|
this.id = id;
|
|
1514
1657
|
this.wire = wire;
|
|
1515
1658
|
this.fixSub = this.peekFixSub(wire);
|
|
@@ -1530,14 +1673,40 @@ var Cursor = class {
|
|
|
1530
1673
|
this.fixlenHeader(FixlenSubtype.Fp32, 4);
|
|
1531
1674
|
return this.rawFp32();
|
|
1532
1675
|
}
|
|
1676
|
+
/**
|
|
1677
|
+
* Read a 32-bit float scalar as its raw 4 wire bytes (little-endian), zero-copy
|
|
1678
|
+
* — the bit-preserving companion to {@link readFp32}.
|
|
1679
|
+
*
|
|
1680
|
+
* {@link readFp32} returns a JS `number` (a 64-bit double), and widening an
|
|
1681
|
+
* fp32 *signaling* NaN into a double quiets it (0x7F800001 → 0x7FC00001), so a
|
|
1682
|
+
* value consumer can never round-trip one bit-for-bit (§4.6). Generated
|
|
1683
|
+
* bit-exact decode reads the bytes here instead and re-emits them verbatim with
|
|
1684
|
+
* {@link OStream.writeFixlen} (subtype fp32) — mirroring the visitor `raw`
|
|
1685
|
+
* channel on the push paths (fast.ts / state.ts), which the pull path was
|
|
1686
|
+
* missing (corelib-ts#66).
|
|
1687
|
+
*
|
|
1688
|
+
* The header (subtype fp32, length 4) is validated exactly as in
|
|
1689
|
+
* {@link readFp32}; the returned view aliases the source buffer, valid only
|
|
1690
|
+
* until it is reused, like {@link readBlob}.
|
|
1691
|
+
*/
|
|
1692
|
+
readFp32Raw() {
|
|
1693
|
+
this.fixlenHeader(FixlenSubtype.Fp32, 4);
|
|
1694
|
+
return this.take(4);
|
|
1695
|
+
}
|
|
1533
1696
|
/** Read a 64-bit float scalar (wire {@link WireType.Fixlen}, subtype fp64). */
|
|
1534
1697
|
readFp64() {
|
|
1535
1698
|
this.fixlenHeader(FixlenSubtype.Fp64, 8);
|
|
1536
1699
|
return this.rawFp64();
|
|
1537
1700
|
}
|
|
1538
|
-
/**
|
|
1539
|
-
|
|
1540
|
-
|
|
1701
|
+
/**
|
|
1702
|
+
* Read a UTF-8 string scalar (wire {@link WireType.Fixlen}, subtype string).
|
|
1703
|
+
* Pass the schema `maxlen` (byte length) for a bounded string so an
|
|
1704
|
+
* over-length is rejected as `INVALID` at the header, before the payload is
|
|
1705
|
+
* taken (see {@link fixlenLen}); the wire length is exactly the UTF-8 byte
|
|
1706
|
+
* length, so the check is exact. Omit for an unbounded string.
|
|
1707
|
+
*/
|
|
1708
|
+
readString(schemaMaxlen) {
|
|
1709
|
+
const len = this.fixlenLen(FixlenSubtype.String, schemaMaxlen);
|
|
1541
1710
|
const bytes = this.take(len);
|
|
1542
1711
|
try {
|
|
1543
1712
|
return _utf8.decode(bytes);
|
|
@@ -1549,13 +1718,18 @@ var Cursor = class {
|
|
|
1549
1718
|
* Read a blob scalar (wire {@link WireType.Fixlen}, subtype blob) as a
|
|
1550
1719
|
* zero-copy {@link Uint8Array} view into the source buffer.
|
|
1551
1720
|
*/
|
|
1552
|
-
readBlob() {
|
|
1553
|
-
const len = this.fixlenLen(FixlenSubtype.Blob);
|
|
1721
|
+
readBlob(schemaMaxlen) {
|
|
1722
|
+
const len = this.fixlenLen(FixlenSubtype.Blob, schemaMaxlen);
|
|
1554
1723
|
return this.take(len);
|
|
1555
1724
|
}
|
|
1556
|
-
/**
|
|
1557
|
-
|
|
1558
|
-
|
|
1725
|
+
/**
|
|
1726
|
+
* Read an unsigned array (wire {@link WireType.ArrayUnsigned}), number-first
|
|
1727
|
+
* per element. Pass the schema `count` for a bounded array so an over-count is
|
|
1728
|
+
* rejected as `INVALID` at the header (see {@link arrayCount}); omit it for an
|
|
1729
|
+
* unbounded array (today's behavior).
|
|
1730
|
+
*/
|
|
1731
|
+
readUnsignedArray(schemaCount) {
|
|
1732
|
+
const count = this.arrayCount(schemaCount);
|
|
1559
1733
|
const out = new Array(count);
|
|
1560
1734
|
for (let i = 0; i < count; i++) {
|
|
1561
1735
|
this.readVarint();
|
|
@@ -1564,8 +1738,8 @@ var Cursor = class {
|
|
|
1564
1738
|
return out;
|
|
1565
1739
|
}
|
|
1566
1740
|
/** Read a signed array (wire {@link WireType.ArraySigned}), zig-zag, number-first per element. */
|
|
1567
|
-
readSignedArray() {
|
|
1568
|
-
const count = this.arrayCount();
|
|
1741
|
+
readSignedArray(schemaCount) {
|
|
1742
|
+
const count = this.arrayCount(schemaCount);
|
|
1569
1743
|
const out = new Array(count);
|
|
1570
1744
|
for (let i = 0; i < count; i++) {
|
|
1571
1745
|
this.readVarint();
|
|
@@ -1578,8 +1752,8 @@ var Cursor = class {
|
|
|
1578
1752
|
* Each element keeps the raw lo/hi halves; call {@link Long.toBigInt} to
|
|
1579
1753
|
* materialise only the values the caller actually needs.
|
|
1580
1754
|
*/
|
|
1581
|
-
readUnsignedArrayLong() {
|
|
1582
|
-
const count = this.arrayCount();
|
|
1755
|
+
readUnsignedArrayLong(schemaCount) {
|
|
1756
|
+
const count = this.arrayCount(schemaCount);
|
|
1583
1757
|
const out = new Array(count);
|
|
1584
1758
|
for (let i = 0; i < count; i++) {
|
|
1585
1759
|
this.readVarint();
|
|
@@ -1588,8 +1762,8 @@ var Cursor = class {
|
|
|
1588
1762
|
return out;
|
|
1589
1763
|
}
|
|
1590
1764
|
/** Read a signed 64-bit array (zig-zag) into {@link Long}[] — the `bigint`-free path. */
|
|
1591
|
-
readSignedArrayLong() {
|
|
1592
|
-
const count = this.arrayCount();
|
|
1765
|
+
readSignedArrayLong(schemaCount) {
|
|
1766
|
+
const count = this.arrayCount(schemaCount);
|
|
1593
1767
|
const out = new Array(count);
|
|
1594
1768
|
for (let i = 0; i < count; i++) {
|
|
1595
1769
|
this.readVarint();
|
|
@@ -1601,15 +1775,29 @@ var Cursor = class {
|
|
|
1601
1775
|
return out;
|
|
1602
1776
|
}
|
|
1603
1777
|
/** Read an fp32 array (wire {@link WireType.ArrayFixlen}, element subtype fp32). */
|
|
1604
|
-
readFp32Array() {
|
|
1605
|
-
const count = this.arrayFixlenHeader(FixlenSubtype.Fp32, 4);
|
|
1778
|
+
readFp32Array(schemaCount) {
|
|
1779
|
+
const count = this.arrayFixlenHeader(FixlenSubtype.Fp32, 4, schemaCount);
|
|
1606
1780
|
const out = new Array(count);
|
|
1607
1781
|
for (let i = 0; i < count; i++) out[i] = this.rawFp32();
|
|
1608
1782
|
return out;
|
|
1609
1783
|
}
|
|
1784
|
+
/**
|
|
1785
|
+
* Read an fp32 array as its raw little-endian element payload (`count * 4`
|
|
1786
|
+
* bytes), zero-copy — the bit-preserving companion to {@link readFp32Array}.
|
|
1787
|
+
* Widening each element to a JS `number` quiets an fp32 *signaling* NaN just as
|
|
1788
|
+
* on the scalar path (§4.6; see {@link readFp32Raw}), so bit-exact decode reads
|
|
1789
|
+
* the whole payload here and re-emits it with {@link OStream.writeFp32ArrayRaw}
|
|
1790
|
+
* (corelib-ts#66). The header (element subtype fp32, size 4) is validated
|
|
1791
|
+
* exactly as in {@link readFp32Array}; the returned view aliases the source
|
|
1792
|
+
* buffer, like {@link readBlob}.
|
|
1793
|
+
*/
|
|
1794
|
+
readFp32ArrayRaw(schemaCount) {
|
|
1795
|
+
const count = this.arrayFixlenHeader(FixlenSubtype.Fp32, 4, schemaCount);
|
|
1796
|
+
return this.take(count * 4);
|
|
1797
|
+
}
|
|
1610
1798
|
/** Read an fp64 array (wire {@link WireType.ArrayFixlen}, element subtype fp64). */
|
|
1611
|
-
readFp64Array() {
|
|
1612
|
-
const count = this.arrayFixlenHeader(FixlenSubtype.Fp64, 8);
|
|
1799
|
+
readFp64Array(schemaCount) {
|
|
1800
|
+
const count = this.arrayFixlenHeader(FixlenSubtype.Fp64, 8, schemaCount);
|
|
1613
1801
|
const out = new Array(count);
|
|
1614
1802
|
for (let i = 0; i < count; i++) out[i] = this.rawFp64();
|
|
1615
1803
|
return out;
|
|
@@ -1689,8 +1877,14 @@ var Cursor = class {
|
|
|
1689
1877
|
}
|
|
1690
1878
|
const id = this.upper();
|
|
1691
1879
|
if (id > ID_MAX) throw invalidMsgError(`field id ${id} out of range`);
|
|
1692
|
-
if (wire === WireType.SequenceStart)
|
|
1693
|
-
|
|
1880
|
+
if (wire === WireType.SequenceStart) {
|
|
1881
|
+
if (this.depth + depth - 1 >= MAX_DEPTH) {
|
|
1882
|
+
throw invalidMsgError(`nesting exceeds MAX_DEPTH (${MAX_DEPTH})`);
|
|
1883
|
+
}
|
|
1884
|
+
depth++;
|
|
1885
|
+
} else {
|
|
1886
|
+
this.skipValue(wire);
|
|
1887
|
+
}
|
|
1694
1888
|
}
|
|
1695
1889
|
}
|
|
1696
1890
|
// --- field helpers ------------------------------------------------------
|
|
@@ -1717,11 +1911,18 @@ var Cursor = class {
|
|
|
1717
1911
|
}
|
|
1718
1912
|
return -1;
|
|
1719
1913
|
}
|
|
1720
|
-
/**
|
|
1721
|
-
|
|
1914
|
+
/**
|
|
1915
|
+
* Read and validate an array count word (0..ARRAY_MAX; §4.7/§4.8). When a
|
|
1916
|
+
* `schemaCount` is given, a count above it is a schema-bound violation and is
|
|
1917
|
+
* rejected as `INVALID` — see the check below.
|
|
1918
|
+
*/
|
|
1919
|
+
arrayCount(schemaCount) {
|
|
1722
1920
|
this.readVarint();
|
|
1723
1921
|
const count = this.num();
|
|
1724
1922
|
if (count > ARRAY_MAX) throw invalidMsgError("array count out of range");
|
|
1923
|
+
if (schemaCount !== void 0 && count > schemaCount) {
|
|
1924
|
+
throw invalidMsgError("array count above schema capacity");
|
|
1925
|
+
}
|
|
1725
1926
|
if (count > this.maxArrayCount) {
|
|
1726
1927
|
throw limitExceededError(
|
|
1727
1928
|
`array count ${count} exceeds maxArrayCount ${this.maxArrayCount}`
|
|
@@ -1738,13 +1939,20 @@ var Cursor = class {
|
|
|
1738
1939
|
if (sub !== wantSub) throw invalidMsgError(`invalid fixlen subtype ${sub}`);
|
|
1739
1940
|
if (len !== wantLen) throw invalidMsgError("fixlen float length mismatch");
|
|
1740
1941
|
}
|
|
1741
|
-
/**
|
|
1742
|
-
|
|
1942
|
+
/**
|
|
1943
|
+
* Read a scalar fixlen sub-header for a string/blob, asserting subtype;
|
|
1944
|
+
* returns the byte length. When a `schemaMaxlen` is given, a length above it
|
|
1945
|
+
* is a schema-bound violation and is rejected as `INVALID` — see below.
|
|
1946
|
+
*/
|
|
1947
|
+
fixlenLen(wantSub, schemaMaxlen) {
|
|
1743
1948
|
this.readVarint();
|
|
1744
1949
|
const sub = this.lo & 7;
|
|
1745
1950
|
const len = this.upper();
|
|
1746
1951
|
if (sub !== wantSub) throw invalidMsgError(`invalid fixlen subtype ${sub}`);
|
|
1747
1952
|
if (len > FIXLEN_MAX) throw invalidMsgError("fixlen length out of range");
|
|
1953
|
+
if (schemaMaxlen !== void 0 && len > schemaMaxlen) {
|
|
1954
|
+
throw invalidMsgError("fixlen length above schema maxlen");
|
|
1955
|
+
}
|
|
1748
1956
|
const limit = wantSub === FixlenSubtype.String ? this.maxStringLen : this.maxBlobLen;
|
|
1749
1957
|
if (len > limit) {
|
|
1750
1958
|
const what = wantSub === FixlenSubtype.String ? "string" : "blob";
|
|
@@ -1755,11 +1963,18 @@ var Cursor = class {
|
|
|
1755
1963
|
}
|
|
1756
1964
|
return len;
|
|
1757
1965
|
}
|
|
1758
|
-
/**
|
|
1759
|
-
|
|
1966
|
+
/**
|
|
1967
|
+
* Read an array fixlen element header (count + element type); returns the
|
|
1968
|
+
* count. When a `schemaCount` is given, a count above it is a schema-bound
|
|
1969
|
+
* violation and is rejected as `INVALID` — see below.
|
|
1970
|
+
*/
|
|
1971
|
+
arrayFixlenHeader(wantSub, wantSize, schemaCount) {
|
|
1760
1972
|
this.readVarint();
|
|
1761
1973
|
const count = this.num();
|
|
1762
1974
|
if (count > ARRAY_MAX) throw invalidMsgError("array count out of range");
|
|
1975
|
+
if (schemaCount !== void 0 && count > schemaCount) {
|
|
1976
|
+
throw invalidMsgError("array count above schema capacity");
|
|
1977
|
+
}
|
|
1763
1978
|
if (count > this.maxArrayCount) {
|
|
1764
1979
|
throw limitExceededError(
|
|
1765
1980
|
`array count ${count} exceeds maxArrayCount ${this.maxArrayCount}`
|