zerodds_wire_codec/value.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! What a probe is asked for.
4
5use zerodds_ast::Extensibility;
6use zerodds_ast_layout::{
7 AggregateDelimiter, CollectionDelimiterPolicy, DefaultedDiscriminatorMember, Dialect,
8 ElementPadding, Encoding, Endianness, EnumHolderWidth, EnumeratorValue, FixedDigitCount,
9 KeyHolderAlignment, KeyHolderExtensibility, KeyHolderNesting, KeyHolderOrder, LayoutKey,
10 LengthCodePolicy, MapDelimiter, MapEntryOrder, MustUnderstandPolicy, MutableUnionFraming,
11 PidFlagPolicy, PlCdr1HeaderPolicy, PlCdr1Reading, Purpose, SequenceElementDelimiter,
12 ShortPidRange, UnionLabelMatch, WcharFraming, WcharWidth, WstringCodeUnits,
13 WstringLengthPolicy, WstringUnitOrder,
14};
15use zerodds_wire::{
16 PidFlagPolicy as WirePidFlagPolicy, PlCdr1Framing, PlCdr1Reading as WirePlCdr1Reading,
17 WcharFraming as WireWcharFraming, WcharWidth as WireWcharWidth,
18 WstringUnitOrder as WireWstringUnitOrder,
19};
20
21/// Whether a delimited `map` carries an entry count.
22///
23/// This policy exists because XTypes 1.3 contradicts itself, not
24/// because two implementations were measured disagreeing. Nothing in
25/// `crates/ast-layout/tests/fixtures/vendor_bytes.md` has a map in it,
26/// so neither shape below is verified against a vendor.
27///
28/// §7.4.1.1.4 defines a map by an equivalence: it "shall be serialized
29/// as if" it were `sequence<MapEntry_<key_type>_<value_type>>`. A
30/// sequence under XCDR2 is rule (12) of §7.4.3.5.3, `DHEADER` then
31/// `O.length` then the elements. So the equivalence yields both words.
32///
33/// Rule (15) of the same clause, which is the rule written for
34/// `MAP_TYPE` itself under XCDR2, writes `DHEADER` and then the pairs —
35/// with no `O.length`. Rules (14) and (16), the primitive-map and the
36/// XCDR1 cases, both write `O.length` and no delimiter, so the omission
37/// is only in (15).
38///
39/// The two readings differ by four bytes. Both are decodable — with a
40/// delimiter a reader can consume entries until the byte extent is
41/// used — which is why neither can be ruled out from the stream alone.
42///
43/// # Both are in the field
44///
45/// Measured on 2026-08-09. Neither C generator parses `map`, so the
46/// implementations that can be asked are eProsima Fast DDS and OpenDDS
47/// 3.34.0, and they took one reading each. `@final struct { map<long,
48/// Cell> entries; long tail; }` with one entry `5 -> {1, 2}`, XCDR2
49/// little endian: Fast DDS writes `10000000 01000000 05000000 01000000
50/// 02000000 07000000` and OpenDDS writes `0c000000 05000000 01000000
51/// 02000000 07000000`. Under XCDR1, where neither delimits, the two are
52/// identical. `crates/ast-layout/tests/fixtures/vendor_bytes.md`,
53/// "Where the three cannot answer", has the run and a second shape.
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
55pub enum MapLengthPolicy {
56 /// Write the entry count whether or not a delimiter precedes it.
57 ///
58 /// The default, because it is the reading under which §7.4.1.1.4's
59 /// equivalence holds, because it is the only one of the two that
60 /// makes a map's bytes the same shape as the sequence the clause
61 /// says it is, and because it is what Fast DDS writes.
62 #[default]
63 SequenceEquivalent,
64 /// Omit the entry count when the map is delimited, as rule (15) is
65 /// written.
66 ///
67 /// OpenDDS's reading: its generated `operator<<` calls
68 /// `write_delimiter` under XCDR2 and the `<< length` in the `else`
69 /// branch under XCDR1, so the count and the delimiter are
70 /// alternatives rather than both.
71 Rule15Omits,
72}
73
74impl MapLengthPolicy {
75 /// Both readings.
76 pub const ALL: [Self; 2] = [Self::SequenceEquivalent, Self::Rule15Omits];
77
78 /// Whether this is the default.
79 #[must_use]
80 pub const fn is_default(self) -> bool {
81 matches!(self, Self::SequenceEquivalent)
82 }
83
84 /// Short name, for configuration and diagnostics.
85 #[must_use]
86 pub const fn as_str(self) -> &'static str {
87 match self {
88 Self::SequenceEquivalent => "sequence-equivalent",
89 Self::Rule15Omits => "rule-15-omits",
90 }
91 }
92
93 /// Parse a configuration value.
94 ///
95 /// An unknown value returns `None` rather than the default: a typo
96 /// in a wire-affecting setting must be loud.
97 #[must_use]
98 pub fn parse(value: &str) -> Option<Self> {
99 Self::ALL
100 .into_iter()
101 .find(|policy| policy.as_str() == value)
102 }
103}
104
105/// What the probe writes out.
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
107pub enum Emit {
108 /// The serialised sample, with no encapsulation header.
109 ///
110 /// The default, because it is the part two implementations have to
111 /// agree on byte for byte. The header says which encoding was
112 /// used, which the caller already chose.
113 #[default]
114 Payload,
115 /// The sample behind its four-byte encapsulation header.
116 PayloadWithEncapsulation,
117 /// The sixteen-byte KeyHash. DDSI-RTPS 2.5 §9.6.3.8.
118 KeyHash,
119}
120
121impl Emit {
122 /// Every form.
123 pub const ALL: [Self; 3] = [Self::Payload, Self::PayloadWithEncapsulation, Self::KeyHash];
124
125 /// The name the command line uses.
126 #[must_use]
127 pub const fn as_str(self) -> &'static str {
128 match self {
129 Self::Payload => "payload",
130 Self::PayloadWithEncapsulation => "payload+encapsulation",
131 Self::KeyHash => "keyhash",
132 }
133 }
134
135 /// Parse a command-line value, rejecting anything else.
136 #[must_use]
137 pub fn parse(value: &str) -> Option<Self> {
138 Self::ALL.into_iter().find(|form| form.as_str() == value)
139 }
140}
141
142/// One probe run.
143///
144/// Every field a foreign probe also takes, so the four command lines
145/// are the same shape. A field that only one implementation
146/// understands would make the matrix unreadable.
147#[derive(Debug, Clone)]
148pub struct ProbeRequest {
149 /// The type, by scoped name — `common::Robot`.
150 pub type_name: String,
151 /// XCDR1 or XCDR2.
152 pub encoding: Encoding,
153 /// Byte order.
154 pub endianness: Endianness,
155 /// What to write out.
156 pub emit: Emit,
157 /// How variable-length `@mutable` members are framed.
158 pub length_codes: LengthCodePolicy,
159 /// Which collections carry a delimiter.
160 pub collection_delimiters: CollectionDelimiterPolicy,
161 /// Which of a `map`'s halves decides whether the map is delimited.
162 pub map_delimiter: MapDelimiter,
163 /// How a `wstring`'s length is counted.
164 pub wstring_length: WstringLengthPolicy,
165 /// How a scalar `wchar` is framed.
166 pub wchar_framing: WcharFraming,
167 /// How wide a scalar `wchar` is.
168 pub wchar_width: WcharWidth,
169 /// In what byte order a `wstring`'s code units go out.
170 pub wstring_unit_order: WstringUnitOrder,
171 /// How a `@mutable` member is framed under XCDR1.
172 pub pl_cdr1_headers: PlCdr1HeaderPolicy,
173 /// What flags a PL_CDR1 parameter id carries, per position.
174 pub pid_flags: PidFlagPolicy,
175 /// How tolerant the PL_CDR1 reader is of another framing.
176 pub pl_cdr1_reading: PlCdr1Reading,
177 /// Which table the identifier of an encapsulation header is from.
178 pub encapsulation_ids: EncapsulationIds,
179 /// Whether a member's `must_understand` reaches the wire.
180 pub must_understand: MustUnderstandPolicy,
181 /// How wide an enumeration's holder is.
182 pub enum_holder: EnumHolderWidth,
183 /// Whether a negative case label of a narrow signed discriminator
184 /// selects its branch.
185 pub union_label: UnionLabelMatch,
186 /// In what order a `map`'s entries go on the wire.
187 pub map_entries: MapEntryOrder,
188 /// Whether an array's element is flattened before the delimiter
189 /// question. See `ArrayElementFlattening`.
190 pub array_element_flattening: zerodds_ast_layout::key::ArrayElementFlattening,
191 /// Which integer an enumerator serialises as.
192 pub enumerator_value: EnumeratorValue,
193 /// How a `@mutable` union numbers its branches, and whether its
194 /// XCDR1 parameter list is terminated.
195 pub union_framing: MutableUnionFraming,
196 /// Which member ids fit a PL_CDR1 short parameter header.
197 pub short_pid_range: ShortPidRange,
198 /// Whether a `fixed<P,S>` carries its declared digit count or the
199 /// value's.
200 pub fixed_digits: FixedDigitCount,
201 /// What an aggregate whose source said nothing is taken to be.
202 pub default_extensibility: Extensibility,
203 /// Whether a key holder keeps the type's extensibility.
204 pub key_holder: KeyHolderExtensibility,
205 /// How deep a key holder's member removal reaches.
206 pub key_holder_nesting: KeyHolderNesting,
207 /// In what order a key holder's members are serialized.
208 pub key_holder_order: KeyHolderOrder,
209 /// Whether a key holder's members are aligned.
210 pub key_holder_alignment: KeyHolderAlignment,
211 /// What becomes of a union's member when `@try_construct(use_default)`
212 /// defaulted its discriminator.
213 pub defaulted_discriminator_member: DefaultedDiscriminatorMember,
214 /// Whether a delimited `map` also carries an entry count.
215 pub map_length: MapLengthPolicy,
216 /// Whether a collection's elements carry alignment padding between
217 /// them.
218 pub element_padding: ElementPadding,
219 /// Whether an aggregate's declared extensibility reaches the wire.
220 pub aggregate_delimiter: AggregateDelimiter,
221 /// Whether a `sequence` element of a non-`@final` type is preceded
222 /// by its own octet length.
223 pub sequence_element_delimiter: SequenceElementDelimiter,
224 /// Which octets stand for a `wstring`'s characters.
225 pub wstring_code_units: WstringCodeUnits,
226}
227
228impl ProbeRequest {
229 /// A request for `type_name` in the given encoding and byte order,
230 /// in the default dialect.
231 #[must_use]
232 pub fn new(type_name: &str, encoding: Encoding, endianness: Endianness) -> Self {
233 Self::in_dialect(type_name, encoding, endianness, Dialect::default())
234 }
235
236 /// A request with every wire policy taken from one dialect.
237 #[must_use]
238 pub fn in_dialect(
239 type_name: &str,
240 encoding: Encoding,
241 endianness: Endianness,
242 dialect: Dialect,
243 ) -> Self {
244 let key = LayoutKey::in_dialect(dialect, encoding, endianness, Purpose::Data);
245 let map_length = if dialect.map_count_omitted_when_delimited() {
246 MapLengthPolicy::Rule15Omits
247 } else {
248 MapLengthPolicy::SequenceEquivalent
249 };
250 Self::from_key(type_name, key, map_length)
251 }
252
253 /// A request for `type_name` planned under `key`, with `map_length`
254 /// standing in for the one framing question `key` does not answer
255 /// (see [`MapLengthPolicy`]'s own documentation for why the entry
256 /// count is a codec question and not a layout one).
257 ///
258 /// The field-by-field mapping from a `LayoutKey` to a `ProbeRequest`
259 /// lives here once. [`Self::in_dialect`] is this plus the one
260 /// `LayoutKey::in_dialect` call a dialect alone needs; a caller that
261 /// has already built a key — a dialect with axis flags overriding
262 /// some of its answers, as `zerodds_ast_layout::axes` produces —
263 /// calls this directly instead of re-deriving the same fields by
264 /// hand.
265 #[must_use]
266 pub fn from_key(type_name: &str, key: LayoutKey, map_length: MapLengthPolicy) -> Self {
267 Self {
268 type_name: type_name.to_string(),
269 encoding: key.encoding,
270 endianness: key.endianness,
271 emit: Emit::Payload,
272 length_codes: key.length_codes,
273 collection_delimiters: key.collection_delimiters,
274 map_delimiter: key.map_delimiter,
275 wstring_length: key.wstring_length,
276 wchar_framing: key.wchar_framing,
277 wchar_width: key.wchar_width,
278 wstring_unit_order: key.wstring_unit_order,
279 pl_cdr1_headers: key.pl_cdr1_headers,
280 pid_flags: key.pid_flags,
281 pl_cdr1_reading: key.pl_cdr1_reading,
282 encapsulation_ids: EncapsulationIds::default(),
283 must_understand: key.must_understand,
284 enum_holder: key.enum_holder,
285 union_label: key.union_label,
286 map_entries: key.map_entries,
287 array_element_flattening: key.array_element_flattening,
288 enumerator_value: key.enumerator_value,
289 union_framing: key.union_framing,
290 short_pid_range: key.short_pid_range,
291 fixed_digits: key.fixed_digits,
292 default_extensibility: key.default_extensibility,
293 key_holder: key.key_holder,
294 key_holder_nesting: key.key_holder_nesting,
295 key_holder_order: key.key_holder_order,
296 key_holder_alignment: key.key_holder_alignment,
297 defaulted_discriminator_member: key.defaulted_discriminator_member,
298 map_length,
299 element_padding: key.element_padding,
300 aggregate_delimiter: key.aggregate_delimiter,
301 sequence_element_delimiter: key.sequence_element_delimiter,
302 wstring_code_units: key.wstring_code_units,
303 }
304 }
305
306 /// The same request writing something else out.
307 #[must_use]
308 pub fn emitting(mut self, emit: Emit) -> Self {
309 self.emit = emit;
310 self
311 }
312
313 /// The same request with a PL_CDR1 reader that takes, or reports, a
314 /// stream in another framing.
315 #[must_use]
316 pub fn with_pl_cdr1_reading(mut self, reading: PlCdr1Reading) -> Self {
317 self.pl_cdr1_reading = reading;
318 self
319 }
320
321 /// The same request writing the identifiers of an encapsulation header
322 /// from another table.
323 #[must_use]
324 pub fn with_encapsulation_ids(mut self, ids: EncapsulationIds) -> Self {
325 self.encapsulation_ids = ids;
326 self
327 }
328
329 /// How tolerant the PL_CDR1 reader is, as the byte layer names it.
330 #[must_use]
331 pub const fn wire_pl_cdr1_reading(&self) -> WirePlCdr1Reading {
332 match self.pl_cdr1_reading {
333 PlCdr1Reading::Strict => WirePlCdr1Reading::Strict,
334 PlCdr1Reading::Tolerant => WirePlCdr1Reading::Tolerant,
335 }
336 }
337
338 /// The layout key this request plans under.
339 ///
340 /// One place, so that the write direction, the read direction and
341 /// the key holder cannot each assemble a slightly different key out
342 /// of the same fields.
343 #[must_use]
344 pub fn layout_key(&self, purpose: Purpose) -> LayoutKey {
345 LayoutKey {
346 encoding: self.encoding,
347 endianness: self.endianness,
348 purpose,
349 length_codes: self.length_codes,
350 collection_delimiters: self.collection_delimiters,
351 map_delimiter: self.map_delimiter,
352 wstring_length: self.wstring_length,
353 wchar_framing: self.wchar_framing,
354 wchar_width: self.wchar_width,
355 wstring_unit_order: self.wstring_unit_order,
356 pl_cdr1_headers: self.pl_cdr1_headers,
357 pid_flags: self.pid_flags,
358 pl_cdr1_reading: self.pl_cdr1_reading,
359 must_understand: self.must_understand,
360 enum_holder: self.enum_holder,
361 union_label: self.union_label,
362 map_entries: self.map_entries,
363 array_element_flattening: self.array_element_flattening,
364 enumerator_value: self.enumerator_value,
365 union_framing: self.union_framing,
366 short_pid_range: self.short_pid_range,
367 fixed_digits: self.fixed_digits,
368 default_extensibility: self.default_extensibility,
369 key_holder: self.key_holder,
370 key_holder_nesting: self.key_holder_nesting,
371 key_holder_order: self.key_holder_order,
372 key_holder_alignment: self.key_holder_alignment,
373 defaulted_discriminator_member: self.defaulted_discriminator_member,
374 element_padding: self.element_padding,
375 aggregate_delimiter: self.aggregate_delimiter,
376 sequence_element_delimiter: self.sequence_element_delimiter,
377 wstring_code_units: self.wstring_code_units,
378 }
379 }
380
381 /// The key-holder key this request plans under.
382 ///
383 /// The same dialect as the payload: §7.6.8 fixes the holder's byte
384 /// order and encoding, not what an unannotated aggregate inside it
385 /// is taken to be, and that decides whether the holder carries a
386 /// DHEADER. A probe that wrote one dialect's payload and another's
387 /// KeyHash would report a divergence of its own making.
388 #[must_use]
389 pub fn key_holder_key(&self) -> LayoutKey {
390 LayoutKey {
391 endianness: Endianness::Big,
392 ..self.layout_key(Purpose::Key)
393 }
394 }
395
396 /// How wide a `Char16` is for this request.
397 #[must_use]
398 pub const fn wchar_width(&self) -> WireWcharWidth {
399 wire_wchar_width_of(self.wchar_width)
400 }
401
402 /// How a `Char16` is framed for this request, as the byte layer
403 /// names it.
404 #[must_use]
405 pub const fn wire_wchar_framing(&self) -> WireWcharFraming {
406 wire_wchar_framing_of(self.wchar_framing)
407 }
408
409 /// In what byte order wide code units go out, as the byte layer
410 /// names it.
411 #[must_use]
412 pub const fn wire_wstring_unit_order(&self) -> WireWstringUnitOrder {
413 match self.wstring_unit_order {
414 WstringUnitOrder::Stream => WireWstringUnitOrder::Stream,
415 WstringUnitOrder::BigEndianAlways => WireWstringUnitOrder::BigEndianAlways,
416 WstringUnitOrder::ByteOrderMark => WireWstringUnitOrder::ByteOrderMark,
417 WstringUnitOrder::HostUnswapped => WireWstringUnitOrder::HostUnswapped,
418 }
419 }
420
421 /// How a writer serving this request frames a PL_CDR1 parameter
422 /// list.
423 ///
424 /// The header *form* is in the plan, because it changes a member's
425 /// extent; these four do not, so they ride on the writer. Both
426 /// come from the same policy, which is what keeps them agreeing.
427 #[must_use]
428 pub fn pl_cdr1_framing(&self) -> PlCdr1Framing {
429 let values = self.layout_key(Purpose::Data).pl_cdr1_framing();
430 PlCdr1Framing {
431 pid_flags: wire_pid_flags_of(values.pid_flags),
432 highest_short_id: values.highest_short_id,
433 pad_declared_length: values.pad_declared_length,
434 pad_declared_optional_length: values.pad_declared_optional_length,
435 member_must_understand: values.member_must_understand,
436 require_extended: values.require_extended,
437 pad_after_value: values.pad_after_value,
438 }
439 }
440}
441
442/// The byte layer's name for a `Char16` framing.
443///
444/// A free function as well as a method because the key-holder writer is
445/// built from a `LayoutKey` and not from a `ProbeRequest`, and a holder
446/// whose framing disagreed with its own plan would put every member
447/// after a `wchar` in the wrong place.
448///
449/// The position axis is not resolved here. Both layers carry all three
450/// variants, so the writer answers the position at the call and this
451/// function stays a rename.
452#[must_use]
453pub const fn wire_wchar_framing_of(framing: WcharFraming) -> WireWcharFraming {
454 match framing {
455 WcharFraming::Fixed => WireWcharFraming::Fixed,
456 WcharFraming::GiopLengthPrefixed => WireWcharFraming::GiopLengthPrefixed,
457 WcharFraming::GiopLengthPrefixedMemberOnly => {
458 WireWcharFraming::GiopLengthPrefixedMemberOnly
459 }
460 }
461}
462
463/// The byte layer's name for a wide code-unit order. As
464/// [`wire_wchar_framing_of`].
465#[must_use]
466pub const fn wire_wstring_unit_order_of(order: WstringUnitOrder) -> WireWstringUnitOrder {
467 match order {
468 WstringUnitOrder::Stream => WireWstringUnitOrder::Stream,
469 WstringUnitOrder::BigEndianAlways => WireWstringUnitOrder::BigEndianAlways,
470 WstringUnitOrder::ByteOrderMark => WireWstringUnitOrder::ByteOrderMark,
471 WstringUnitOrder::HostUnswapped => WireWstringUnitOrder::HostUnswapped,
472 }
473}
474
475/// The byte layer's name for a PID-flag policy. As
476/// [`wire_wchar_framing_of`].
477///
478/// The position is not resolved here either: both layers carry all the
479/// positions, the writer and the reader ask at the call, and this is a
480/// rename.
481#[must_use]
482pub const fn wire_pid_flags_of(policy: PidFlagPolicy) -> WirePidFlagPolicy {
483 match policy {
484 PidFlagPolicy::SentinelMustUnderstand => WirePidFlagPolicy::SentinelMustUnderstand,
485 PidFlagPolicy::Bare => WirePidFlagPolicy::Bare,
486 PidFlagPolicy::ImplExtensionOnMembers => WirePidFlagPolicy::ImplExtensionOnMembers,
487 }
488}
489
490/// A scalar `wchar`'s width, as the byte layer names it.
491///
492/// It used to be read off [`WstringLengthPolicy::char16_size`], on the
493/// reading that the scalar and the `wstring`'s code units cannot
494/// disagree. They can: `rmw_cyclonedds_cpp` writes two octets for a
495/// `wchar` member and four for a `wstring`'s units, so the width is its
496/// own axis and this only carries it down to `zerodds-wire`, which
497/// knows nothing about a policy.
498#[must_use]
499pub const fn wire_wchar_width_of(width: WcharWidth) -> WireWcharWidth {
500 match width {
501 WcharWidth::Two => WireWcharWidth::Two,
502 WcharWidth::Four => WireWcharWidth::Four,
503 }
504}
505
506/// How many zero bytes bring `length` up to the next multiple of four.
507///
508/// XTypes 1.3 §7.4.1.1: the serialized data is padded to a four-byte
509/// boundary, and §7.6.3.1.2 puts the count of those bytes in the
510/// encapsulation options so a receiver can find where the data ended.
511/// Measured on the wire, 2026-08-09: Cyclone, RTI and Fast DDS all pad
512/// the payload and all three state the count.
513#[must_use]
514pub const fn encapsulation_padding(length: usize) -> u8 {
515 // `& 3` and not a modulo of the length: the count is what the two
516 // bits can hold, and the two bits are the field.
517 ((4 - (length & 3)) & 3) as u8
518}
519
520/// The encapsulation header for an encoding and byte order.
521///
522/// DDSI-RTPS 2.5 §10.5 and XTypes 1.3 §7.4.1.1: the identifier, then
523/// two option bytes carrying the payload's trailing padding count.
524///
525/// # The options field is not decoration
526///
527/// §7.6.3.1.2 makes the low two bits of the second options byte the
528/// number of padding bytes at the end of the serialized data, and says
529/// they "shall be interpreted by the receiver to determine where the
530/// serialized data exactly ended". Leaving them zero over a padded
531/// payload tells a receiver that reads them that the padding is data.
532///
533/// Measured on the loopback wire on 2026-08-09, `@final struct One {
534/// octet a; }` with `a = 0xAB`, captured with `tshark`:
535///
536/// ```text
537/// cyclone CDR_LE options 0x0003 serializedData ab000000
538/// rti CDR_LE options 0x0003 serializedData ab000000
539/// fastdds CDR_LE options 0x0003 serializedData ab000000
540/// ```
541///
542/// and `@final struct TypeB { short a; char b; }` — §7.6.3.1.2's own
543/// example — through Cyclone: `options 0x0001`, `serializedData
544/// 23006200`. Fast CDR 2.3.5 `Cdr::set_dds_cdr_options` computes the
545/// same count for bodies of 1, 2, 3 and 4 bytes (3, 2, 1, 0), and
546/// Cyclone's `dds_serdata_default.c` writes `ddsrt_toBE2u(pad)` into
547/// `hdr.options` and reads the data back as `d->pos - pad`.
548///
549/// RTI's static `Plugin_serialize_to_cdr_buffer_ex` leaves the field
550/// zero and does not pad; its RTPS layer sets both, which is what the
551/// capture above shows. The field belongs to the encapsulated form, so
552/// `Emit::Payload` — the unpadded serialized data, which is what every
553/// row of `vendor_bytes.md` records — is unaffected.
554///
555/// The word is big-endian like the identifier: Cyclone writes
556/// `ddsrt_toBE2u`, Fast CDR serialises `options_[0]` then `options_[1]`
557/// whatever the payload's order, and both put the count in the second
558/// byte.
559#[must_use]
560pub fn encapsulation_header(
561 encoding: Encoding,
562 endianness: Endianness,
563 little: bool,
564 padding: u8,
565) -> [u8; 4] {
566 encapsulation_header_in(
567 EncapsulationIds::Table60,
568 encoding,
569 endianness,
570 little,
571 padding,
572 )
573}
574
575/// Which table the identifiers of an encapsulation header are from.
576///
577/// DDS-XTypes 1.3 gives them twice, and the two tables do not agree for
578/// encoding version 2 or for XML: Table 39 (§7.4.3.4, the function
579/// `ENC_HEADER`) and DDSI-RTPS 2.5 Table 10.3 name `{0x00, 0x10}` to
580/// `{0x00, 0x15}` and `{0x01, 0x00}`; Table 60 (§7.6.3.1.2) names
581/// `{0x00, 0x06}` to `{0x00, 0x0b}` and `{0x00, 0x04}`. Cyclone DDS, RTI
582/// Connext and Fast DDS were measured writing Table 60's, which is the
583/// default; a reader here takes both
584/// ([`encapsulation_form`]).
585#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
586pub enum EncapsulationIds {
587 /// Table 60.
588 #[default]
589 Table60,
590 /// Table 39.
591 Table39,
592}
593
594impl EncapsulationIds {
595 /// Both tables.
596 pub const ALL: [Self; 2] = [Self::Table60, Self::Table39];
597
598 /// Whether this is the default.
599 #[must_use]
600 pub const fn is_default(self) -> bool {
601 matches!(self, Self::Table60)
602 }
603
604 /// Short name, for configuration and diagnostics.
605 #[must_use]
606 pub const fn as_str(self) -> &'static str {
607 match self {
608 Self::Table60 => "table-60",
609 Self::Table39 => "table-39",
610 }
611 }
612
613 /// Parse a configuration value.
614 ///
615 /// An unknown value returns `None` rather than the default: a typo in
616 /// a wire-affecting setting must be loud.
617 #[must_use]
618 pub fn parse(value: &str) -> Option<Self> {
619 Self::ALL.into_iter().find(|ids| ids.as_str() == value)
620 }
621
622 /// The two identifier octets of an encoding and a byte order.
623 #[must_use]
624 pub const fn identifier(self, encoding: Encoding, endianness: Endianness) -> [u8; 2] {
625 let id: u8 = match (self, encoding, endianness) {
626 (_, Encoding::PlainCdr1, Endianness::Big) => 0x00,
627 (_, Encoding::PlainCdr1, Endianness::Little) => 0x01,
628 (_, Encoding::PlCdr1, Endianness::Big) => 0x02,
629 (_, Encoding::PlCdr1, Endianness::Little) => 0x03,
630 (Self::Table60, Encoding::PlainCdr2, Endianness::Big) => 0x06,
631 (Self::Table60, Encoding::PlainCdr2, Endianness::Little) => 0x07,
632 (Self::Table60, Encoding::DelimitedCdr, Endianness::Big) => 0x08,
633 (Self::Table60, Encoding::DelimitedCdr, Endianness::Little) => 0x09,
634 (Self::Table60, Encoding::PlCdr2, Endianness::Big) => 0x0a,
635 (Self::Table60, Encoding::PlCdr2, Endianness::Little) => 0x0b,
636 (Self::Table39, Encoding::PlainCdr2, Endianness::Big) => 0x10,
637 (Self::Table39, Encoding::PlainCdr2, Endianness::Little) => 0x11,
638 (Self::Table39, Encoding::PlCdr2, Endianness::Big) => 0x12,
639 (Self::Table39, Encoding::PlCdr2, Endianness::Little) => 0x13,
640 (Self::Table39, Encoding::DelimitedCdr, Endianness::Big) => 0x14,
641 (Self::Table39, Encoding::DelimitedCdr, Endianness::Little) => 0x15,
642 };
643 [0x00, id]
644 }
645
646 /// The identifier of an XML-encoded sample: `{0x00, 0x04}` in
647 /// Table 60, `{0x01, 0x00}` in Table 39.
648 #[must_use]
649 pub const fn xml_identifier(self) -> [u8; 2] {
650 match self {
651 Self::Table60 => [0x00, 0x04],
652 Self::Table39 => [0x01, 0x00],
653 }
654 }
655}
656
657/// The encoding and byte order an encapsulation identifier names, from
658/// either table; `None` for an identifier of neither (XML included, which
659/// has no encoding of the kind [`Encoding`] names).
660///
661/// DDS-XTypes 1.3 Table 39 and Table 60 name the same six forms of
662/// version 2 by different identifiers; a reader that takes only one of the
663/// tables drops a sample the other table's writers send.
664#[must_use]
665pub const fn encapsulation_form(identifier: [u8; 2]) -> Option<(Encoding, Endianness)> {
666 if identifier[0] != 0x00 {
667 return None;
668 }
669 Some(match identifier[1] {
670 0x00 => (Encoding::PlainCdr1, Endianness::Big),
671 0x01 => (Encoding::PlainCdr1, Endianness::Little),
672 0x02 => (Encoding::PlCdr1, Endianness::Big),
673 0x03 => (Encoding::PlCdr1, Endianness::Little),
674 0x06 | 0x10 => (Encoding::PlainCdr2, Endianness::Big),
675 0x07 | 0x11 => (Encoding::PlainCdr2, Endianness::Little),
676 0x08 | 0x14 => (Encoding::DelimitedCdr, Endianness::Big),
677 0x09 | 0x15 => (Encoding::DelimitedCdr, Endianness::Little),
678 0x0a | 0x12 => (Encoding::PlCdr2, Endianness::Big),
679 0x0b | 0x13 => (Encoding::PlCdr2, Endianness::Little),
680 _ => return None,
681 })
682}
683
684/// [`encapsulation_header`] with the identifiers of the table named.
685#[must_use]
686pub fn encapsulation_header_in(
687 ids: EncapsulationIds,
688 encoding: Encoding,
689 endianness: Endianness,
690 little: bool,
691 padding: u8,
692) -> [u8; 4] {
693 // The identifier itself is always big-endian on the wire, whatever
694 // the payload behind it is — it is what tells a reader which order
695 // to switch to. `little` is the payload's order, not the header's.
696 let _ = little;
697 let [high, low] = ids.identifier(encoding, endianness);
698 [high, low, 0, padding & 0x3]
699}
700
701#[cfg(test)]
702mod tests {
703 #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
704 use super::*;
705
706 #[test]
707 fn the_emit_names_round_trip_and_a_typo_is_rejected() {
708 for emit in Emit::ALL {
709 assert_eq!(Emit::parse(emit.as_str()), Some(emit));
710 }
711 assert_eq!(Emit::parse("Payload"), None);
712 assert_eq!(Emit::parse(""), None);
713 }
714
715 #[test]
716 fn the_map_length_names_round_trip_and_a_typo_is_rejected() {
717 for policy in MapLengthPolicy::ALL {
718 assert_eq!(MapLengthPolicy::parse(policy.as_str()), Some(policy));
719 }
720 assert_eq!(MapLengthPolicy::parse("rule15-omits"), None);
721 assert!(MapLengthPolicy::default().is_default());
722 }
723
724 /// `zerodds_ast_layout::axes::MAP_LENGTH_VALUES` cannot name this
725 /// type — this crate depends on `zerodds-ast-layout`, so that
726 /// module naming this type back would be a cycle — and writes the
727 /// same two spellings out by hand instead. This is the other half
728 /// of that duplication: it fails the day `Self::as_str` changes a
729 /// spelling or `Self::ALL` grows a third reading and the axis
730 /// table's two literals were not told.
731 #[test]
732 fn map_length_spellings_match_the_axis_table() {
733 assert_eq!(MapLengthPolicy::ALL.len(), 2);
734 assert_eq!(
735 MapLengthPolicy::SequenceEquivalent.as_str(),
736 "sequence-equivalent"
737 );
738 assert_eq!(MapLengthPolicy::Rule15Omits.as_str(), "rule-15-omits");
739 }
740
741 /// `PlCdr1Framing::DEFAULT` is the default dialect's framing, in
742 /// every one of its six fields.
743 ///
744 /// The constant lives in `zerodds-wire`, which depends on neither
745 /// `zerodds-ast-layout` nor this crate, so it is written out by hand
746 /// there and can only be compared here. It was Cyclone's while
747 /// `Dialect::Cyclone` was the default; two of the six moved when the
748 /// default did, and a hand-written constant nobody compares is a
749 /// constant that goes stale in silence.
750 ///
751 /// Asserted over both byte orders because the framing must not
752 /// depend on one, and under encoding version 1 because PL_CDR1 is
753 /// the only place these six reach the wire.
754 #[test]
755 fn the_default_framing_is_the_default_dialects() {
756 for endianness in [Endianness::Little, Endianness::Big] {
757 let request = ProbeRequest::new("Sample", Encoding::PlCdr1, endianness);
758 assert_eq!(
759 request.pl_cdr1_framing(),
760 PlCdr1Framing::DEFAULT,
761 "`PlCdr1Framing::DEFAULT` is not `Dialect::default`'s framing"
762 );
763 }
764 // The two that moved, named so a failure says which. Through
765 // the policy and not against the constant directly: a literal
766 // `assert!` over a `const` field folds at compile time, which
767 // clippy refuses and which would be a check the compiler had
768 // already made rather than one this test makes.
769 let default = Dialect::default().pl_cdr1_headers();
770 assert_eq!(
771 default.always_extended(),
772 PlCdr1Framing::DEFAULT.require_extended,
773 "the clause takes the short parameter header wherever §7.4.1.2.2 allows it"
774 );
775 assert_eq!(
776 default.pads_declared_optional_length(),
777 PlCdr1Framing::DEFAULT.pad_declared_optional_length,
778 "the clause declares an `@optional` member's exact length"
779 );
780 assert_eq!(
781 default,
782 PlCdr1HeaderPolicy::Specification,
783 "the default dialect frames a PL_CDR1 parameter list some other way now"
784 );
785 // And the dialect it used to be, which must now differ in
786 // exactly those two.
787 let cyclone = ProbeRequest::in_dialect(
788 "Sample",
789 Encoding::PlCdr1,
790 Endianness::Little,
791 Dialect::Cyclone,
792 )
793 .pl_cdr1_framing();
794 assert_ne!(cyclone, PlCdr1Framing::DEFAULT);
795 assert!(cyclone.require_extended);
796 assert!(!cyclone.pad_declared_optional_length);
797 assert_eq!(cyclone.pid_flags, PlCdr1Framing::DEFAULT.pid_flags);
798 assert_eq!(
799 cyclone.highest_short_id,
800 PlCdr1Framing::DEFAULT.highest_short_id
801 );
802 assert_eq!(
803 default.pads_declared_length(),
804 PlCdr1Framing::DEFAULT.pad_declared_length,
805 "XTypes 1.3 §7.4.1.2 declares a member's exact length"
806 );
807 assert!(
808 cyclone.pad_declared_length,
809 "Cyclone declares the length rounded up to four"
810 );
811 assert_eq!(
812 cyclone.member_must_understand,
813 PlCdr1Framing::DEFAULT.member_must_understand
814 );
815 }
816
817 #[test]
818 fn the_encapsulation_identifiers_are_the_ones_the_vendors_wrote() {
819 // Read off the `header=` column of
820 // `crates/ast-layout/tests/fixtures/vendor_bytes.md`, which is
821 // what RTI Connext put in front of each of those samples.
822 assert_eq!(
823 encapsulation_header(Encoding::PlainCdr1, Endianness::Little, true, 0)[..2],
824 [0x00, 0x01]
825 );
826 assert_eq!(
827 encapsulation_header(Encoding::PlainCdr2, Endianness::Little, true, 0)[..2],
828 [0x00, 0x07]
829 );
830 assert_eq!(
831 encapsulation_header(Encoding::DelimitedCdr, Endianness::Little, true, 0)[..2],
832 [0x00, 0x09]
833 );
834 assert_eq!(
835 encapsulation_header(Encoding::PlCdr2, Endianness::Little, true, 0)[..2],
836 [0x00, 0x0b]
837 );
838 assert_eq!(
839 encapsulation_header(Encoding::PlCdr1, Endianness::Little, true, 0)[..2],
840 [0x00, 0x03]
841 );
842 }
843}