Skip to main content

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}