@zerodds/wire 0.0.0-stage → 1.0.0-rc.7

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/dist/reader.js ADDED
@@ -0,0 +1,1345 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2026 ZeroDDS Contributors
3
+ /**
4
+ * The read half of XCDR, implemented from DDS-XTypes 1.3 §7.4.
5
+ *
6
+ * The mirror of `writer.ts` clause for clause and not by inversion of
7
+ * it: a reader written as "whatever undoes the writer" agrees with the
8
+ * writer about a misreading of the clause, and agreement between two
9
+ * halves of one column is the one thing that proves nothing.
10
+ *
11
+ * ## Two kinds of failure, and why they are not one
12
+ *
13
+ * A malformed *stream* throws. A length that runs past the end of the
14
+ * buffer is not a state a caller can carry on from, and
15
+ * {@link Truncated} against {@link ImpossibleLength} is the distinction
16
+ * that matters: the first says the stream is short, the second says the
17
+ * stream is lying. Allocating for a claimed length before checking it
18
+ * is how a reader turns a bad message into an out-of-memory.
19
+ *
20
+ * A refusal about the *type* sets {@link Status} and does not throw.
21
+ * The runtime owns the wire and the generated decoder owns the type,
22
+ * and this reader cannot see an integer that names no enumerator, a
23
+ * bitmask bit the declaration does not have, or a member that did not
24
+ * arrive. §7.2.2.7's DISCARD is a sample the receiver drops and carries
25
+ * on from, which is not a failure of the peer.
26
+ *
27
+ * ## What TypeScript decides differently from the other columns
28
+ *
29
+ * - `readChar` and `readWchar` answer with a one-character **string**,
30
+ * not with a code point. TypeScript has no character type, the
31
+ * materialiser declares a `char` member as `string`, and
32
+ * `Writer.writeChar` takes one; a reader answering with a `number`
33
+ * would be the only one of the three that did not.
34
+ * - `readU32` answers with a `number`, because 4294967295 is far below
35
+ * `Number.MAX_SAFE_INTEGER`. `readI64` and `readU64` answer with a
36
+ * `bigint`, because 2^63 is not.
37
+ * - `readEnum`, `readBitmask` and `readBitset` answer with a `bigint`
38
+ * whatever their bound. The generated decoder compares the answer
39
+ * against 64-bit patterns, and one comparison that mixed a `number`
40
+ * with a `bigint` is a `tsc` error rather than a wrong arm — which
41
+ * is the outcome this column wants.
42
+ * - `readF128` answers with the sixteen octets. A binary64 would drop
43
+ * sixty bits of the significand at the assignment.
44
+ */
45
+ import { isLittle } from "./endianness.js";
46
+ import { ImpossibleLength, NotUtf8, OutOfRange, Truncated } from "./error.js";
47
+ import { FixedDigitCount, ParameterRole, Status, WcharFraming, WcharWidth, WstringLengthPolicy, WstringUnitOrder, } from "./policy.js";
48
+ import { PID_EXTENDED, PID_IMPL_EXTENSION, PID_LIST_END, PID_MUST_UNDERSTAND, PL_CDR1_DEFAULT, pidFlagsAt, } from "./writer.js";
49
+ /**
50
+ * The low fourteen bits of a short parameter id, which are the id.
51
+ *
52
+ * §7.4.1.2.2 puts `FLAG_IMPL_EXTENSION` at bit 15 and the
53
+ * must-understand flag at bit 14; everything below them is the
54
+ * identifier.
55
+ */
56
+ export const PID_ID_MASK = 0x3fff;
57
+ /**
58
+ * `PID_IGNORE`. §7.4.1.2.2: "All consumers of this Data Representation
59
+ * shall ignore this parameter" — with no exception for the
60
+ * must-understand bit above it.
61
+ *
62
+ * Declared here and not in `writer.ts` because no writer in this tree
63
+ * emits one. It is a parameter a *peer* sends, so only the reader has
64
+ * anything to do with it.
65
+ */
66
+ export const PID_IGNORE = 0x3f03;
67
+ /** `FLAG_2` of an `eMemberHeader`: the member's must-understand. §7.4.1.2.1. */
68
+ const E_FLAG_MUST_UNDERSTAND = 0x4000_0000;
69
+ /**
70
+ * `FLAG_1` of an `eMemberHeader`: `FLAG_IMPL_EXTENSION`, at bit 31
71
+ * exactly as bit 15 carries it in the short header.
72
+ */
73
+ const E_FLAG_IMPL_EXTENSION = 0x8000_0000;
74
+ /** The member id an `eMemberHeader` carries. §7.4.1.2.1. */
75
+ const E_MEMBER_ID_MASK = 0x0fff_ffff;
76
+ /** The UTF-16 byte-order mark. */
77
+ const BYTE_ORDER_MARK = 0xfeff;
78
+ /**
79
+ * The byte-swapped mark.
80
+ *
81
+ * `U+FFFE` is not a character: a mark that reads as one says the units
82
+ * behind it are the other way round, which is the whole of what a mark
83
+ * is for.
84
+ */
85
+ const SWAPPED_BYTE_ORDER_MARK = 0xfffe;
86
+ /**
87
+ * The widest `fixed` IDL admits is `fixed<31, S>`, which is sixteen
88
+ * octets — thirty-two nibbles. A value-precision field that presents no
89
+ * sign nibble inside them is not a `fixed` at all.
90
+ */
91
+ const MAX_FIXED_NIBBLES = 32;
92
+ /** The first nibble value [CDR] §9.3.2.8 reserves for a sign. */
93
+ const FIRST_SIGN_NIBBLE = 0x0a;
94
+ /** The sign nibble that means negative. [CDR] §9.3.2.8. */
95
+ const MINUS_NIBBLE = 0x0d;
96
+ /**
97
+ * Whether a parameter can be one of the reading type's members.
98
+ *
99
+ * False for a vendor-private parameter, for a reserved id and for
100
+ * `PID_IGNORE`. All three are legal for a peer to send and none of them
101
+ * is a member, so all three take the unknown-parameter path — skipped,
102
+ * or the sample discarded when the writer set must-understand.
103
+ * §7.4.1.2.2, §7.2.2.4.4.4.6.
104
+ *
105
+ * A free function and not a method, because the generated decoder calls
106
+ * it without a receiver.
107
+ */
108
+ export function pidIsUserMember(header) {
109
+ return !header.implExtension && !header.reserved && !header.ignore;
110
+ }
111
+ /**
112
+ * How many octets `text` is as UTF-8, which is what a `string<N>` bound
113
+ * counts: `string<5>` is `char[6]` in the C mapping of every measured
114
+ * DDS implementation, so a five-scalar value of ten octets is one no
115
+ * peer can hold.
116
+ */
117
+ export function utf8Octets(text) {
118
+ return new TextEncoder().encode(text).length;
119
+ }
120
+ /**
121
+ * How many UTF-16 code units `text` is, which is what a `wstring<N>`
122
+ * bound counts. A surrogate pair is two, which is what `String.length`
123
+ * already answers.
124
+ */
125
+ export function utf16Units(text) {
126
+ return text.length;
127
+ }
128
+ /**
129
+ * The same value in a `number`, whatever holder it arrived in.
130
+ *
131
+ * The generated decoder's narrowings are `as` in every other column and
132
+ * `as` is *erased* here: `zd_bits as number` on a `bigint` is a `tsc`
133
+ * error, and where it is not — `x as unknown as number` — it would put
134
+ * a `bigint` in a `number` field and every later comparison would be
135
+ * false. So the three coercions below are real conversions, and the
136
+ * TypeScript backend emits a call to one where the other columns emit a
137
+ * cast.
138
+ *
139
+ * A one-character string answers with its code point, which is what the
140
+ * narrowing means where the source is a `char`.
141
+ */
142
+ export function zdNumber(value) {
143
+ if (typeof value === "string") {
144
+ return value.codePointAt(0) ?? 0;
145
+ }
146
+ if (typeof value === "boolean") {
147
+ return value ? 1 : 0;
148
+ }
149
+ return Number(value);
150
+ }
151
+ /** The same value in a `bigint`. See {@link zdNumber}. */
152
+ export function zdBigInt(value) {
153
+ if (typeof value === "string") {
154
+ return BigInt(value.codePointAt(0) ?? 0);
155
+ }
156
+ return BigInt(value);
157
+ }
158
+ /**
159
+ * The same value as a one-character `string`, which is what an IDL
160
+ * `char` and `wchar` are here.
161
+ *
162
+ * A value that is already a string is itself; a number or a `bigint` is
163
+ * the character its code point names. `String(65)` would be `"65"`,
164
+ * which is two characters and a different value.
165
+ */
166
+ export function zdChar(value) {
167
+ if (typeof value === "string") {
168
+ return value;
169
+ }
170
+ return String.fromCodePoint(Number(value));
171
+ }
172
+ const WSTRING_CELLS = new Map([
173
+ [
174
+ WstringLengthPolicy.Specification,
175
+ { countsCharacters: false, terminated: false, unitWidth: 2 },
176
+ ],
177
+ [
178
+ WstringLengthPolicy.CharacterCount,
179
+ { countsCharacters: true, terminated: false, unitWidth: 2 },
180
+ ],
181
+ [
182
+ WstringLengthPolicy.CharacterCountTerminated,
183
+ { countsCharacters: true, terminated: true, unitWidth: 4 },
184
+ ],
185
+ [
186
+ WstringLengthPolicy.SpecificationWideUnits,
187
+ { countsCharacters: false, terminated: false, unitWidth: 4 },
188
+ ],
189
+ [
190
+ WstringLengthPolicy.RosTypeSupport,
191
+ { countsCharacters: true, terminated: false, unitWidth: 4 },
192
+ ],
193
+ [
194
+ WstringLengthPolicy.OctetsTerminated2,
195
+ { countsCharacters: false, terminated: true, unitWidth: 2 },
196
+ ],
197
+ [
198
+ WstringLengthPolicy.OctetsTerminated4,
199
+ { countsCharacters: false, terminated: true, unitWidth: 4 },
200
+ ],
201
+ [
202
+ WstringLengthPolicy.CharactersTerminated2,
203
+ { countsCharacters: true, terminated: true, unitWidth: 2 },
204
+ ],
205
+ ]);
206
+ /**
207
+ * Whether a unit order reads its code units little-endian regardless of
208
+ * the stream.
209
+ *
210
+ * `host-unswapped` is what Fast CDR writes — the host's order, never
211
+ * byte-swapped — and this runtime's host is little-endian wherever
212
+ * JavaScript's `DataView` default is not asked for. `byte-order-mark`
213
+ * reads little too and then obeys the mark it found, which is correct in
214
+ * all four combinations of writer host and stream order: the mark is
215
+ * written in the same order as the content it introduces.
216
+ */
217
+ function readsLittleAlways(order) {
218
+ return (order === WstringUnitOrder.HostUnswapped ||
219
+ order === WstringUnitOrder.ByteOrderMark);
220
+ }
221
+ export class Reader {
222
+ order;
223
+ plCdr1;
224
+ bytes;
225
+ at = 0;
226
+ origin = 0;
227
+ maxAlignment;
228
+ state = Status.Ok;
229
+ /**
230
+ * A reader over `data` in `order`, capped at `maxAlign`.
231
+ *
232
+ * Four for XCDR2, eight for XCDR1 — {@link Writer}'s counterpart.
233
+ * §7.4.1: `ALIGN(N)` aligns to `MIN(N, XCDR.maxalign)`. A reader that
234
+ * skipped padding the writer never wrote reads the wrong octets from
235
+ * the second member on.
236
+ *
237
+ * `plCdr1` is the writer's framing, and the reader needs two things
238
+ * out of it. Under a framing that puts `FLAG_IMPL_EXTENSION` on every
239
+ * member id — OpenDDS writes `c0010004` for `@id(1) long a` — the flag
240
+ * no longer tells a vendor-private parameter from a user member. And
241
+ * §7.4.1.2.2 stops the short form's ids at `0x3F00` inclusive while
242
+ * one measured implementation carries the whole low fourteen bits, so
243
+ * a reader holding the clause's threshold as a constant reports
244
+ * `@id(16383)` as a reserved parameter and drops the member.
245
+ */
246
+ constructor(data, order, maxAlign = 8, plCdr1 = PL_CDR1_DEFAULT) {
247
+ this.order = order;
248
+ this.plCdr1 = plCdr1;
249
+ this.bytes = data;
250
+ this.maxAlignment = Math.max(1, maxAlign);
251
+ }
252
+ /** The alignment cap in force. */
253
+ get maxAlign() {
254
+ return this.maxAlignment;
255
+ }
256
+ // -- the status a generated decoder reads ----------------------------
257
+ /**
258
+ * Whether a refusal has been made, so every later read does nothing.
259
+ *
260
+ * The generated decoder carries on to the end of the type rather than
261
+ * returning early — that is what lets one `if` at the top of the
262
+ * caller decide what to do with the sample — and a read that consumed
263
+ * octets after a refusal would leave the cursor somewhere no writer
264
+ * put it, which the trailing-octet check at the top would then report
265
+ * as a second, invented, fault.
266
+ */
267
+ stopped() {
268
+ return this.state !== Status.Ok;
269
+ }
270
+ /** How the read is going. */
271
+ readerStatus() {
272
+ return this.state;
273
+ }
274
+ /** Refuse the stream from outside the runtime. The first refusal sticks. */
275
+ readerRefuse(refusal) {
276
+ if (refusal !== Status.Ok && this.state === Status.Ok) {
277
+ this.state = refusal;
278
+ }
279
+ }
280
+ /**
281
+ * Take back a {@link Status.Discarded}, and only that one.
282
+ *
283
+ * A member whose own `@try_construct` is `USE_DEFAULT` but which also
284
+ * carries an explicit `@default` cannot be answered inside the type's
285
+ * decoder: the decoder knows Table 9's value for the type and not the
286
+ * literal the member's declaration gave it, and §7.2.2.4.4.4.9 puts
287
+ * the literal first. So the call site asks for DISCARD, and where it
288
+ * gets one it clears it and assigns the literal.
289
+ *
290
+ * Every other status stays: a truncated stream is not a narrowing.
291
+ */
292
+ readerClearDiscard() {
293
+ if (this.state === Status.Discarded) {
294
+ this.state = Status.Ok;
295
+ return true;
296
+ }
297
+ return false;
298
+ }
299
+ /** Where it is. The generated decoder's name for {@link position}. */
300
+ readerPosition() {
301
+ return this.at;
302
+ }
303
+ /** How many octets are left. */
304
+ readerRemaining() {
305
+ return this.remaining();
306
+ }
307
+ /**
308
+ * Move the alignment origin to the current position.
309
+ *
310
+ * The read-side pair of the writer's `beginPackedElement`, for
311
+ * `ElementPadding::Packed`. Returns the origin displaced.
312
+ */
313
+ readerBeginPackedElement() {
314
+ const saved = this.origin;
315
+ this.origin = this.at;
316
+ return saved;
317
+ }
318
+ /** Restore the origin {@link readerBeginPackedElement} displaced. */
319
+ readerFinishPackedElement(saved) {
320
+ this.origin = saved;
321
+ }
322
+ /**
323
+ * Move the cursor forward to `target`, which is never backwards.
324
+ *
325
+ * A framing word pointing behind the cursor means the reader has
326
+ * already read past what it framed, and seeking back would paper over
327
+ * it: the member would be read twice and the sample would come back
328
+ * plausible.
329
+ */
330
+ readerSkipTo(target) {
331
+ if (this.stopped()) {
332
+ return;
333
+ }
334
+ if (target < this.at) {
335
+ this.readerRefuse(Status.OutOfRange);
336
+ return;
337
+ }
338
+ if (target > this.bytes.length) {
339
+ throw new Truncated(this.at, target - this.at, this.remaining());
340
+ }
341
+ this.at = target;
342
+ }
343
+ /** The cursor, the origin and the status, to be put back later. */
344
+ readerSave() {
345
+ return { at: this.at, origin: this.origin, status: this.state };
346
+ }
347
+ /** Put back what {@link readerSave} took. */
348
+ readerRestore(mark) {
349
+ this.at = mark.at;
350
+ this.origin = mark.origin;
351
+ this.state = mark.status;
352
+ }
353
+ // -- state -----------------------------------------------------------
354
+ /** Where it is. */
355
+ position() {
356
+ return this.at;
357
+ }
358
+ /** How many octets are left. */
359
+ remaining() {
360
+ return Math.max(0, this.bytes.length - this.at);
361
+ }
362
+ /** Skip padding to `align`, counted from the stream's origin. */
363
+ alignTo(align) {
364
+ if (this.stopped()) {
365
+ return;
366
+ }
367
+ const to = Math.min(align, this.maxAlignment);
368
+ if (to <= 1) {
369
+ return;
370
+ }
371
+ const offset = this.at - this.origin;
372
+ this.at += (to - (((offset % to) + to) % to)) % to;
373
+ }
374
+ /** Step over `count` octets, refusing to step past the end. */
375
+ skip(count) {
376
+ if (this.stopped()) {
377
+ return;
378
+ }
379
+ this.take(count);
380
+ }
381
+ /** Move to an absolute offset inside the buffer. */
382
+ seek(target) {
383
+ if (this.stopped()) {
384
+ return;
385
+ }
386
+ if (target < 0 || target > this.bytes.length) {
387
+ throw new Truncated(this.at, target, this.bytes.length);
388
+ }
389
+ this.at = target;
390
+ }
391
+ /**
392
+ * Advance over `count` octets and answer where they started.
393
+ *
394
+ * A negative count is refused rather than treated as zero: it can only
395
+ * come from a length field this reader computed wrongly, and a zero
396
+ * there would carry on reading at an offset nothing wrote.
397
+ */
398
+ take(count) {
399
+ if (!Number.isInteger(count) || count < 0) {
400
+ throw new Truncated(this.at, count, this.remaining());
401
+ }
402
+ const end = this.at + count;
403
+ if (end > this.bytes.length) {
404
+ throw new Truncated(this.at, count, this.remaining());
405
+ }
406
+ const from = this.at;
407
+ this.at = end;
408
+ return from;
409
+ }
410
+ /**
411
+ * An integer of `width` octets in the stream's order, as a `bigint`.
412
+ *
413
+ * A `bigint` throughout and narrowed by the caller, because the eight-
414
+ * octet case does not fit a `number` and a second code path for it
415
+ * would be the one place the two widths could disagree about a sign.
416
+ */
417
+ readInt(width, signed) {
418
+ this.alignTo(width);
419
+ return this.rawInt(width, signed, isLittle(this.order));
420
+ }
421
+ /** {@link readInt} in a stated order rather than the stream's. */
422
+ rawInt(width, signed, little) {
423
+ const from = this.take(width);
424
+ let value = 0n;
425
+ for (let index = 0; index < width; index += 1) {
426
+ const shift = little ? index : width - 1 - index;
427
+ value |= BigInt(this.bytes[from + index] ?? 0) << BigInt(8 * shift);
428
+ }
429
+ if (signed) {
430
+ const sign = 1n << BigInt(width * 8 - 1);
431
+ if ((value & sign) !== 0n) {
432
+ value -= 1n << BigInt(width * 8);
433
+ }
434
+ }
435
+ return value;
436
+ }
437
+ // -- primitives ------------------------------------------------------
438
+ /**
439
+ * `boolean`.
440
+ *
441
+ * An octet that is neither 0 nor 1 is a stream that disagrees with its
442
+ * own type, and is refused rather than coerced: `!!octet` would turn
443
+ * every one of the 254 impossible values into `true` and say nothing.
444
+ */
445
+ readBool() {
446
+ if (this.stopped()) {
447
+ return false;
448
+ }
449
+ const where = this.at;
450
+ const value = this.readU8();
451
+ if (value === 0) {
452
+ return false;
453
+ }
454
+ if (value === 1) {
455
+ return true;
456
+ }
457
+ throw new OutOfRange(where, "a boolean of 0 or 1");
458
+ }
459
+ /** `octet` and `uint8`. */
460
+ readU8() {
461
+ if (this.stopped()) {
462
+ return 0;
463
+ }
464
+ return this.bytes[this.take(1)] ?? 0;
465
+ }
466
+ /** `int8`. */
467
+ readI8() {
468
+ if (this.stopped()) {
469
+ return 0;
470
+ }
471
+ const value = this.bytes[this.take(1)] ?? 0;
472
+ return value >= 0x80 ? value - 0x100 : value;
473
+ }
474
+ /**
475
+ * `char`, one octet, as the one-character string it is here.
476
+ *
477
+ * The octet is a code point in `[0, 255]` and not a UTF-8 lead: an IDL
478
+ * `char` is one octet and §7.4.1.1.1 gives it no encoding of its own,
479
+ * so `0xE4` is U+00E4 and not the first half of anything.
480
+ */
481
+ readChar() {
482
+ if (this.stopped()) {
483
+ return "\0";
484
+ }
485
+ return String.fromCharCode(this.bytes[this.take(1)] ?? 0);
486
+ }
487
+ /**
488
+ * `wchar`, at `width` octets and in `framing`'s shape.
489
+ *
490
+ * The four-octet form is RTI Connext's under XCDR1, where the high
491
+ * sixteen bits are padding around one UTF-16 code unit. A value that
492
+ * uses them is refused rather than narrowed: narrowing would return a
493
+ * character the peer never sent.
494
+ *
495
+ * {@link WcharFraming.GiopLengthPrefixed} is CORBA 3.3 part 2
496
+ * §9.3.1.6's GIOP 1.2 form, which all three ORBs write. The octet
497
+ * count is checked against the configured code-set width rather than
498
+ * believed: a stream whose first octet says `0xE4` — which is what a
499
+ * bare two-octet `wchar` looks like read this way — would otherwise
500
+ * consume 228 octets of whatever followed and return a character
501
+ * assembled out of the next four members.
502
+ */
503
+ readWchar(width = WcharWidth.Octets2, framing = WcharFraming.Fixed, order = WstringUnitOrder.Stream) {
504
+ if (this.stopped()) {
505
+ return "\0";
506
+ }
507
+ const where = this.at;
508
+ const octets = width;
509
+ let value;
510
+ if (framing === WcharFraming.GiopLengthPrefixed) {
511
+ const declared = this.readU8();
512
+ if (declared !== octets) {
513
+ throw new OutOfRange(where, `a GIOP 1.2 wchar of ${octets} octets, the configured transmission ` +
514
+ `code set, and not ${declared}`);
515
+ }
516
+ // A scalar cannot carry a byte-order mark, so `byte-order-mark`
517
+ // means CORBA's default here — big-endian — exactly as
518
+ // `big-endian-always` does. The units are aligned to one inside
519
+ // the GIOP form, so `rawInt` and not `readInt`.
520
+ const little = order === WstringUnitOrder.Stream && isLittle(this.order);
521
+ value = Number(this.rawInt(octets, false, little));
522
+ }
523
+ else if (octets === 4) {
524
+ value = this.readU32();
525
+ if (value > 0xffff) {
526
+ throw new OutOfRange(where, "a four-octet wchar holding one UTF-16 code unit");
527
+ }
528
+ }
529
+ else {
530
+ value = this.readU16();
531
+ }
532
+ // §7.2.2.2.1.2.5 restricts a `wchar` to the BMP, and a lone
533
+ // surrogate is not a character of it: returning one would produce a
534
+ // string no `TextEncoder` can encode back.
535
+ if (value >= 0xd800 && value <= 0xdfff) {
536
+ throw new OutOfRange(where, "a Unicode code point");
537
+ }
538
+ return String.fromCharCode(value);
539
+ }
540
+ /** `short`. */
541
+ readI16() {
542
+ if (this.stopped()) {
543
+ return 0;
544
+ }
545
+ return Number(this.readInt(2, true));
546
+ }
547
+ /** `unsigned short`. */
548
+ readU16() {
549
+ if (this.stopped()) {
550
+ return 0;
551
+ }
552
+ return Number(this.readInt(2, false));
553
+ }
554
+ /** `long`. */
555
+ readI32() {
556
+ if (this.stopped()) {
557
+ return 0;
558
+ }
559
+ return Number(this.readInt(4, true));
560
+ }
561
+ /**
562
+ * `unsigned long`, as a `number`.
563
+ *
564
+ * 4294967295 is far below `Number.MAX_SAFE_INTEGER`, so unlike the
565
+ * Java column nothing has to widen and unlike the C column nothing
566
+ * comes back negative.
567
+ */
568
+ readU32() {
569
+ if (this.stopped()) {
570
+ return 0;
571
+ }
572
+ return Number(this.readInt(4, false));
573
+ }
574
+ /** `long long`, which is a `bigint` because a binary64 is not one. */
575
+ readI64() {
576
+ if (this.stopped()) {
577
+ return 0n;
578
+ }
579
+ return this.readInt(8, true);
580
+ }
581
+ /**
582
+ * `unsigned long long`, as the value and not as a pattern.
583
+ *
584
+ * Where the Java column has to hand back `-1L` for
585
+ * `0xFFFFFFFFFFFFFFFF` because it has nothing wider than a `long`, a
586
+ * `bigint` holds 18446744073709551615 as itself.
587
+ */
588
+ readU64() {
589
+ if (this.stopped()) {
590
+ return 0n;
591
+ }
592
+ return this.readInt(8, false);
593
+ }
594
+ /** `float`, a binary32. */
595
+ readF32() {
596
+ if (this.stopped()) {
597
+ return 0;
598
+ }
599
+ this.alignTo(4);
600
+ const from = this.take(4);
601
+ return new DataView(this.bytes.buffer, this.bytes.byteOffset + from, 4).getFloat32(0, isLittle(this.order));
602
+ }
603
+ /** `double`, a binary64. */
604
+ readF64() {
605
+ if (this.stopped()) {
606
+ return 0;
607
+ }
608
+ this.alignTo(8);
609
+ const from = this.take(8);
610
+ return new DataView(this.bytes.buffer, this.bytes.byteOffset + from, 8).getFloat64(0, isLittle(this.order));
611
+ }
612
+ /**
613
+ * `long double`: all sixteen octets, as the writer's counterpart holds
614
+ * them.
615
+ *
616
+ * The octets come back in the order a little-endian stream carries
617
+ * them, which is what {@link Writer.writeF128} takes; a big-endian
618
+ * stream's are reversed on the way in. Reading the first eight as a
619
+ * `double` and skipping the rest would decode every value a binary64
620
+ * cannot hold as a different number and say nothing.
621
+ */
622
+ readF128() {
623
+ if (this.stopped()) {
624
+ return { octets: new Uint8Array(16) };
625
+ }
626
+ this.alignTo(8);
627
+ const from = this.take(16);
628
+ const octets = this.bytes.slice(from, from + 16);
629
+ if (!isLittle(this.order)) {
630
+ octets.reverse();
631
+ }
632
+ return { octets };
633
+ }
634
+ /** A 16-bit value little-endian whatever the stream's order is. */
635
+ readU16Little() {
636
+ if (this.stopped()) {
637
+ return 0;
638
+ }
639
+ this.alignTo(2);
640
+ return Number(this.rawInt(2, false, true));
641
+ }
642
+ /** A 16-bit value big-endian whatever the stream's order is. */
643
+ readU16Big() {
644
+ if (this.stopped()) {
645
+ return 0;
646
+ }
647
+ this.alignTo(2);
648
+ return Number(this.rawInt(2, false, false));
649
+ }
650
+ /** A 32-bit value little-endian whatever the stream's order is. */
651
+ readU32Little() {
652
+ if (this.stopped()) {
653
+ return 0;
654
+ }
655
+ this.alignTo(4);
656
+ return Number(this.rawInt(4, false, true));
657
+ }
658
+ /** A 32-bit value big-endian whatever the stream's order is. */
659
+ readU32Big() {
660
+ if (this.stopped()) {
661
+ return 0;
662
+ }
663
+ this.alignTo(4);
664
+ return Number(this.rawInt(4, false, false));
665
+ }
666
+ // -- strings ---------------------------------------------------------
667
+ /**
668
+ * `string`: a four-octet length that includes the terminator, then the
669
+ * octets, then a NUL. §7.4.1.1.1.
670
+ *
671
+ * A declared length of zero is impossible rather than empty: the count
672
+ * includes the terminator, so the shortest honest frame states one.
673
+ * A length past the end is {@link ImpossibleLength} and not
674
+ * {@link Truncated} — the stream is not short, the field is lying, and
675
+ * a reader that allocated for it first would be the vector.
676
+ */
677
+ readString() {
678
+ if (this.stopped()) {
679
+ return "";
680
+ }
681
+ const where = this.at;
682
+ const length = this.readU32();
683
+ if (length === 0 || length > this.remaining() + 1) {
684
+ throw new ImpossibleLength(where, length, this.remaining());
685
+ }
686
+ const from = this.take(length - 1);
687
+ const text = this.decodeUtf8(from, length - 1, where);
688
+ this.readU8();
689
+ return text;
690
+ }
691
+ /**
692
+ * A `string` the reader materialises itself.
693
+ *
694
+ * `runtime/c` takes it out of a heap the caller set, because an
695
+ * embedded target decides where memory comes from. TypeScript has one
696
+ * answer to that question and this is it; the name is kept so that one
697
+ * walk names one operation in both columns.
698
+ */
699
+ readStringAlloc() {
700
+ return this.readString();
701
+ }
702
+ /**
703
+ * A `wstring`, in whichever length convention the policy names.
704
+ *
705
+ * `runtime/c` converts to UTF-8 because that is what its
706
+ * `const char *` holds, and its name carries it. A TypeScript `string`
707
+ * is UTF-16 already and {@link readWstring} is the whole of the
708
+ * operation.
709
+ */
710
+ readWstringUtf8(policy, order) {
711
+ return this.readWstring(policy, order);
712
+ }
713
+ /**
714
+ * The inverse of `Writer.writeWstringUtf8Octets`.
715
+ *
716
+ * `WstringCodeUnits::Utf8`: the declared length is the UTF-8 octet count,
717
+ * and the terminator `policy` asks for is the last of those octets. A count
718
+ * the stream cannot hold, a missing or an interior NUL, or an ill-formed
719
+ * UTF-8 sequence is refused, never a replacement character or a truncated
720
+ * value.
721
+ */
722
+ readWstringUtf8Octets(policy) {
723
+ if (this.stopped()) {
724
+ return "";
725
+ }
726
+ const where = this.at;
727
+ const cell = WSTRING_CELLS.get(policy);
728
+ if (cell === undefined) {
729
+ throw new OutOfRange(where, `a wstring length policy, not \`${policy}\``);
730
+ }
731
+ const declared = this.readU32();
732
+ if (declared > this.remaining()) {
733
+ throw new ImpossibleLength(where, declared, this.remaining());
734
+ }
735
+ const from = this.take(declared);
736
+ let count = declared;
737
+ if (cell.terminated) {
738
+ if (count === 0 || this.bytes[from + count - 1] !== 0) {
739
+ throw new OutOfRange(where, "a NUL octet ending the wstring, as its count includes one");
740
+ }
741
+ // A NUL inside the text is the narrow string's problem in wide form:
742
+ // every C peer stops at it.
743
+ for (let index = 0; index < count - 1; index += 1) {
744
+ if (this.bytes[from + index] === 0) {
745
+ throw new OutOfRange(where, "no NUL inside a wstring a C peer would stop at");
746
+ }
747
+ }
748
+ count -= 1;
749
+ }
750
+ return this.decodeUtf8(from, count, where);
751
+ }
752
+ /**
753
+ * A `wstring`, in the form `policy` and `order` name.
754
+ *
755
+ * Under {@link WstringLengthPolicy.Specification} the count is octets
756
+ * and not characters — §7.4.1.1.2 — so a count the code-unit width
757
+ * does not divide is a stream no encoder produced and is refused
758
+ * rather than rounded down: rounding would leave the remainder in the
759
+ * stream and put every later member at the wrong offset.
760
+ *
761
+ * The form is never inferred from the stream. The cells are not
762
+ * separable: a Fast DDS `"ab"` followed by a `long` is byte-identical
763
+ * to a specification `"a"` followed by the same `long`, differing only
764
+ * in padding no clause fixes. Choosing between them would mean letting
765
+ * the length field — the one field a hostile peer controls — decide
766
+ * how much is read.
767
+ */
768
+ readWstring(policy, order) {
769
+ if (this.stopped()) {
770
+ return "";
771
+ }
772
+ const where = this.at;
773
+ const cell = WSTRING_CELLS.get(policy);
774
+ if (cell === undefined) {
775
+ throw new OutOfRange(where, `a wstring length policy, not \`${policy}\``);
776
+ }
777
+ const declared = this.readU32();
778
+ let count;
779
+ if (cell.countsCharacters) {
780
+ count = declared;
781
+ }
782
+ else {
783
+ if (declared % cell.unitWidth !== 0) {
784
+ throw new ImpossibleLength(where, declared, this.remaining());
785
+ }
786
+ count = declared / cell.unitWidth;
787
+ }
788
+ let units = this.readWstringUnits(where, count, cell.unitWidth, order);
789
+ if (cell.terminated) {
790
+ // The count includes the NUL, so a count of zero is a frame that
791
+ // does not hold its own terminator.
792
+ if (declared < 1 || units.length === 0) {
793
+ throw new ImpossibleLength(where, declared, this.remaining());
794
+ }
795
+ if (units[units.length - 1] !== 0) {
796
+ throw new OutOfRange(where, "a NUL character ending the wstring, as its count includes one");
797
+ }
798
+ units = units.slice(0, units.length - 1);
799
+ // A NUL inside the text is the narrow string's problem in wide
800
+ // form: every C peer stops at it. Only where a terminator is
801
+ // expected — a cell without one is entitled to carry U+0000 as a
802
+ // character.
803
+ if (units.includes(0)) {
804
+ throw new OutOfRange(where, "no NUL inside a wstring a C peer would stop at");
805
+ }
806
+ }
807
+ if (order === WstringUnitOrder.ByteOrderMark) {
808
+ units = stripByteOrderMark(units);
809
+ }
810
+ let out = "";
811
+ let index = 0;
812
+ while (index < units.length) {
813
+ const unit = units[index] ?? 0;
814
+ if (unit >= 0xd800 && unit <= 0xdbff && index + 1 < units.length) {
815
+ const low = units[index + 1] ?? 0;
816
+ if (low >= 0xdc00 && low <= 0xdfff) {
817
+ out += String.fromCodePoint(0x10000 + ((unit - 0xd800) << 10) + (low - 0xdc00));
818
+ index += 2;
819
+ continue;
820
+ }
821
+ }
822
+ if (unit >= 0xd800 && unit <= 0xdfff) {
823
+ throw new OutOfRange(where, "a UTF-16 sequence with no lone surrogate");
824
+ }
825
+ out += String.fromCharCode(unit);
826
+ index += 1;
827
+ }
828
+ return out;
829
+ }
830
+ /**
831
+ * `count` `wstring` characters of `width` octets, as UTF-16 units.
832
+ *
833
+ * The whole run is checked against what is left **before** the first
834
+ * unit is read, because a count is what a hostile peer controls: the
835
+ * check is the difference between refusing a frame and reading 4·2^32
836
+ * octets of whatever follows.
837
+ *
838
+ * The four-octet width is RTI's XCDR1 form, where a character is a
839
+ * code unit zero-extended. A value above `0xFFFF` there is not a form
840
+ * this reader knows how to narrow, so it is refused rather than
841
+ * truncated — truncating is the defect this whole policy exists
842
+ * because of.
843
+ */
844
+ readWstringUnits(where, count, width, order) {
845
+ if (count < 0 || count * width > this.remaining()) {
846
+ throw new ImpossibleLength(where, count * width, this.remaining());
847
+ }
848
+ const little = readsLittleAlways(order);
849
+ const units = [];
850
+ for (let index = 0; index < count; index += 1) {
851
+ let unit;
852
+ if (width === 4) {
853
+ if (order === WstringUnitOrder.BigEndianAlways) {
854
+ unit = this.readU32Big();
855
+ }
856
+ else if (little) {
857
+ unit = this.readU32Little();
858
+ }
859
+ else {
860
+ unit = this.readU32();
861
+ }
862
+ if (unit > 0xffff) {
863
+ throw new OutOfRange(where, "a four-octet wstring character holding one UTF-16 unit");
864
+ }
865
+ }
866
+ else if (order === WstringUnitOrder.BigEndianAlways) {
867
+ unit = this.readU16Big();
868
+ }
869
+ else if (little) {
870
+ unit = this.readU16Little();
871
+ }
872
+ else {
873
+ unit = this.readU16();
874
+ }
875
+ units.push(unit);
876
+ }
877
+ return units;
878
+ }
879
+ /**
880
+ * `text` cut to at most `octets` UTF-8 octets, never inside a
881
+ * character.
882
+ *
883
+ * Table 11: "The order of the characters in the string ... is
884
+ * preserved", so this is a prefix. A character that would straddle the
885
+ * bound is left out whole: a peer holding `char[octets + 1]` cannot
886
+ * store half of one either, and half a UTF-8 sequence is not text.
887
+ */
888
+ trimUtf8(text, octets) {
889
+ if (octets < 0) {
890
+ return "";
891
+ }
892
+ const encoder = new TextEncoder();
893
+ let out = "";
894
+ let used = 0;
895
+ for (const character of text) {
896
+ const width = encoder.encode(character).length;
897
+ if (used + width > octets) {
898
+ break;
899
+ }
900
+ out += character;
901
+ used += width;
902
+ }
903
+ return out;
904
+ }
905
+ /**
906
+ * `text` cut to at most `units` UTF-16 code units, never between the
907
+ * halves of a surrogate pair.
908
+ */
909
+ trimUtf16(text, units) {
910
+ if (units < 0) {
911
+ return "";
912
+ }
913
+ if (text.length <= units) {
914
+ return text;
915
+ }
916
+ let end = units;
917
+ // A high surrogate at the cut has lost its pair, and half a pair is
918
+ // not a character.
919
+ const last = text.charCodeAt(end - 1);
920
+ if (end > 0 && last >= 0xd800 && last <= 0xdbff) {
921
+ end -= 1;
922
+ }
923
+ return text.slice(0, end);
924
+ }
925
+ /**
926
+ * `fixed<digits, scale>`, as the decimal string the writer took.
927
+ *
928
+ * The mirror of `Writer.writeFixed`: `ceil((digits + 1) / 2)` octets
929
+ * of BCD, digits most-significant first, sign nibble last.
930
+ *
931
+ * The leading zeros go, because the BCD field is `digits` wide
932
+ * whatever the value: the nibbles for `0.01` in a `fixed<5, 2>` spell
933
+ * `00001`, and returning `"000.01"` would put a decimal on the read
934
+ * side that the write side would never produce from the same value.
935
+ * A negative zero loses its sign for the same reason: `-0.00` and
936
+ * `0.00` are one value, and the sign nibble is the only place they
937
+ * differ.
938
+ *
939
+ * Under {@link FixedDigitCount.ValuePrecision} the field's end is read
940
+ * out of the octets: [CDR] §9.3.2.8 makes a digit `0` to `9` and a
941
+ * sign `0xa` upwards, and the nibble count is even by construction, so
942
+ * the field ends at the first octet whose **low** nibble is a sign. A
943
+ * field that never presents one within the sixteen octets of the
944
+ * widest `fixed` IDL admits is refused.
945
+ */
946
+ readFixed(digits, scale, count) {
947
+ if (this.stopped()) {
948
+ return "";
949
+ }
950
+ // The same bound as the writer's, on this side too: OMG IDL 4.2 rule
951
+ // (42) caps the BCD rule at 31 digits, and a reader asked for a wider
952
+ // field would take octets no declaration can name.
953
+ if (digits <= 0 || digits > 31 || scale > digits) {
954
+ throw new OutOfRange(this.at, `a fixed<P, S> with S <= P and 1 <= P <= 31, not fixed<${digits}, ${scale}>`);
955
+ }
956
+ let nibbles = [];
957
+ let sign;
958
+ if (count === FixedDigitCount.ValuePrecision ||
959
+ count === FixedDigitCount.ValueStringDigits) {
960
+ for (;;) {
961
+ if (nibbles.length >= MAX_FIXED_NIBBLES) {
962
+ throw new OutOfRange(this.at, "a sign nibble within sixteen octets");
963
+ }
964
+ const octet = this.readU8();
965
+ nibbles.push(octet >> 4);
966
+ nibbles.push(octet & 0x0f);
967
+ if ((octet & 0x0f) >= FIRST_SIGN_NIBBLE) {
968
+ break;
969
+ }
970
+ }
971
+ sign = nibbles.pop() ?? 0;
972
+ }
973
+ else if (count === FixedDigitCount.Declared) {
974
+ const octets = Math.floor((digits + 2) / 2);
975
+ const from = this.take(octets);
976
+ for (let index = 0; index < octets; index += 1) {
977
+ const octet = this.bytes[from + index] ?? 0;
978
+ nibbles.push(octet >> 4);
979
+ nibbles.push(octet & 0x0f);
980
+ }
981
+ sign = nibbles.pop() ?? 0;
982
+ // An even declared digit count leaves the field one nibble wider
983
+ // than the value, and the pad is at the front.
984
+ if ((digits + 1) % 2 === 1) {
985
+ nibbles = nibbles.slice(1);
986
+ }
987
+ }
988
+ else {
989
+ throw new OutOfRange(this.at, `a fixed digit count, not \`${count}\``);
990
+ }
991
+ let text = "";
992
+ for (const nibble of nibbles) {
993
+ if (nibble > 9) {
994
+ throw new OutOfRange(this.at, "BCD digits");
995
+ }
996
+ text += String.fromCharCode(0x30 + nibble);
997
+ }
998
+ // The point sits `scale` digits from the right and not
999
+ // `digits - scale` from the left: under the value's count the field
1000
+ // carries fewer than `digits`, and one that carries fewer than the
1001
+ // scale — `1c` under `fixed<5,2>` — needs its integer part written
1002
+ // as a zero.
1003
+ text = text.padStart(scale + 1, "0");
1004
+ const split = text.length - scale;
1005
+ const whole = text.slice(0, split).replace(/^0+(?=.)/, "");
1006
+ const fraction = text.slice(split);
1007
+ const out = scale === 0 ? whole : `${whole}.${fraction}`;
1008
+ const zero = whole === "0" && !/[1-9]/.test(fraction);
1009
+ return sign === MINUS_NIBBLE && !zero ? `-${out}` : out;
1010
+ }
1011
+ // -- the bit-bounded kinds -------------------------------------------
1012
+ /**
1013
+ * An enumerator's holder, **sign-extended from its holder's width**.
1014
+ *
1015
+ * §7.4.3.5.3 rule (5) makes the holder signed: `@value(200)` under a
1016
+ * bound of eight goes out as `c8`, and that octet read as the type
1017
+ * says is `-56`. `@value(-1)` and `@value(255)` are the same octet and
1018
+ * nothing in the stream separates them — which one was meant is a
1019
+ * question about the *type*, and the generated decoder answers it by
1020
+ * comparing this against each declared enumerator's own pattern, taken
1021
+ * the same way.
1022
+ *
1023
+ * The Java column had this unsigned while its walk compared against
1024
+ * the signed pattern, and every `@bit_bound(8)` enumerator above 127
1025
+ * matched no arm at all — the sample came back discarded rather than
1026
+ * wrong, which is why it took a corpus run to find. The sign extension
1027
+ * is from the *holder's* width and not from the bound: `@bit_bound(5)`
1028
+ * still occupies an octet, and rule (5) makes the octet signed.
1029
+ */
1030
+ readEnum(bitBound) {
1031
+ if (this.stopped()) {
1032
+ return 0n;
1033
+ }
1034
+ if (bitBound <= 0 || bitBound > 32) {
1035
+ throw new OutOfRange(this.at, `an @bit_bound between 1 and 32, not ${bitBound}`);
1036
+ }
1037
+ const width = bitBound <= 8 ? 8 : bitBound <= 16 ? 16 : 32;
1038
+ const pattern = width === 8
1039
+ ? BigInt(this.readU8())
1040
+ : width === 16
1041
+ ? BigInt(this.readU16())
1042
+ : BigInt(this.readU32());
1043
+ const sign = 1n << BigInt(width - 1);
1044
+ return (pattern & sign) === 0n ? pattern : pattern - (1n << BigInt(width));
1045
+ }
1046
+ /** A bitmask's holder. Table 33 of §7.4.1.1.3.2. */
1047
+ readBitmask(bitBound) {
1048
+ if (this.stopped()) {
1049
+ return 0n;
1050
+ }
1051
+ if (bitBound <= 0 || bitBound > 64) {
1052
+ throw new OutOfRange(this.at, `an @bit_bound between 1 and 64, not ${bitBound}`);
1053
+ }
1054
+ if (bitBound <= 8) {
1055
+ return BigInt(this.readU8());
1056
+ }
1057
+ if (bitBound <= 16) {
1058
+ return BigInt(this.readU16());
1059
+ }
1060
+ if (bitBound <= 32) {
1061
+ return BigInt(this.readU32());
1062
+ }
1063
+ return this.readU64();
1064
+ }
1065
+ /** A bitset's anchor integer, of the width the planner sized. */
1066
+ readBitset(anchorOctets) {
1067
+ if (this.stopped()) {
1068
+ return 0n;
1069
+ }
1070
+ switch (anchorOctets) {
1071
+ case 1:
1072
+ return BigInt(this.readU8());
1073
+ case 2:
1074
+ return BigInt(this.readU16());
1075
+ case 4:
1076
+ return BigInt(this.readU32());
1077
+ case 8:
1078
+ return this.readU64();
1079
+ default:
1080
+ throw new OutOfRange(this.at, `a bitset anchor of 1, 2, 4 or 8 octets, not ${anchorOctets}`);
1081
+ }
1082
+ }
1083
+ // -- framing ---------------------------------------------------------
1084
+ /**
1085
+ * A sequence's element count, refusing one the stream cannot hold.
1086
+ *
1087
+ * An element is one octet or more, so a stream with fewer octets left
1088
+ * than elements claimed cannot be honest about it. Allocating for it
1089
+ * is how a reader turns a bad message into an out-of-memory — and in
1090
+ * this column `new Array(n)` for a claimed 4294967295 is exactly that.
1091
+ */
1092
+ readSequenceLength() {
1093
+ if (this.stopped()) {
1094
+ return 0;
1095
+ }
1096
+ const where = this.at;
1097
+ const count = this.readU32();
1098
+ if (count > this.remaining()) {
1099
+ throw new ImpossibleLength(where, count, this.remaining());
1100
+ }
1101
+ return count;
1102
+ }
1103
+ /** A DHEADER: the length the members that follow occupy. §7.4.3.5.3. */
1104
+ readDheader() {
1105
+ if (this.stopped()) {
1106
+ return 0;
1107
+ }
1108
+ const where = this.at;
1109
+ const length = this.readU32();
1110
+ if (length > this.remaining()) {
1111
+ throw new ImpossibleLength(where, length, this.remaining());
1112
+ }
1113
+ return length;
1114
+ }
1115
+ /**
1116
+ * An EMHEADER, unpacked into its parts and the NEXTINT it carries.
1117
+ *
1118
+ * Codes 5 to 7 report the NEXTINT they read, but the reader is put
1119
+ * back before it: §7.4.3.4.2 says that word "shall be reused also as
1120
+ * part of the serialized member", so it is the member's own leading
1121
+ * length and the member's decoder has to see it.
1122
+ */
1123
+ readEmheader() {
1124
+ if (this.stopped()) {
1125
+ return emHeader(0, 0, false, 0, false);
1126
+ }
1127
+ const word = this.readU32();
1128
+ const lengthCode = (word >>> 28) & 0x7;
1129
+ const memberId = word & 0x0fff_ffff;
1130
+ const mustUnderstand = word >>> 31 !== 0;
1131
+ if (lengthCode === 4) {
1132
+ return emHeader(memberId, lengthCode, mustUnderstand, this.readU32(), true);
1133
+ }
1134
+ if (lengthCode >= 5) {
1135
+ const where = this.at;
1136
+ const peeked = this.readU32();
1137
+ this.at = where;
1138
+ return emHeader(memberId, lengthCode, mustUnderstand, peeked, true);
1139
+ }
1140
+ return emHeader(memberId, lengthCode, mustUnderstand, 0, false);
1141
+ }
1142
+ /** A PL_CDR1 parameter header in the {@link ParameterRole.Member} role. */
1143
+ readPid() {
1144
+ return this.readPidAs(ParameterRole.Member);
1145
+ }
1146
+ /**
1147
+ * A PL_CDR1 parameter header, in whichever form it was written.
1148
+ *
1149
+ * `role` is the caller's, not the stream's: the octets are the same
1150
+ * octets and only what the declared length means differs. §7.4.1.2.
1151
+ */
1152
+ readPidAs(role) {
1153
+ if (this.stopped()) {
1154
+ return {
1155
+ pid: 0,
1156
+ length: 0,
1157
+ end: true,
1158
+ mustUnderstand: false,
1159
+ implExtension: false,
1160
+ reserved: false,
1161
+ ignore: false,
1162
+ role,
1163
+ savedOrigin: this.origin,
1164
+ valueAt: this.at,
1165
+ };
1166
+ }
1167
+ this.alignTo(4);
1168
+ const pid = this.readU16();
1169
+ const length = this.readU16();
1170
+ if ((pid & PID_ID_MASK) === (PID_EXTENDED & PID_ID_MASK)) {
1171
+ // The four flag bits are not part of the id: §7.4.1.2.1 splits the
1172
+ // `eMemberHeader` into `FLAG_1` to `FLAG_4` and a 28-bit member
1173
+ // id, and `FLAG_2` is the member's must-understand. Taking the
1174
+ // whole word gave a Cyclone member declared `@must_understand` an
1175
+ // id of `0x40000001`, which matches nothing, and the member was
1176
+ // dropped.
1177
+ const memberHeader = this.readU32();
1178
+ const wideLength = this.readU32();
1179
+ // The parameter's value is its own stream block: §7.4.1.2 counts a
1180
+ // member's internal alignment from the first octet after the
1181
+ // header, not from the start of the message.
1182
+ const saved = this.origin;
1183
+ this.origin = this.at;
1184
+ return {
1185
+ pid: memberHeader & E_MEMBER_ID_MASK,
1186
+ length: wideLength,
1187
+ end: false,
1188
+ mustUnderstand: (memberHeader & E_FLAG_MUST_UNDERSTAND) !== 0,
1189
+ implExtension: (memberHeader & E_FLAG_IMPL_EXTENSION) !== 0,
1190
+ // The extended id is 28 bits and has no reserved range.
1191
+ reserved: false,
1192
+ ignore: false,
1193
+ role,
1194
+ savedOrigin: saved,
1195
+ valueAt: this.at,
1196
+ };
1197
+ }
1198
+ const identifier = pid & PID_ID_MASK;
1199
+ const end = identifier === (PID_LIST_END & PID_ID_MASK);
1200
+ // Under a framing that puts `FLAG_IMPL_EXTENSION` on every member id
1201
+ // the bit no longer tells a vendor-private parameter from a user
1202
+ // member — OpenDDS writes `c0010004` for `@id(1) long a`. It is
1203
+ // reported absent there because under that framing it carries no
1204
+ // information, and reporting it would send every member down the
1205
+ // unknown-parameter path.
1206
+ const flagged = (pid & PID_IMPL_EXTENSION) !== 0 &&
1207
+ (pidFlagsAt(this.plCdr1.pidFlags, "member") & PID_IMPL_EXTENSION) === 0;
1208
+ const saved = this.origin;
1209
+ this.origin = this.at;
1210
+ return {
1211
+ pid: identifier,
1212
+ length,
1213
+ end,
1214
+ mustUnderstand: (pid & PID_MUST_UNDERSTAND) !== 0,
1215
+ implExtension: flagged,
1216
+ reserved: !end && identifier > this.plCdr1.shortMaxId,
1217
+ ignore: identifier === PID_IGNORE,
1218
+ role,
1219
+ savedOrigin: saved,
1220
+ valueAt: this.at,
1221
+ };
1222
+ }
1223
+ /**
1224
+ * End a parameter's value and step to the next header.
1225
+ *
1226
+ * The padding to four follows the value and is outside the declared
1227
+ * length, so the next parameter is found by alignment and not by
1228
+ * adding the length — which is what lets one reader take RTI's
1229
+ * rounded-up length and Fast DDS's exact one.
1230
+ *
1231
+ * The declared length is checked against the configured framing rather
1232
+ * than believed: a length belonging to the other convention is a
1233
+ * stream this reader was pointed at with the wrong framing, and
1234
+ * reading on would put every later member at an offset no writer
1235
+ * produced. Both readings are accepted where they coincide, which is
1236
+ * every value already a multiple of four.
1237
+ */
1238
+ finishPidRead(header) {
1239
+ this.origin = header.savedOrigin;
1240
+ if (this.stopped() || header.end) {
1241
+ return;
1242
+ }
1243
+ const used = this.at - header.valueAt;
1244
+ const padded = (used + 3) & ~3;
1245
+ const pads = header.role === ParameterRole.Optional
1246
+ ? this.plCdr1.padsOptionalLength
1247
+ : this.plCdr1.padsDeclaredLength;
1248
+ if (header.length !== (pads ? padded : used) &&
1249
+ header.length !== used &&
1250
+ header.length !== padded) {
1251
+ this.readerRefuse(Status.OutOfRange);
1252
+ }
1253
+ this.alignTo(4);
1254
+ }
1255
+ /**
1256
+ * Step over a member this reader does not know.
1257
+ *
1258
+ * The whole point of the length code, and the reason §7.4.3.4.2 gives
1259
+ * one to every mutable member: a reader of an older type has to be
1260
+ * able to walk past what it does not have a field for.
1261
+ */
1262
+ skipMember(header) {
1263
+ if (this.stopped()) {
1264
+ return;
1265
+ }
1266
+ if (header.lengthCode > 3 && !header.hasNextInt) {
1267
+ throw new OutOfRange(this.at, "a NEXTINT for a length code of 4 or above");
1268
+ }
1269
+ this.skip(header.extent);
1270
+ }
1271
+ /**
1272
+ * Start reading a parameter's value, answering with the origin it
1273
+ * replaces. The reader's half of the writer's parameter block.
1274
+ */
1275
+ beginParameterValue() {
1276
+ const saved = this.origin;
1277
+ this.origin = this.at;
1278
+ return saved;
1279
+ }
1280
+ /** End a parameter's value and step over its trailing padding. */
1281
+ finishParameterValue(savedOrigin) {
1282
+ this.origin = savedOrigin;
1283
+ this.alignTo(4);
1284
+ }
1285
+ // -- text -------------------------------------------------------------
1286
+ /**
1287
+ * `length` octets from `from` as UTF-8, refusing anything that is not.
1288
+ *
1289
+ * `fatal: true` is the whole of it: without it `TextDecoder` replaces
1290
+ * every malformed sequence with U+FFFD and answers a string that is
1291
+ * the right length and the wrong text, which is precisely the silent
1292
+ * substitution {@link NotUtf8} exists to prevent.
1293
+ */
1294
+ decodeUtf8(from, length, where) {
1295
+ try {
1296
+ return new TextDecoder("utf-8", { fatal: true }).decode(this.bytes.subarray(from, from + length));
1297
+ }
1298
+ catch {
1299
+ throw new NotUtf8(where);
1300
+ }
1301
+ }
1302
+ }
1303
+ /** An {@link EmHeaderRead} with its {@link EmHeaderRead.extent} worked out. */
1304
+ function emHeader(memberId, lengthCode, mustUnderstand, nextInt, hasNextInt) {
1305
+ let extent;
1306
+ if (lengthCode <= 3) {
1307
+ extent = 1 << lengthCode;
1308
+ }
1309
+ else if (lengthCode === 4) {
1310
+ extent = nextInt;
1311
+ }
1312
+ else {
1313
+ const scale = lengthCode === 5 ? 1 : lengthCode === 6 ? 4 : 8;
1314
+ extent = 4 + nextInt * scale;
1315
+ }
1316
+ return { memberId, lengthCode, mustUnderstand, nextInt, hasNextInt, extent };
1317
+ }
1318
+ /**
1319
+ * Take the byte-order mark off the front and apply what it said.
1320
+ *
1321
+ * The units arrived little-endian, because that is what omniORB writes
1322
+ * on a little-endian host and what the `byte-order-mark` order therefore
1323
+ * reads. Three cases, and the third is the one that matters:
1324
+ *
1325
+ * - `FEFF` — the mark is the right way up, the units are as read.
1326
+ * - `FFFE` — the mark is byte-swapped, so every unit behind it is too,
1327
+ * and each one is swapped back. This is what a conforming big-endian
1328
+ * omniORB peer produces and what a reader that ignored the mark would
1329
+ * return as different characters, well formed and the right length.
1330
+ * - no mark — CORBA's rule for a GIOP 1.2 `wstring` without one is
1331
+ * UTF-16 big-endian, so every unit read little-endian is swapped.
1332
+ *
1333
+ * The mark is never a character of the returned string in any of the
1334
+ * three, which is the half §7.4.1.1.2 cares about.
1335
+ */
1336
+ function stripByteOrderMark(units) {
1337
+ if (units.length === 0) {
1338
+ return units;
1339
+ }
1340
+ if (units[0] === BYTE_ORDER_MARK) {
1341
+ return units.slice(1);
1342
+ }
1343
+ const rest = units[0] === SWAPPED_BYTE_ORDER_MARK ? units.slice(1) : units;
1344
+ return rest.map((unit) => ((unit & 0xff) << 8) | (unit >> 8));
1345
+ }