@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/README.md +13 -2
- package/dist/branded.d.ts +98 -0
- package/dist/branded.js +64 -0
- package/dist/dds_any.d.ts +47 -0
- package/dist/dds_any.js +41 -0
- package/dist/endianness.d.ts +22 -0
- package/dist/endianness.js +35 -0
- package/dist/equal.d.ts +46 -0
- package/dist/equal.js +273 -0
- package/dist/error.d.ts +52 -0
- package/dist/error.js +78 -0
- package/dist/float128.d.ts +15 -0
- package/dist/float128.js +181 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +33 -0
- package/dist/operations.d.ts +180 -0
- package/dist/operations.js +74 -0
- package/dist/policy.d.ts +133 -0
- package/dist/policy.js +184 -0
- package/dist/reader.d.ts +605 -0
- package/dist/reader.js +1345 -0
- package/dist/registry.d.ts +84 -0
- package/dist/registry.js +166 -0
- package/dist/test_backend.d.ts +9 -0
- package/dist/test_backend.js +173 -0
- package/dist/types.d.ts +246 -0
- package/dist/types.js +3 -0
- package/dist/wasm.d.ts +203 -0
- package/dist/wasm.js +194 -0
- package/dist/writer.d.ts +662 -0
- package/dist/writer.js +1333 -0
- package/package.json +27 -4
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
|
+
}
|