@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/writer.js ADDED
@@ -0,0 +1,1333 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2026 ZeroDDS Contributors
3
+ /**
4
+ * The write half of XCDR, implemented from DDS-XTypes 1.3 §7.4.
5
+ *
6
+ * The fifth independent runtime beside `crates/wire`, `runtime/c`,
7
+ * `runtime/cpp`, `runtime/java` and `runtime/python`. None of them
8
+ * calls another, and that is the point: a difference between two of
9
+ * them is a finding about the clause, not about a shared bug.
10
+ *
11
+ * What TypeScript decides differently from the others:
12
+ *
13
+ * - a `number` is an IEEE 754 binary64, so every integer narrower than
14
+ * 2^53 is one and everything wider is a `bigint`. The boundary is the
15
+ * type's, not the wire's — `long long` is a `bigint` because a
16
+ * binary64 cannot hold every `i64`, and `long` is a `number` because
17
+ * it can hold every `i32`;
18
+ * - a range check happens at the point of the write. In Rust an `i16`
19
+ * is an `i16` and the compiler has refused the value already; here
20
+ * `70000` is as much a `number` as `7` is, and `& 0xffff` would put
21
+ * `4464` on the wire and report success;
22
+ * - a `long double` never becomes a `number`. Sixteen octets in and
23
+ * sixteen octets out; a binary64 would drop sixty bits of the
24
+ * significand at the assignment.
25
+ */
26
+ import { isLittle } from "./endianness.js";
27
+ import { OutOfRange } from "./error.js";
28
+ import { FixedDigitCount, ParameterRole, WcharFraming, WcharWidth, WstringLengthPolicy, WstringUnitOrder, wstringCountsCharacters, wstringIsTerminated, wstringOrderIsFixedLittle, wstringUnitWidth, } from "./policy.js";
29
+ /** The parameter id that introduces the extended form. §7.4.1.2.2. */
30
+ export const PID_EXTENDED = 0x3f01;
31
+ /** The parameter id that ends a parameter list. §7.4.1.2.4. */
32
+ export const PID_LIST_END = 0x3f02;
33
+ /**
34
+ * The highest member id a short parameter header can carry.
35
+ *
36
+ * §7.4.1.2.2 gives the range as 0 to 0x3F00 *inclusive*, and
37
+ * PID_EXTENDED is one above it.
38
+ */
39
+ export const PID_SHORT_MAX_ID = 0x3f00;
40
+ /** The implementation-extension flag of a parameter id. */
41
+ export const PID_IMPL_EXTENSION = 0x8000;
42
+ /** The must-understand flag of a parameter id. */
43
+ export const PID_MUST_UNDERSTAND = 0x4000;
44
+ /** `FLAG_2` of an `eMemberHeader`. §7.4.1.2.1. */
45
+ const E_FLAG_MUST_UNDERSTAND = 0x4000_0000;
46
+ /** The member id an `eMemberHeader` carries. §7.4.1.2.1. */
47
+ const E_MEMBER_ID_MASK = 0x0fff_ffff;
48
+ /** How many octets a KeyHash holder has. RTPS 2.5 §9.6.3.3. */
49
+ export const KEY_HASH_LEN = 16;
50
+ /**
51
+ * The longest value a short parameter header's `ssize` can declare.
52
+ *
53
+ * §7.4.1.2.2's `ssize` is sixteen bits. Which values overrun it is a
54
+ * property of the *sample* and not of the type — a `string` member is
55
+ * short in almost every sample and over 64 KiB in some — so the layout
56
+ * plan writes the short form and {@link Writer.finishPid} promotes it
57
+ * where the length turns out not to fit.
58
+ */
59
+ const PID_SHORT_MAX_LEN = 0xffff;
60
+ /** The code unit a byte order mark is. Unicode 15 §23.8. */
61
+ const BYTE_ORDER_MARK = 0xfeff;
62
+ /** No flags anywhere, which is Fast DDS's framing. */
63
+ export const PID_FLAGS_NONE = {
64
+ member: 0,
65
+ sentinel: 0,
66
+ extendedMarker: 0,
67
+ };
68
+ /** The name `PidFlagPolicy::Bare` gives {@link PID_FLAGS_NONE}. */
69
+ export const PID_FLAGS_BARE = PID_FLAGS_NONE;
70
+ /**
71
+ * A bare member id, the must-understand bit on the sentinel and on the
72
+ * extended marker. Cyclone DDS and RTI Connext 7.7. The default.
73
+ *
74
+ * Measured, on `struct-mutable` and `annotation-hashid-implicit` under
75
+ * XCDR1: both write `7f02` for the sentinel and `7f01` for the extended
76
+ * marker — the must-understand bit alone. The implementation-extension
77
+ * bit is **not** on either of them, and a runtime that also set it wrote
78
+ * `ff02` and `ff01`, which is what OpenDDS writes for the marker and
79
+ * nobody writes for the sentinel.
80
+ */
81
+ export const PID_FLAGS_SENTINEL_MUST_UNDERSTAND = {
82
+ member: 0,
83
+ sentinel: PID_MUST_UNDERSTAND,
84
+ extendedMarker: PID_MUST_UNDERSTAND,
85
+ };
86
+ /**
87
+ * Both flags on every member id whatever the member declared, a bare
88
+ * sentinel, and both on the extended marker. OpenDDS.
89
+ *
90
+ * `annotation-hashid-implicit` is `ff010008 0343172c 00000004`: both
91
+ * flags on the marker, and a **bare** `eMemberHeader` behind it for a
92
+ * member that declares nothing.
93
+ */
94
+ export const PID_FLAGS_IMPL_EXTENSION_ON_MEMBERS = {
95
+ member: PID_MUST_UNDERSTAND | PID_IMPL_EXTENSION,
96
+ sentinel: 0,
97
+ extendedMarker: PID_MUST_UNDERSTAND | PID_IMPL_EXTENSION,
98
+ };
99
+ /** The flags a framing puts on a parameter in `role`. */
100
+ export function pidFlagsAt(flags, role) {
101
+ switch (role) {
102
+ case "member":
103
+ return flags.member;
104
+ case "sentinel":
105
+ return flags.sentinel;
106
+ case "extended-marker":
107
+ return flags.extendedMarker;
108
+ }
109
+ }
110
+ /** The framing every field of which is the specification's reading. */
111
+ export const PL_CDR1_DEFAULT = {
112
+ pidFlags: PID_FLAGS_NONE,
113
+ shortMaxId: PID_SHORT_MAX_ID,
114
+ padsDeclaredLength: false,
115
+ padsOptionalLength: false,
116
+ memberMustUnderstand: true,
117
+ padAfterValue: true,
118
+ };
119
+ /** The DHEADER a `@final` aggregate does not write. */
120
+ export function noDheader() {
121
+ return { at: -1 };
122
+ }
123
+ export class Writer {
124
+ order;
125
+ plCdr1;
126
+ shortLengthCodes;
127
+ out = [];
128
+ origin = 0;
129
+ maxAlignment;
130
+ /**
131
+ * A writer in `order` whose alignment is capped at `maxAlign`.
132
+ *
133
+ * Four for XCDR2, eight for XCDR1. §7.4.1: `ALIGN(N)` aligns to
134
+ * `MIN(N, XCDR.maxalign)`. A writer that did not know this padded an
135
+ * eight-octet primitive to eight under XCDR2 and put four octets on
136
+ * the wire that no peer expects.
137
+ */
138
+ constructor(order, maxAlign = 8, plCdr1 = {
139
+ pidFlags: PID_FLAGS_NONE,
140
+ shortMaxId: PID_SHORT_MAX_ID,
141
+ padsDeclaredLength: false,
142
+ padsOptionalLength: false,
143
+ memberMustUnderstand: true,
144
+ padAfterValue: true,
145
+ }, shortLengthCodes = false) {
146
+ this.order = order;
147
+ this.plCdr1 = plCdr1;
148
+ this.shortLengthCodes = shortLengthCodes;
149
+ this.maxAlignment = Math.max(1, maxAlign);
150
+ }
151
+ /** The alignment cap in force. */
152
+ get maxAlign() {
153
+ return this.maxAlignment;
154
+ }
155
+ /** How many octets have been written. */
156
+ position() {
157
+ return this.out.length;
158
+ }
159
+ /** The octets written so far. */
160
+ finish() {
161
+ return Uint8Array.from(this.out);
162
+ }
163
+ /** Pad to `align`, counted from the stream's origin. */
164
+ alignTo(align) {
165
+ const to = Math.min(align, this.maxAlignment);
166
+ if (to <= 1) {
167
+ return;
168
+ }
169
+ const offset = this.out.length - this.origin;
170
+ const padding = (to - (offset % to)) % to;
171
+ for (let index = 0; index < padding; index += 1) {
172
+ this.out.push(0);
173
+ }
174
+ }
175
+ /**
176
+ * Move the alignment origin to the current position.
177
+ *
178
+ * `ElementPadding::Packed` is HDDS's measured departure from XTypes 1.3
179
+ * §7.4.1, which counts every element's alignment from the encapsulation:
180
+ * each collection element and each aggregate value counts its own
181
+ * alignment from its own first octet instead. `array-1d-struct-final` is
182
+ * `01000000 0200 03000000 0400 77`, the second `Cell`'s `long` at offset
183
+ * six where the clause puts it at eight, and `typedef-of-struct` is the
184
+ * same inside two `@final` members.
185
+ *
186
+ * Returns the origin displaced, for {@link finishPackedElement}.
187
+ */
188
+ beginPackedElement() {
189
+ const saved = this.origin;
190
+ this.origin = this.out.length;
191
+ return saved;
192
+ }
193
+ /** Restore the origin {@link beginPackedElement} displaced. */
194
+ finishPackedElement(saved) {
195
+ this.origin = saved;
196
+ }
197
+ refuse(expected) {
198
+ throw new OutOfRange(this.out.length, expected);
199
+ }
200
+ check(value, low, high, what) {
201
+ if (!Number.isInteger(value) || value < low || value > high) {
202
+ this.refuse(`${what} in [${low}, ${high}], not ${value}`);
203
+ }
204
+ }
205
+ checkBig(value, low, high, what) {
206
+ if (value < low || value > high) {
207
+ this.refuse(`${what} in [${low}, ${high}], not ${value}`);
208
+ }
209
+ }
210
+ raw(octets) {
211
+ for (const octet of octets) {
212
+ this.out.push(octet & 0xff);
213
+ }
214
+ }
215
+ /** An integer of `width` octets, in the stream's order. */
216
+ writeInt(value, width, align = width) {
217
+ this.alignTo(align);
218
+ this.rawInt(value, width, isLittle(this.order));
219
+ }
220
+ /**
221
+ * An integer of `width` octets in a named order, with no alignment.
222
+ *
223
+ * The order is an argument because two forms fix it against the
224
+ * stream: CORBA's length-prefixed `wchar` and the `wstring` unit
225
+ * orders. Everything else goes through {@link writeInt}.
226
+ */
227
+ rawInt(value, width, little) {
228
+ const octets = new Array(width);
229
+ let rest = value & ((1n << BigInt(width * 8)) - 1n);
230
+ for (let index = 0; index < width; index += 1) {
231
+ octets[index] = Number(rest & 0xffn);
232
+ rest >>= 8n;
233
+ }
234
+ if (!little) {
235
+ octets.reverse();
236
+ }
237
+ this.raw(octets);
238
+ }
239
+ // -- primitives ------------------------------------------------------
240
+ /** `boolean`, one octet, 0 or 1. */
241
+ writeBool(value) {
242
+ this.out.push(value ? 1 : 0);
243
+ }
244
+ /** `octet` and `uint8`. */
245
+ writeU8(value) {
246
+ this.check(value, 0, 0xff, "an octet");
247
+ this.out.push(value);
248
+ }
249
+ /** `int8`. */
250
+ writeI8(value) {
251
+ this.check(value, -0x80, 0x7f, "an int8");
252
+ this.out.push(value & 0xff);
253
+ }
254
+ /**
255
+ * `char`, one octet.
256
+ *
257
+ * A code point above 255 is refused rather than truncated: a `char`
258
+ * field holding `ä` from a UTF-8 source is a mistake worth stopping
259
+ * on, and `0xe4` is a different character.
260
+ */
261
+ writeChar(value) {
262
+ if ([...value].length !== 1) {
263
+ this.refuse(`exactly one character, not \`${value}\``);
264
+ }
265
+ const point = value.codePointAt(0) ?? 0;
266
+ if (point > 0xff) {
267
+ this.refuse(`a character below U+0100, not U+${point.toString(16).toUpperCase()}`);
268
+ }
269
+ this.out.push(point);
270
+ }
271
+ /**
272
+ * `wchar`, at `width` octets and in the shape `framing` gives it.
273
+ *
274
+ * Two octets under Table 31 and for seven of the nine measured
275
+ * implementations; four under RTI's XCDR1, under TAO and under
276
+ * `rosidl_typesupport_fastrtps_c`. The width is an argument of its
277
+ * own and not a reading of the `wstring` policy, because
278
+ * `rmw_cyclonedds_cpp` writes two here and four inside a `wstring`.
279
+ *
280
+ * The BMP restriction of §7.2.2.2.1.2.5 is a property of the type
281
+ * and not of the width: the four-octet form zero-extends a code
282
+ * unit, it does not carry a code point.
283
+ *
284
+ * `order` reaches only the length-prefixed form: a scalar cannot
285
+ * carry a byte order mark, so anything but the stream's order means
286
+ * CORBA's default, which is big-endian.
287
+ */
288
+ writeWchar(value, width = WcharWidth.Octets2, framing = WcharFraming.Fixed, order = WstringUnitOrder.Stream) {
289
+ if ([...value].length !== 1) {
290
+ this.refuse(`exactly one character, not \`${value}\``);
291
+ }
292
+ const point = value.codePointAt(0) ?? 0;
293
+ if (point > 0xffff) {
294
+ this.refuse(`a BMP character, not U+${point.toString(16).toUpperCase()}`);
295
+ }
296
+ if (width !== WcharWidth.Octets2 && width !== WcharWidth.Octets4) {
297
+ this.refuse(`a wchar width of 2 or 4, not ${String(width)}`);
298
+ }
299
+ if (framing === WcharFraming.GiopLengthPrefixed) {
300
+ // CORBA 3.3 part 2 §9.3.1.6's GIOP 1.2 form: the octet count
301
+ // first, then the code unit's octets, all aligned to one. The
302
+ // count is the transmission code set's width and not the value's,
303
+ // so it is the same octet for every character. Measured
304
+ // 2026-08-10: omniORB writes `02 00e4` for U+00E4 in either
305
+ // stream order and TAO `04 e4000000`.
306
+ this.out.push(width);
307
+ const little = order === WstringUnitOrder.Stream && isLittle(this.order);
308
+ this.rawInt(BigInt(point), width, little);
309
+ return;
310
+ }
311
+ this.writeInt(BigInt(point), width);
312
+ }
313
+ /** `short`. */
314
+ writeI16(value) {
315
+ this.check(value, -0x8000, 0x7fff, "a short");
316
+ this.writeInt(BigInt(value), 2);
317
+ }
318
+ /** `unsigned short`. */
319
+ writeU16(value) {
320
+ this.check(value, 0, 0xffff, "an unsigned short");
321
+ this.writeInt(BigInt(value), 2);
322
+ }
323
+ /** `long`. */
324
+ writeI32(value) {
325
+ this.check(value, -0x8000_0000, 0x7fff_ffff, "a long");
326
+ this.writeInt(BigInt(value), 4);
327
+ }
328
+ /** `unsigned long`. */
329
+ writeU32(value) {
330
+ this.check(value, 0, 0xffff_ffff, "an unsigned long");
331
+ this.writeInt(BigInt(value), 4);
332
+ }
333
+ /** `long long`, which is a `bigint` because a binary64 is not one. */
334
+ writeI64(value) {
335
+ this.checkBig(value, -(2n ** 63n), 2n ** 63n - 1n, "a long long");
336
+ this.writeInt(value, 8);
337
+ }
338
+ /** `unsigned long long`. */
339
+ writeU64(value) {
340
+ this.checkBig(value, 0n, 2n ** 64n - 1n, "an unsigned long long");
341
+ this.writeInt(value, 8);
342
+ }
343
+ /**
344
+ * The Cyclone union overlay: the low `octets` of `pattern`, written
345
+ * as one unsigned integer of that width in the stream's byte order.
346
+ *
347
+ * A discriminator carrying a label the `union-labels` dialect axis
348
+ * says selects nothing makes the peer fall through to `default:` and
349
+ * serialise its one C union storage *as that arm* — the selected
350
+ * member's octets, reinterpreted. The pattern is the `bigint` whose
351
+ * little-endian bytes are the selected member's little-endian
352
+ * storage octets, zero-extended; the `bitPattern*` functions
353
+ * produce it.
354
+ */
355
+ writeUnionOverlay(pattern, octets) {
356
+ switch (octets) {
357
+ case 1:
358
+ this.writeU8(Number(pattern & 0xffn));
359
+ return;
360
+ case 2:
361
+ this.writeU16(Number(pattern & 0xffffn));
362
+ return;
363
+ case 4:
364
+ this.writeU32(Number(pattern & 0xffffffffn));
365
+ return;
366
+ case 8:
367
+ this.writeU64(pattern);
368
+ return;
369
+ default:
370
+ throw new OutOfRange(this.out.length, `a union arm of 1, 2, 4 or 8 octets, not ${octets}`);
371
+ }
372
+ }
373
+ /**
374
+ * `float`, a binary32.
375
+ *
376
+ * The value is rounded once, here, and not by the caller: a `number`
377
+ * is a binary64 and the narrowing is part of writing the field.
378
+ */
379
+ writeF32(value) {
380
+ this.alignTo(4);
381
+ const buffer = new DataView(new ArrayBuffer(4));
382
+ buffer.setFloat32(0, value, isLittle(this.order));
383
+ this.raw(new Uint8Array(buffer.buffer));
384
+ }
385
+ /** `double`, a binary64. */
386
+ writeF64(value) {
387
+ this.alignTo(8);
388
+ const buffer = new DataView(new ArrayBuffer(8));
389
+ buffer.setFloat64(0, value, isLittle(this.order));
390
+ this.raw(new Uint8Array(buffer.buffer));
391
+ }
392
+ /**
393
+ * `long double`, as the sixteen octets it is.
394
+ *
395
+ * The octets arrive in the order the wire carries them for a
396
+ * little-endian stream, which is how `ZdFloat128` holds them, and are
397
+ * reversed for a big-endian one. A `number` never appears: a binary64
398
+ * would drop sixty bits of the significand.
399
+ */
400
+ writeF128(value) {
401
+ if (value.octets.length !== 16) {
402
+ this.refuse(`sixteen octets, not ${value.octets.length}`);
403
+ }
404
+ this.alignTo(8);
405
+ const octets = Array.from(value.octets);
406
+ if (!isLittle(this.order)) {
407
+ octets.reverse();
408
+ }
409
+ this.raw(octets);
410
+ }
411
+ // -- strings ---------------------------------------------------------
412
+ /**
413
+ * `string`: a four-octet length that **includes** the terminator,
414
+ * then the octets, then a NUL. §7.4.1.1.1.
415
+ */
416
+ writeString(value) {
417
+ const octets = new TextEncoder().encode(value);
418
+ this.writeU32(octets.length + 1);
419
+ this.raw(octets);
420
+ this.out.push(0);
421
+ }
422
+ /**
423
+ * A bounded `string`: the same bytes {@link writeString} writes. The
424
+ * bound is a promise about the source and not a difference on the
425
+ * wire, so this is the only place it can be enforced — refused rather
426
+ * than cut at a character boundary, which would put a sample on the
427
+ * wire nobody wrote. The bound counts octets, which is what a
428
+ * `string<N>` bound counts for every measured implementation.
429
+ */
430
+ writeStringBounded(value, bound) {
431
+ const octets = new TextEncoder().encode(value).length;
432
+ if (octets > bound) {
433
+ this.refuse(`at most ${bound} octets in a bounded string, and this one has ${octets}`);
434
+ }
435
+ this.writeString(value);
436
+ }
437
+ /**
438
+ * A bounded `wstring`, as {@link writeStringBounded} is to
439
+ * {@link writeString}. The bound counts UTF-16 code units, not
440
+ * characters and not octets, whatever `policy` puts on the wire.
441
+ */
442
+ writeWstringBounded(value, bound, policy, order) {
443
+ this.checkWstringBound(value, bound);
444
+ this.writeWstringUtf8(value, policy, order);
445
+ }
446
+ /**
447
+ * The count check every bounded `sequence` makes before a byte of it
448
+ * is written. Table 17 makes `O2.length <= T1.length` the condition
449
+ * for constructing a `sequence<T, N>`, and the wire carries no bound
450
+ * to compare against: a sender whose count already exceeds its own
451
+ * declaration has written a value of a type nobody declared.
452
+ */
453
+ checkSequenceBound(actual, bound) {
454
+ if (actual > bound) {
455
+ this.refuse(`at most ${bound} elements in a bounded sequence, and this one has ${actual}`);
456
+ }
457
+ }
458
+ /**
459
+ * The count check every bounded `map` makes before a byte of it is
460
+ * written. Its own message and not {@link checkSequenceBound}'s: the
461
+ * unit is an entry and not an element, per XTypes 1.3 §7.4.1.1.4's
462
+ * map-as-a-sequence-of-pairs equivalence.
463
+ */
464
+ checkMapBound(actual, bound) {
465
+ if (actual > bound) {
466
+ this.refuse(`at most ${bound} entries in a bounded map, and this one has ${actual}`);
467
+ }
468
+ }
469
+ /**
470
+ * `wstring`: a four-octet **octet** count, then UTF-16 code units.
471
+ *
472
+ * No BOM and no terminator — §7.4.1.1.2 forbids both outright. The
473
+ * order the units go out in is the stream's, which the encapsulation
474
+ * identifier already announced.
475
+ *
476
+ * The specification cell of {@link writeWstringUtf8}, kept as a
477
+ * method of its own because it is the one every reader of this file
478
+ * looks for.
479
+ */
480
+ writeWstring(value) {
481
+ this.writeWstringUtf8(value, WstringLengthPolicy.Specification, WstringUnitOrder.Stream);
482
+ }
483
+ /**
484
+ * A `wstring` in the form `policy` and `order` name.
485
+ *
486
+ * {@link WstringLengthPolicy.Specification} is a four-octet octet
487
+ * count and then UTF-16 code units. No BOM and no terminator —
488
+ * §7.4.1.1.2 forbids both outright: "The UTF-16 representation … shall
489
+ * not include a Byte Order Mark (BOM). The representation shall also
490
+ * not include any terminating NUL character(s)."
491
+ *
492
+ * The count is of **octets** under that policy and not of characters:
493
+ * §7.4.1.1.2 says "This is twice the number of characters in the
494
+ * string", with the clause's own rationale being that a code point
495
+ * outside the Basic Multilingual Plane takes two units and an octet
496
+ * count still reaches the end. So `"𝄞"` is a length of four.
497
+ *
498
+ * A TypeScript `string` already holds UTF-16 code units, which is the
499
+ * one thing this column gets for free — `charCodeAt` is the unit and
500
+ * `codePointAt` the character, and the two differ exactly where the
501
+ * policies do.
502
+ */
503
+ writeWstringUtf8(value, policy, order) {
504
+ const width = wstringUnitWidth(policy);
505
+ const counts = wstringCountsCharacters(policy);
506
+ if (counts && width === 2) {
507
+ // Fast CDR counts characters and writes each four-octet `wchar_t`
508
+ // as two octets, so a code point outside the BMP goes out as its
509
+ // low sixteen bits and arrives as a different character. A
510
+ // vendor's length convention is a policy; its data loss is not, so
511
+ // the value is refused. The refusal is on the pair (characters,
512
+ // two octets) and not on a named form: a character count over
513
+ // four-octet units carries the surrogate pair intact.
514
+ for (const character of value) {
515
+ const point = character.codePointAt(0) ?? 0;
516
+ if (point > 0xffff) {
517
+ this.refuse("a code point inside the BMP: a character count over two-octet units " +
518
+ `has no way to carry U+${point.toString(16).toUpperCase()}`);
519
+ }
520
+ }
521
+ }
522
+ const count = value.length;
523
+ const terminated = wstringIsTerminated(policy);
524
+ // CORBA §9.3.1.6's GIOP 1.1 rule, and RTI's XCDR1 form: a NUL code
525
+ // unit at the end, inside whatever the length counts.
526
+ //
527
+ // An empty `wstring` carries no mark: omniORB writes `00000000` for
528
+ // one, measured 2026-08-10 and byte-identical to every other
529
+ // implementation. A mark on an empty string would be a two-octet
530
+ // string that decodes to nothing, and no peer writes it.
531
+ const marked = order === WstringUnitOrder.ByteOrderMark && count > 0;
532
+ const total = count + (terminated ? 1 : 0) + (marked ? 1 : 0);
533
+ this.writeU32(counts ? total : total * width);
534
+ if (marked) {
535
+ this.writeWstringUnit(BYTE_ORDER_MARK, width, order);
536
+ }
537
+ for (let index = 0; index < count; index += 1) {
538
+ this.writeWstringUnit(value.charCodeAt(index), width, order);
539
+ }
540
+ if (terminated) {
541
+ this.writeWstringUnit(0, width, order);
542
+ }
543
+ }
544
+ /**
545
+ * A `wstring` whose characters go out as their own UTF-8 octets.
546
+ *
547
+ * `WstringCodeUnits::Utf8` is the measured departure of one product's
548
+ * generated serialiser: XTypes 1.3 Table 31 gives a `wstring`'s code unit
549
+ * as `Char16` and §7.4.1.1.2 makes the length "twice the number of
550
+ * characters", so the clause has no UTF-8 form at all. The declared length
551
+ * is the UTF-8 octet count and only C2 — the terminator — still reaches
552
+ * the wire. `wstring-member` is `04 00 00 00 61 62 63 00` for `"abc"`:
553
+ * three octets, a NUL, and the four declares both.
554
+ */
555
+ writeWstringUtf8Octets(value, policy) {
556
+ // A lone surrogate is not a code point. `TextEncoder` would replace one
557
+ // with U+FFFD and report success — a different character on the wire,
558
+ // silently. The iterator yields code points, so a lone surrogate is the
559
+ // only value in the surrogate range here.
560
+ for (const character of value) {
561
+ const point = character.codePointAt(0) ?? 0;
562
+ if (point >= 0xd800 && point <= 0xdfff) {
563
+ this.refuse("a Unicode code point, not a lone surrogate");
564
+ }
565
+ }
566
+ const octets = new TextEncoder().encode(value);
567
+ const terminated = wstringIsTerminated(policy);
568
+ const total = octets.length + (terminated ? 1 : 0);
569
+ if (total > 0xffffffff) {
570
+ this.refuse("a wstring length the UInt32 field can hold");
571
+ }
572
+ this.writeU32(total);
573
+ this.raw(octets);
574
+ if (terminated) {
575
+ this.out.push(0);
576
+ }
577
+ }
578
+ /**
579
+ * A bounded UTF-8-octet `wstring`. The bound still counts UTF-16 code
580
+ * units, which is what a `wstring<N>` bound means, while the wire carries
581
+ * the UTF-8 octets.
582
+ */
583
+ writeWstringUtf8OctetsBounded(value, bound, policy) {
584
+ this.checkWstringBound(value, bound);
585
+ this.writeWstringUtf8Octets(value, policy);
586
+ }
587
+ /**
588
+ * Refuses a `wstring<N>` value longer than its bound. The bound counts
589
+ * UTF-16 code units, whatever form the wire carries.
590
+ */
591
+ checkWstringBound(value, bound) {
592
+ const units = value.length;
593
+ if (units > bound) {
594
+ this.refuse(`at most ${bound} UTF-16 code units in a bounded wstring, and this one has ${units}`);
595
+ }
596
+ }
597
+ /** One `wstring` code unit, at its width and in its order. */
598
+ writeWstringUnit(unit, width, order) {
599
+ if (order === WstringUnitOrder.BigEndianAlways) {
600
+ this.alignTo(width);
601
+ this.rawInt(BigInt(unit), width, false);
602
+ return;
603
+ }
604
+ if (wstringOrderIsFixedLittle(order)) {
605
+ this.alignTo(width);
606
+ this.rawInt(BigInt(unit), width, true);
607
+ return;
608
+ }
609
+ this.writeInt(BigInt(unit), width);
610
+ }
611
+ /**
612
+ * `fixed<digits, scale>` as packed BCD. [CDR] §9.3.2.8.
613
+ *
614
+ * DDS-XTypes 1.3 does not carry the type at all — §7.2.2 has no
615
+ * fixed-point kind and §7.4.3.5.3 has no rule whose left-hand side
616
+ * matches one — so the form is CORBA's, which is where an IDL `fixed`
617
+ * came from and the only wire definition it has.
618
+ *
619
+ * The value arrives as a `string` because a `number` is a binary64 and
620
+ * `fixed<31, 2>` is exactly the case a binary64 cannot carry. It is
621
+ * not parsed through anything either: a parser that accepted an
622
+ * exponent or normalised a scale would change the digit count, which
623
+ * is the measured axis {@link FixedDigitCount} names.
624
+ *
625
+ * Aligned to one. [CDR] §9.3.2.8 gives a fixed-point value an
626
+ * alignment of one octet, so a `fixed` never pads.
627
+ */
628
+ writeFixed(value, digits, scale, count) {
629
+ const nibbles = this.fixedNibbles(value, digits, scale, count);
630
+ for (let index = 0; index + 1 < nibbles.length; index += 2) {
631
+ this.out.push((((nibbles[index] ?? 0) << 4) | (nibbles[index + 1] ?? 0)) & 0xff);
632
+ }
633
+ }
634
+ /**
635
+ * The BCD nibbles of `text` under `fixed<digits, scale>`.
636
+ *
637
+ * [CDR] §9.3.2.8: digit nibbles most-significant first, then a sign
638
+ * nibble — `0xc` for positive or zero, `0xd` for negative — with a
639
+ * leading zero nibble prepended when the nibble count is odd so the
640
+ * nibbles pair up.
641
+ *
642
+ * Neither half is padded in the direction that would lose a digit: a
643
+ * value too long for its integer part or its fraction is one this type
644
+ * cannot represent, and is refused.
645
+ *
646
+ * How many digits go out is {@link FixedDigitCount}: the clause gives
647
+ * the packing and says nothing about the count, and the four
648
+ * implementations that write a `fixed` split two and two.
649
+ */
650
+ fixedNibbles(text, digits, scale, count) {
651
+ if (digits === 0) {
652
+ this.refuse("a fixed with at least one digit");
653
+ }
654
+ // OMG IDL 4.2 rule (42) caps the BCD rule at 31 digits, and
655
+ // fixed<31, S> is the widest declaration IDL admits. A wider one has
656
+ // no packing defined for it, so it is refused rather than packed into
657
+ // octets no reader can be told the width of.
658
+ if (digits > 31) {
659
+ this.refuse(`a fixed of at most 31 digits, not ${digits}`);
660
+ }
661
+ if (scale > digits) {
662
+ this.refuse("a scale no larger than the digit count");
663
+ }
664
+ let magnitude = text;
665
+ let negative = false;
666
+ if (magnitude.startsWith("-")) {
667
+ negative = true;
668
+ magnitude = magnitude.slice(1);
669
+ }
670
+ else if (magnitude.startsWith("+")) {
671
+ magnitude = magnitude.slice(1);
672
+ }
673
+ const dot = magnitude.indexOf(".");
674
+ let whole = dot < 0 ? magnitude : magnitude.slice(0, dot);
675
+ const fraction = dot < 0 ? "" : magnitude.slice(dot + 1);
676
+ const bad = `a decimal that fits fixed<${digits}, ${scale}>, not \`${text}\``;
677
+ if (whole === "" && fraction === "") {
678
+ this.refuse(bad);
679
+ }
680
+ if (!/^\d*$/.test(whole) || !/^\d*$/.test(fraction)) {
681
+ this.refuse(bad);
682
+ }
683
+ const asWritten = whole.length + scale;
684
+ const integralWidth = digits - scale;
685
+ whole = whole.replace(/^0+/, "");
686
+ if (whole.length > integralWidth) {
687
+ this.refuse(`a decimal whose integer part fits ${integralWidth} digits, not \`${text}\``);
688
+ }
689
+ if (fraction.length > scale) {
690
+ this.refuse(`a decimal whose fraction fits ${scale} digits, not \`${text}\``);
691
+ }
692
+ // The full `digits`-wide field first, whatever the count: the
693
+ // value's precision is that field with its leading zeros removed, so
694
+ // both counts are read off one construction.
695
+ const every = new Array(digits).fill(0);
696
+ let at = integralWidth - whole.length;
697
+ for (const character of whole) {
698
+ every[at] = character.charCodeAt(0) - 0x30;
699
+ at += 1;
700
+ }
701
+ for (const character of fraction) {
702
+ every[at] = character.charCodeAt(0) - 0x30;
703
+ at += 1;
704
+ }
705
+ let leadingZeros = 0;
706
+ while (leadingZeros < every.length && every[leadingZeros] === 0) {
707
+ leadingZeros += 1;
708
+ }
709
+ const significant = every.length - leadingZeros;
710
+ let wire = digits;
711
+ if (count === FixedDigitCount.ValuePrecision) {
712
+ wire = Math.max(significant, 1);
713
+ }
714
+ else if (count === FixedDigitCount.ValueStringDigits) {
715
+ // ACE/TAO 2.5.24: the integer digits as written plus S, made odd by one leading
716
+ // zero. Past 31 no reader can be told the field's width.
717
+ wire = asWritten + (asWritten % 2 === 0 ? 1 : 0);
718
+ if (wire > 31) {
719
+ this.refuse(`a fixed of at most 31 digits on the wire, not ${wire} for \`${text}\``);
720
+ }
721
+ }
722
+ const odd = (wire + 1) % 2 === 1;
723
+ const nibbles = [];
724
+ if (odd) {
725
+ nibbles.push(0);
726
+ }
727
+ // A count wider than P is filled with leading zeros.
728
+ const kept = Math.min(wire, every.length);
729
+ for (let index = kept; index < wire; index += 1) {
730
+ nibbles.push(0);
731
+ }
732
+ for (let index = every.length - kept; index < every.length; index += 1) {
733
+ nibbles.push(every[index] ?? 0);
734
+ }
735
+ // A negative zero is written positive, because there is one zero.
736
+ nibbles.push(negative && significant > 0 ? 0x0d : 0x0c);
737
+ return nibbles;
738
+ }
739
+ // -- aggregates ------------------------------------------------------
740
+ /** A DHEADER: a four-octet length of what follows. §7.4.3.5.3. */
741
+ writeDheader() {
742
+ this.alignTo(4);
743
+ const at = this.out.length;
744
+ this.raw([0, 0, 0, 0]);
745
+ return { at };
746
+ }
747
+ /**
748
+ * Patch a DHEADER with the length of what followed it.
749
+ *
750
+ * A header from {@link noDheader} is not there and is ignored, which
751
+ * is what lets a generated codec write the same two lines for a
752
+ * `@final` aggregate and an `@appendable` one.
753
+ */
754
+ finishDheader(header) {
755
+ if (header.at < 0) {
756
+ return;
757
+ }
758
+ this.patch(header.at, this.out.length - header.at - 4, 4);
759
+ }
760
+ /**
761
+ * An XCDR2 EMHEADER: the member id, its length code and the
762
+ * must-understand flag in one 32-bit word. §7.4.3.4.2.
763
+ *
764
+ * The length code is the layout plan's decision and arrives here as
765
+ * a number; this runtime does not choose one.
766
+ *
767
+ * LC0 through LC3 carry the length in the code itself. LC4 is
768
+ * followed by a NEXTINT. LC5 through LC7 also have one, and
769
+ * §7.4.3.4.2 says it "shall be reused also as part of the serialized
770
+ * member", so no placeholder is reserved for them: the member's own
771
+ * leading word lands in that position and *is* the NEXTINT.
772
+ */
773
+ writeEmheader(memberId, lengthCode, mustUnderstand) {
774
+ // §7.2.2.4.4.4.4 runs member ids from 0 to 0x0FFFFFFF and §7.4.3.4.2
775
+ // leaves them the low 28 bits of the word. Masking a larger one
776
+ // writes a well-formed header for `id & 0x0FFFFFFF`: the peer reads
777
+ // a member the caller did not name, and nothing at either end says
778
+ // so.
779
+ if (!Number.isInteger(memberId) || memberId < 0 || memberId > E_MEMBER_ID_MASK) {
780
+ this.refuse(`a member id inside the 28 bits of an EMHEADER, not ${memberId}`);
781
+ }
782
+ this.alignTo(4);
783
+ const word = ((mustUnderstand ? 1 : 0) << 31) |
784
+ ((lengthCode & 0x7) << 28) |
785
+ (memberId & 0x0fff_ffff);
786
+ this.writeInt(BigInt(word >>> 0), 4);
787
+ const wordAt = this.out.length - 4;
788
+ if (lengthCode === 4) {
789
+ const at = this.out.length;
790
+ this.raw([0, 0, 0, 0]);
791
+ return { nextInt: at, word: wordAt, lengthCode };
792
+ }
793
+ return { nextInt: undefined, word: wordAt, lengthCode };
794
+ }
795
+ /**
796
+ * Patch an EMHEADER's NEXTINT with the length of what followed.
797
+ *
798
+ * Under {@link shortLengthCodes} an LC4 header whose member turned
799
+ * out to be 1, 2, 4 or 8 octets instead loses its NEXTINT and states
800
+ * the length in the code, which is what Fast DDS does.
801
+ */
802
+ finishEmheader(header) {
803
+ if (header.nextInt === undefined) {
804
+ // Under codes 0 to 3 the code *is* the length — 1, 2, 4 or 8
805
+ // octets, §7.4.3.4.2. A value of another size misdeclares it and
806
+ // the reader's skip lands inside data, so the mismatch is refused
807
+ // here rather than written. Codes 5 to 7 declare nothing of their
808
+ // own: the member's leading length word is the NEXTINT.
809
+ if (header.lengthCode >= 0 && header.lengthCode <= 3 && header.word + 4 <= this.out.length) {
810
+ const declared = 1 << header.lengthCode;
811
+ const body = this.out.length - header.word - 4;
812
+ if (body !== declared) {
813
+ this.refuse(`a member of ${declared} octets under length code ${header.lengthCode}, not ${body}`);
814
+ }
815
+ }
816
+ return;
817
+ }
818
+ const length = this.out.length - header.nextInt - 4;
819
+ if (this.shortLengthCodes && [1, 2, 4, 8].includes(length)) {
820
+ this.shortenEmheader(header, length);
821
+ return;
822
+ }
823
+ this.patch(header.nextInt, length, 4);
824
+ }
825
+ /**
826
+ * Remove an LC4 header's NEXTINT and raise its length code.
827
+ *
828
+ * Every octet of the member's value is unchanged and only its
829
+ * position moves, which is safe for one reason and only under XCDR2:
830
+ * MAXALIGN is four there, so the value began at a four-aligned
831
+ * offset with the NEXTINT in front of it and begins at a four-aligned
832
+ * offset without it.
833
+ */
834
+ shortenEmheader(header, length) {
835
+ const at = header.nextInt;
836
+ if (at === undefined) {
837
+ return;
838
+ }
839
+ const code = { 1: 0, 2: 1, 4: 2, 8: 3 }[length];
840
+ this.out.splice(at, 4);
841
+ const previous = this.read(header.word, 4);
842
+ this.patch(header.word, ((previous & ~(0x7 << 28)) | (code << 28)) >>> 0, 4);
843
+ }
844
+ // -- parameter lists -------------------------------------------------
845
+ /**
846
+ * A PL_CDR1 parameter header, short form: `[id:u16][length:u16]`.
847
+ *
848
+ * `mustUnderstand` is bit 14 of the id. §7.4.1.2.2: the flag "shall
849
+ * be set if and only if the must_understand property of the member is
850
+ * set to true".
851
+ */
852
+ writePid(pid, role = ParameterRole.Member, mustUnderstand = false) {
853
+ // §7.4.1.2.2 gives the short form the ids 0 to `shortMaxId`
854
+ // inclusive and reserves what is above it — PID_EXTENDED is 0x3F01
855
+ // itself. A short header with a reserved id collides with the
856
+ // protocol's own parameters, so such an id takes the extended form
857
+ // or none.
858
+ if (!Number.isInteger(pid) || pid < 0 || pid > this.plCdr1.shortMaxId) {
859
+ this.refuse(`a parameter id the short header carries, not ${pid} — the extended header takes it`);
860
+ }
861
+ this.alignTo(4);
862
+ // Two claimants on the same word, deliberately: the member's own
863
+ // declaration, and whatever the framing puts on every id regardless.
864
+ let id = pid;
865
+ if (mustUnderstand && this.plCdr1.memberMustUnderstand) {
866
+ id |= PID_MUST_UNDERSTAND;
867
+ }
868
+ id |= pidFlagsAt(this.plCdr1.pidFlags, "member");
869
+ const bare = pid & ~(PID_MUST_UNDERSTAND | PID_IMPL_EXTENSION);
870
+ this.writeInt(BigInt(id & 0xffff), 2);
871
+ const at = this.out.length;
872
+ this.raw([0, 0]);
873
+ return {
874
+ at,
875
+ extended: false,
876
+ savedOrigin: this.beginParameterValue(),
877
+ role,
878
+ pid: bare,
879
+ mustUnderstand,
880
+ };
881
+ }
882
+ /**
883
+ * A PL_CDR1 parameter header, extended form.
884
+ *
885
+ * `[PID_EXTENDED][8][id:u32][length:u32]`, required when the id
886
+ * reaches the reserved range or the value is longer than `0xFFFF`.
887
+ * §7.4.1.2.
888
+ */
889
+ writePidExtended(pid, role = ParameterRole.Member, mustUnderstand = false) {
890
+ // The same 28 bits as an EMHEADER's, here in the `eMemberHeader` of
891
+ // §7.4.1.2.1. Masking aliases the parameter onto another member
892
+ // exactly as it does there.
893
+ if (!Number.isInteger(pid) || pid < 0 || pid > E_MEMBER_ID_MASK) {
894
+ this.refuse(`a member id inside the 28 bits of an eMemberHeader, not ${pid}`);
895
+ }
896
+ this.alignTo(4);
897
+ const marker = PID_EXTENDED | pidFlagsAt(this.plCdr1.pidFlags, "extended-marker");
898
+ this.writeInt(BigInt(marker), 2);
899
+ this.writeInt(8n, 2);
900
+ // The member's must-understand is `FLAG_2` of the `eMemberHeader`
901
+ // (§7.4.1.2.1) and not the marker's bit above it.
902
+ let header = pid & E_MEMBER_ID_MASK;
903
+ if (mustUnderstand && this.plCdr1.memberMustUnderstand) {
904
+ header |= E_FLAG_MUST_UNDERSTAND;
905
+ }
906
+ this.writeInt(BigInt(header >>> 0), 4);
907
+ const at = this.out.length;
908
+ this.raw([0, 0, 0, 0]);
909
+ return {
910
+ at,
911
+ extended: true,
912
+ savedOrigin: this.beginParameterValue(),
913
+ role,
914
+ pid,
915
+ mustUnderstand,
916
+ };
917
+ }
918
+ /**
919
+ * Start a parameter's value, returning the origin it replaces.
920
+ *
921
+ * "Each element, or parameter, within a parameter list data
922
+ * structure is simply a CDR-encapsulated block of data" — §7.4.1.2.
923
+ * A block counts alignment from its own start, so an eight-octet
924
+ * member at the head of one is already aligned and takes no padding
925
+ * inside its own length.
926
+ */
927
+ beginParameterValue() {
928
+ const saved = this.origin;
929
+ this.origin = this.out.length;
930
+ return saved;
931
+ }
932
+ /**
933
+ * Patch a parameter header with its value's length.
934
+ *
935
+ * The declared length covers the value's trailing padding or stops at
936
+ * the value, depending on the framing — and which of the two framing
937
+ * flags answers is the header's own {@link Pid.role}, not an argument
938
+ * the caller has to remember. A generated codec calls
939
+ * `w.finishPid(h)`, exactly as the C and Java columns do; a runtime
940
+ * that took the answer as a defaulted parameter never reached
941
+ * `padsOptionalLength` at all.
942
+ *
943
+ * The padding itself starts the next parameter at a four-octet offset
944
+ * measured from the enclosing stream, so the origin goes back before
945
+ * it is written.
946
+ */
947
+ finishPid(header) {
948
+ const width = header.extended ? 4 : 2;
949
+ let length = this.out.length - header.at - width;
950
+ const pads = header.role === ParameterRole.Optional
951
+ ? this.plCdr1.padsOptionalLength
952
+ : this.plCdr1.padsDeclaredLength;
953
+ if (pads) {
954
+ const past = (this.out.length - header.savedOrigin) % 4;
955
+ if (past !== 0) {
956
+ length += 4 - past;
957
+ }
958
+ }
959
+ if (!header.extended && length > PID_SHORT_MAX_LEN) {
960
+ this.promotePid(header, length);
961
+ }
962
+ else {
963
+ this.patch(header.at, length, width);
964
+ }
965
+ this.origin = header.savedOrigin;
966
+ // Under Fast DDS's framing the parameter writes no padding of its
967
+ // own — the next header aligns itself, and a final parameter ends
968
+ // the stream where its value does.
969
+ if (this.plCdr1.padAfterValue) {
970
+ this.alignTo(4);
971
+ }
972
+ }
973
+ /**
974
+ * Rewrite a short parameter header as the extended form.
975
+ *
976
+ * §7.4.1.2.2's `ssize` is sixteen bits, and which values overrun it is
977
+ * a property of the sample rather than of the type. So the plan writes
978
+ * the short form for an unbounded member and the fallback lives here,
979
+ * where the length is known. Without it a value over 64 KiB had its
980
+ * length silently truncated to sixteen bits and the payload still
981
+ * looked well-formed — a reader would take the next parameter from
982
+ * inside this one.
983
+ *
984
+ * Eight octets are inserted between the header and the value. The
985
+ * value's own octets do not change: XCDR1's MAXALIGN is eight and the
986
+ * insertion is eight, so every offset inside it keeps its remainder.
987
+ */
988
+ promotePid(header, length) {
989
+ const start = header.at - 2;
990
+ this.out.splice(header.at + 2, 0, ...new Array(8).fill(0));
991
+ const marker = PID_EXTENDED | pidFlagsAt(this.plCdr1.pidFlags, "extended-marker");
992
+ this.patch(start, marker & 0xffff, 2);
993
+ this.patch(start + 2, 8, 2);
994
+ let memberHeader = header.pid & E_MEMBER_ID_MASK;
995
+ if (header.mustUnderstand && this.plCdr1.memberMustUnderstand) {
996
+ memberHeader |= E_FLAG_MUST_UNDERSTAND;
997
+ }
998
+ this.patch(start + 4, memberHeader >>> 0, 4);
999
+ this.patch(start + 8, length, 4);
1000
+ }
1001
+ /** The `[PID_LIST_END][0]` that ends a parameter list. §7.4.1.2.4. */
1002
+ writeSentinel() {
1003
+ this.alignTo(4);
1004
+ this.writeInt(BigInt(PID_LIST_END | pidFlagsAt(this.plCdr1.pidFlags, "sentinel")), 2);
1005
+ this.writeInt(0n, 2);
1006
+ }
1007
+ // -- enumerations ----------------------------------------------------
1008
+ /**
1009
+ * An enumerator, as the integer its bit bound calls for.
1010
+ *
1011
+ * §7.4.3.5.3 rule (5) is
1012
+ * `XCDR << {O : ENUM_TYPE} = XCDR << {O.value : O.holder_type}`, and
1013
+ * the ENUM_TYPE row of the same clause's table says "O.holder_type is
1014
+ * either Int8, Int16 or Int32 depending on the value of the
1015
+ * `@bit_bound` annotation". A **signed** holder: `@value(-1)` is the
1016
+ * two's complement of one, so at the default bound it goes out as
1017
+ * `ffffffff`.
1018
+ *
1019
+ * What fits is everything expressible in the bound's bits, signed or
1020
+ * unsigned — `-2^(n-1) ..= 2^n - 1`. RTI Connext 7.7 accepts
1021
+ * `@value(-1)` at the default bound and Cyclone accepts `@value(200)`
1022
+ * under `@bit_bound(8)`; a signed-only range would reject the second
1023
+ * and a wider one would accept values neither can express.
1024
+ *
1025
+ * §7.3.1.2.1.5 caps the bound itself at 1 to 32, so a bound outside
1026
+ * that has no holder and is refused rather than widened.
1027
+ *
1028
+ * The bound comes **first**, which is the order the shared walk in
1029
+ * `crates/lang-rules/src/emit/encode.rs` calls it in and the order
1030
+ * `zdw_write_enum` and Java's `writeEnum` take. When both arguments
1031
+ * were a `number` a swapped pair was a legal call that wrote a
1032
+ * plausible octet — the Java column found exactly that, at run time.
1033
+ *
1034
+ * The value is a `bigint` and the bound a `number`, which is the same
1035
+ * widened holder `zdw_write_enum`'s `int64_t` and Java's `long` are:
1036
+ * the walk casts the enumerator to `Scalar::Int64` before the call, so
1037
+ * the two arguments now have different types and a swap does not
1038
+ * compile.
1039
+ */
1040
+ writeEnum(bitBound, value) {
1041
+ if (bitBound <= 0 || bitBound > 32) {
1042
+ this.refuse(`an @bit_bound between 1 and 32, not ${bitBound}`);
1043
+ }
1044
+ const low = -(1n << BigInt(bitBound - 1));
1045
+ const high = (1n << BigInt(bitBound)) - 1n;
1046
+ if (value < low || value > high) {
1047
+ this.refuse(`an enumerator a ${bitBound}-bit holder can hold, not ${value}`);
1048
+ }
1049
+ const pattern = value & ((1n << BigInt(bitBound)) - 1n);
1050
+ if (bitBound <= 8) {
1051
+ this.writeU8(Number(pattern));
1052
+ }
1053
+ else if (bitBound <= 16) {
1054
+ this.writeU16(Number(pattern));
1055
+ }
1056
+ else {
1057
+ this.writeU32(Number(pattern));
1058
+ }
1059
+ }
1060
+ /**
1061
+ * A bitmask, as the unsigned integer its bit bound calls for.
1062
+ *
1063
+ * §7.4.3.5.3 rule (6) writes `O.value` as `O.holder_type`, and Table
1064
+ * 33 of §7.4.1.1.3.2 gives the holder: `UInt8` for a bound of 1 to 8,
1065
+ * `UInt16` to 16, `UInt32` to 32 and `UInt64` to 64. The same clause
1066
+ * fixes the direction — "Bit indexes are counted from zero starting at
1067
+ * the least-significant bit of the full byte size of the bitmask" — so
1068
+ * bit `n` is `1 << n` whatever the holder's width.
1069
+ *
1070
+ * A bit at or past the bound is refused: the clause says those bits
1071
+ * "are not considered to be part of the bitmask", so setting one would
1072
+ * put a value on the wire no peer must read back.
1073
+ *
1074
+ * The value is a `bigint` at every bound and not only at 64, because
1075
+ * the caller is a generated codec that casts once. The bound comes
1076
+ * first, as it does for {@link writeEnum}.
1077
+ */
1078
+ writeBitmask(bitBound, bits) {
1079
+ if (bitBound <= 0 || bitBound > 64) {
1080
+ this.refuse(`an @bit_bound between 1 and 64, not ${bitBound}`);
1081
+ }
1082
+ if (bits < 0n || bits >> BigInt(bitBound) !== 0n) {
1083
+ this.refuse(`a bitmask below 2^${bitBound}, not ${bits}`);
1084
+ }
1085
+ if (bitBound <= 8) {
1086
+ this.writeU8(Number(bits));
1087
+ }
1088
+ else if (bitBound <= 16) {
1089
+ this.writeU16(Number(bits));
1090
+ }
1091
+ else if (bitBound <= 32) {
1092
+ this.writeU32(Number(bits));
1093
+ }
1094
+ else {
1095
+ this.writeU64(bits);
1096
+ }
1097
+ }
1098
+ /**
1099
+ * One bitfield, shifted into the accumulator a bitset packs into.
1100
+ *
1101
+ * The counterpart of `zdw_bitfield` and of Java's `Writer.bitfield`.
1102
+ * A method of the writer and not a free function, because the shared
1103
+ * walk emits it as a call on the writer — `call_expr` in
1104
+ * `crates/lang-rules/src/emit/encode.rs` is `T::runtime("w", …)` — and
1105
+ * a free function of that name would be a second `bitfield` the
1106
+ * generated code never reaches.
1107
+ *
1108
+ * A value wider than the field is refused rather than masked: masking
1109
+ * would put a different number on the wire than the caller asked for,
1110
+ * and the caller would never find out.
1111
+ */
1112
+ bitfield(value, width, position) {
1113
+ if (width <= 0 || width > 64 || position < 0 || position + width > 64) {
1114
+ this.refuse(`a bitfield of ${width} bits at ${position}, inside 64`);
1115
+ }
1116
+ const limit = (1n << BigInt(width)) - 1n;
1117
+ if (value < 0n || value > limit) {
1118
+ this.refuse(`a value inside ${width} bits, not ${value}`);
1119
+ }
1120
+ return value << BigInt(position);
1121
+ }
1122
+ /**
1123
+ * A bitset, as the anchor integer its bitfields pack into.
1124
+ *
1125
+ * DDS-XTypes 1.3 has no serialisation rule for a bitset — §7.4.3.5.3
1126
+ * runs from rule (1) to rule (30) and no left-hand side matches one.
1127
+ * How wide the anchor is comes from the layout planner and arrives
1128
+ * here as a count of **octets**, not of bits: `zerodds_ast_layout`'s
1129
+ * `Extent::Sized` is an octet count and the shared walk passes it
1130
+ * through. A runtime that read it as a bit count answered "8" for an
1131
+ * eight-octet anchor and wrote one octet where the plan said eight.
1132
+ */
1133
+ writeBitset(anchorOctets, bits) {
1134
+ switch (anchorOctets) {
1135
+ case 1:
1136
+ this.writeBitmask(8, bits);
1137
+ return;
1138
+ case 2:
1139
+ this.writeBitmask(16, bits);
1140
+ return;
1141
+ case 4:
1142
+ this.writeBitmask(32, bits);
1143
+ return;
1144
+ case 8:
1145
+ this.writeBitmask(64, bits);
1146
+ return;
1147
+ default:
1148
+ this.refuse(`a bitset anchor of 1, 2, 4 or 8 octets, not ${anchorOctets}`);
1149
+ }
1150
+ }
1151
+ // -- patching --------------------------------------------------------
1152
+ patch(at, value, width) {
1153
+ let rest = BigInt(value) & ((1n << BigInt(width * 8)) - 1n);
1154
+ const octets = new Array(width);
1155
+ for (let index = 0; index < width; index += 1) {
1156
+ octets[index] = Number(rest & 0xffn);
1157
+ rest >>= 8n;
1158
+ }
1159
+ if (!isLittle(this.order)) {
1160
+ octets.reverse();
1161
+ }
1162
+ for (let index = 0; index < width; index += 1) {
1163
+ this.out[at + index] = octets[index] ?? 0;
1164
+ }
1165
+ }
1166
+ read(at, width) {
1167
+ const octets = this.out.slice(at, at + width);
1168
+ if (!isLittle(this.order)) {
1169
+ octets.reverse();
1170
+ }
1171
+ let value = 0;
1172
+ for (let index = width - 1; index >= 0; index -= 1) {
1173
+ value = value * 256 + (octets[index] ?? 0);
1174
+ }
1175
+ return value >>> 0;
1176
+ }
1177
+ // -- the KeyHash -------------------------------------------------------
1178
+ /**
1179
+ * The holder as a KeyHash, zero-padded to sixteen octets.
1180
+ *
1181
+ * For a holder of sixteen octets or fewer. DDSI-RTPS 2.5 §9.6.3.8.
1182
+ */
1183
+ zeroPadded() {
1184
+ const hash = new Uint8Array(KEY_HASH_LEN);
1185
+ hash.set(this.out.slice(0, KEY_HASH_LEN));
1186
+ return hash;
1187
+ }
1188
+ /**
1189
+ * The MD5 of the holder, for one longer than sixteen octets.
1190
+ *
1191
+ * MD5 because DDSI-RTPS 2.5 §9.6.3.8 names it, not as a security claim:
1192
+ * the KeyHash identifies an instance and is not a signature.
1193
+ */
1194
+ md5() {
1195
+ return md5(this.finish());
1196
+ }
1197
+ }
1198
+ /**
1199
+ * The MD5 a KeyHash is made of. RFC 1321.
1200
+ *
1201
+ * Hand-rolled rather than a call into a platform digest, for the same
1202
+ * reason `runtime/csharp`'s `Md5.cs` gives and not `runtime/java`'s: this
1203
+ * runtime is isomorphic (Node, a browser, `runtime/typescript`'s own WASM
1204
+ * bridge in `wasm.ts`), and the Web Crypto API a browser exposes has no
1205
+ * MD5 at all — `SubtleCrypto.digest` supports only the SHA family — so
1206
+ * there is no single platform primitive to call from every environment
1207
+ * this package runs in, unlike Java's `MessageDigest` or even C#'s
1208
+ * FIPS-mode-sensitive one. `WireSelfTest`'s Java and C# counterparts check
1209
+ * RFC 1321's own test vectors; `against-recorded-values.ts` does the same
1210
+ * here, so the arithmetic is asserted and not assumed.
1211
+ *
1212
+ * Not a security primitive here and not used as one: the specification
1213
+ * names MD5 for this, and a different digest would produce hashes that no
1214
+ * other vendor agrees with. DDSI-RTPS 2.5 §9.6.3.8.
1215
+ */
1216
+ export function md5(input) {
1217
+ const s = [
1218
+ 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
1219
+ 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
1220
+ 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
1221
+ 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
1222
+ ];
1223
+ const k = [
1224
+ 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee,
1225
+ 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
1226
+ 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be,
1227
+ 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
1228
+ 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa,
1229
+ 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
1230
+ 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed,
1231
+ 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
1232
+ 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c,
1233
+ 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
1234
+ 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05,
1235
+ 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
1236
+ 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039,
1237
+ 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
1238
+ 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1,
1239
+ 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391,
1240
+ ];
1241
+ const bitLength = BigInt(input.length) * 8n;
1242
+ let paddedLength = input.length + 1;
1243
+ paddedLength += (56 - (paddedLength % 64) + 64) % 64;
1244
+ const message = new Uint8Array(paddedLength + 8);
1245
+ message.set(input);
1246
+ message[input.length] = 0x80;
1247
+ for (let index = 0; index < 8; index += 1) {
1248
+ message[paddedLength + index] = Number((bitLength >> BigInt(8 * index)) & 0xffn);
1249
+ }
1250
+ let a0 = 0x67452301;
1251
+ let b0 = 0xefcdab89;
1252
+ let c0 = 0x98badcfe;
1253
+ let d0 = 0x10325476;
1254
+ const rotl = (value, count) => ((value << count) | (value >>> (32 - count))) >>> 0;
1255
+ for (let chunkStart = 0; chunkStart < message.length; chunkStart += 64) {
1256
+ const words = new Array(16);
1257
+ for (let word = 0; word < 16; word += 1) {
1258
+ const at = chunkStart + word * 4;
1259
+ words[word] =
1260
+ ((message[at] ?? 0) |
1261
+ ((message[at + 1] ?? 0) << 8) |
1262
+ ((message[at + 2] ?? 0) << 16) |
1263
+ ((message[at + 3] ?? 0) << 24)) >>>
1264
+ 0;
1265
+ }
1266
+ let [a, b, c, d] = [a0, b0, c0, d0];
1267
+ for (let step = 0; step < 64; step += 1) {
1268
+ let mixed;
1269
+ let which;
1270
+ if (step < 16) {
1271
+ mixed = ((b & c) | (~b & d)) >>> 0;
1272
+ which = step;
1273
+ }
1274
+ else if (step < 32) {
1275
+ mixed = ((d & b) | (~d & c)) >>> 0;
1276
+ which = (5 * step + 1) % 16;
1277
+ }
1278
+ else if (step < 48) {
1279
+ mixed = (b ^ c ^ d) >>> 0;
1280
+ which = (3 * step + 5) % 16;
1281
+ }
1282
+ else {
1283
+ mixed = (c ^ (b | ~d)) >>> 0;
1284
+ which = (7 * step) % 16;
1285
+ }
1286
+ const sum = (a + mixed + (k[step] ?? 0) + (words[which] ?? 0)) >>> 0;
1287
+ const rotated = rotl(sum, s[step] ?? 0);
1288
+ [a, b, c, d] = [d, (b + rotated) >>> 0, b, c];
1289
+ }
1290
+ a0 = (a0 + a) >>> 0;
1291
+ b0 = (b0 + b) >>> 0;
1292
+ c0 = (c0 + c) >>> 0;
1293
+ d0 = (d0 + d) >>> 0;
1294
+ }
1295
+ const digest = new Uint8Array(16);
1296
+ const store = (at, value) => {
1297
+ digest[at] = value & 0xff;
1298
+ digest[at + 1] = (value >>> 8) & 0xff;
1299
+ digest[at + 2] = (value >>> 16) & 0xff;
1300
+ digest[at + 3] = (value >>> 24) & 0xff;
1301
+ };
1302
+ store(0, a0);
1303
+ store(4, b0);
1304
+ store(8, c0);
1305
+ store(12, d0);
1306
+ return digest;
1307
+ }
1308
+ /**
1309
+ * The low `octets` of `value`'s two's-complement bits, zero-extended —
1310
+ * {@link Writer.writeUnionOverlay}'s pattern for an integer, character,
1311
+ * enumeration or bitmask arm. The mask is the function: `-2` in four
1312
+ * octets has to come out `0x00000000fffffffen`.
1313
+ */
1314
+ export function bitPatternInt(value, octets) {
1315
+ const pattern = BigInt.asUintN(64, value);
1316
+ return octets >= 8 ? pattern : pattern & ((1n << BigInt(octets * 8)) - 1n);
1317
+ }
1318
+ /** One octet, 0 or 1 — the boolean overlay pattern. */
1319
+ export function bitPatternBool(value) {
1320
+ return value ? 1n : 0n;
1321
+ }
1322
+ /** The IEEE 754 binary32 bits, zero-extended. */
1323
+ export function bitPatternF32(value) {
1324
+ const buffer = new DataView(new ArrayBuffer(4));
1325
+ buffer.setFloat32(0, value, true);
1326
+ return BigInt(buffer.getUint32(0, true));
1327
+ }
1328
+ /** The IEEE 754 binary64 bits. */
1329
+ export function bitPatternF64(value) {
1330
+ const buffer = new DataView(new ArrayBuffer(8));
1331
+ buffer.setFloat64(0, value, true);
1332
+ return buffer.getBigUint64(0, true);
1333
+ }