Skip to main content

zerodds_wire/
writer.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Putting bytes on the wire.
4//!
5//! Alignment is relative to the start of the stream, not to the start of
6//! the value: XCDR aligns each primitive to its own size counted from
7//! the encapsulation, which is why a `long` after a `char` sits at 4 and
8//! not at 1. DDS-XTypes 1.3 §7.4.1.1.
9
10use crate::{Endianness, WireError};
11use alloc::format;
12use alloc::vec::Vec;
13use zerodds_float128::Float128;
14
15/// How a PL_CDR1 parameter list is framed.
16///
17/// The fields below are the byte-level ways the measured
18/// implementations differ inside a `@mutable` XCDR1 sample. Which header
19/// *form* each member takes is not one of them: the plan chooses it, and
20/// [`Writer::finish_pid`] overrides the plan in the one case the plan
21/// cannot see — a value that turns out to overrun the short form's
22/// sixteen-bit length. Set this from
23/// `zerodds_ast_layout::PlCdr1HeaderPolicy`, whose documentation
24/// carries the measurement, rather than assembling it by hand.
25///
26/// A writer that framed one way while its plan assumed another would
27/// produce bytes no implementation writes, so the default here is the
28/// same dialect as the layout key's: `Dialect::Specification`, the
29/// clause-by-clause reading. [`PlCdr1Framing::DEFAULT`] carries the
30/// four values and names where each comes from, because this crate has
31/// no dependency on `zerodds-ast-layout` and cannot derive them.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub struct PlCdr1Framing {
34    /// What flags a parameter id carries at a position — a member's id,
35    /// `PID_LIST_END`, and the `PID_EXTENDED` marker.
36    ///
37    /// One setting for all three, asked per [`WirePosition`], because no
38    /// position's answer can be read off another's: OpenDDS sets both
39    /// flags on every member and writes a bare `0x3f02`, while Cyclone
40    /// and RTI leave the member bare and write `0x7f02`; and at the
41    /// marker Cyclone and RTI agree (`7f01`) across two different
42    /// framing forms while Fast DDS and OpenDDS share a framing form and
43    /// write `3f01` against `ff01`.
44    pub pid_flags: PidFlagPolicy,
45    /// Whether a `@mutable` member's declared length covers the padding
46    /// that follows its value to the next four-byte boundary.
47    pub pad_declared_length: bool,
48    /// The same question for an `@optional` member of a type that is not
49    /// `@mutable`, which is a separate axis because one implementation
50    /// answers the two differently.
51    ///
52    /// Measured 2026-08-09: Cyclone declares `04000000` for the two-byte
53    /// `short c` of `ext_mutable` and `01000000` for the one-byte
54    /// `@optional octet b` of `opt_final_present`. RTI pads both, Fast
55    /// DDS pads neither. One boolean for both would make
56    /// `Dialect::Cyclone` wrong about one of them whichever way it was
57    /// set.
58    pub pad_declared_optional_length: bool,
59    /// Whether a member's own must-understand bit goes out.
60    ///
61    /// §7.4.1.2.2 makes it unconditional and two of the three
62    /// implementations write it; Fast DDS writes the bare id. Set from
63    /// `PlCdr1HeaderPolicy::member_must_understand`, which carries the
64    /// measurement.
65    pub member_must_understand: bool,
66    /// The highest member id a short parameter header carries.
67    ///
68    /// §7.4.1.2.2 stops at `0x3F00` inclusive and reserves the rest of
69    /// the low fourteen bits; one implementation carries all fourteen
70    /// bits, so a reader that took the clause's threshold for a fact
71    /// would report `@id(16383)` as a reserved parameter and drop the
72    /// member. The byte-layer half of
73    /// `zerodds_ast_layout::ShortPidRange`.
74    ///
75    /// Both sides. The layout plan chooses the form, because it changes
76    /// a member's extent; [`Writer::write_pid`] reads this to refuse an
77    /// id the short header cannot carry, which is the floor under a
78    /// caller whose plan did not choose.
79    pub highest_short_id: u16,
80    /// Whether every parameter is expected in the extended form.
81    ///
82    /// Read-side only. The writer takes the form from the layout plan,
83    /// because it changes a member's extent and the plan is where
84    /// extents are decided; the reader has no plan for a stream it did
85    /// not write, so it needs telling. Both come from the same
86    /// `PlCdr1HeaderPolicy`, which is what keeps them agreeing.
87    pub require_extended: bool,
88    /// Whether the parameter that just ended writes its own trailing
89    /// padding to the next four-byte boundary.
90    ///
91    /// Every header writer aligns itself, so the two readings only
92    /// part at the end of the stream: RTI pads after a final
93    /// `@optional` parameter, Fast DDS does not. Set from
94    /// `PlCdr1HeaderPolicy::pads_after_value`.
95    pub pad_after_value: bool,
96}
97
98/// Whether a PL_CDR1 reader takes a stream framed another way than it was
99/// configured for. The byte-layer half of
100/// `zerodds_ast_layout::PlCdr1Reading`, which carries the reasoning.
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
102pub enum PlCdr1Reading {
103    /// The configured framing is read; another framing's flags, header
104    /// form and length convention are reported as
105    /// [`crate::WireError::WrongFraming`].
106    #[default]
107    Strict,
108    /// Any flags on `PID_EXTENDED` and `PID_LIST_END`, either header form
109    /// and either length convention are read.
110    Tolerant,
111}
112
113impl PlCdr1Reading {
114    /// Whether this is [`Self::Tolerant`].
115    #[must_use]
116    pub const fn is_tolerant(self) -> bool {
117        matches!(self, Self::Tolerant)
118    }
119}
120
121impl PlCdr1Framing {
122    /// The clause's framing, which is this project's default dialect.
123    ///
124    /// A `const` as well as a `Default` because both the writer's and
125    /// the reader's `const fn` constructors need it, and a `Default`
126    /// impl cannot be called from one.
127    ///
128    /// It was Cyclone's while `Dialect::Cyclone` was the default, and
129    /// two of the six moved with the default: `require_extended`, which
130    /// is `PlCdr1HeaderPolicy::AlwaysExtended` in Cyclone and the short
131    /// header wherever §7.4.1.2.2 allows it in the clause; and
132    /// `pad_declared_optional_length`, which Cyclone answers `false` and
133    /// the clause `true`.
134    /// `zerodds_wire_probe::value`'s
135    /// `the_default_framing_is_the_default_dialects` holds all six
136    /// against `zerodds_ast_layout::Dialect::default`. It is there and
137    /// not here because this crate depends on neither the layout crate
138    /// nor the probe, and a constant nothing compares is a constant that
139    /// drifts.
140    pub const DEFAULT: Self = Self {
141        pid_flags: PidFlagPolicy::SentinelMustUnderstand,
142        highest_short_id: PID_SHORT_MAX_ID,
143        pad_declared_length: false,
144        pad_declared_optional_length: false,
145        member_must_understand: true,
146        require_extended: false,
147        pad_after_value: true,
148    };
149
150    /// Whether a parameter in this role declares a padded length.
151    #[must_use]
152    pub const fn pads_declared_length(self, role: ParameterRole) -> bool {
153        match role {
154            ParameterRole::Member => self.pad_declared_length,
155            ParameterRole::Optional => self.pad_declared_optional_length,
156        }
157    }
158}
159
160/// How many bytes one `Char16` occupies on the wire.
161///
162/// XTypes 1.3 §7.4.1.1.1, Table 31 says two, and two of the three
163/// measured implementations write two everywhere. RTI Connext 7.7 writes
164/// **four** under XCDR1 — measured 2026-08-09 with
165/// `@final struct WCh { wchar c; octet tail; }`, `c = U+0061`: RTI's
166/// XCDR1 payload is `61000000 ab` little endian and `00000061 ab` big
167/// endian, against `6100 ab` / `0061 ab` from Cyclone, Fast DDS and
168/// RTI's own XCDR2. It is the same character width RTI's XCDR1 `wstring`
169/// uses, which is why the two are one setting and not two —
170/// `zerodds_ast_layout::WstringLengthPolicy::character_width` is where
171/// the width is chosen, and this enum is how it reaches the byte layer.
172///
173/// A width is not a formatting difference. A `wchar` read two bytes wide
174/// out of a four-byte field returns the wrong character *and* leaves
175/// every member after it two bytes out of place.
176#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
177pub enum WcharWidth {
178    /// Two bytes, one UTF-16 code unit. Table 31, and what three of the
179    /// four measured (vendor, encoding version) pairs write.
180    #[default]
181    Two,
182    /// Four bytes, one UTF-16 code unit zero-extended. RTI Connext under
183    /// XCDR1.
184    Four,
185}
186
187impl WcharWidth {
188    /// The width in bytes, which is also the alignment under
189    /// [`WcharFraming::Fixed`]: a `Char16` is a primitive and aligns to
190    /// its own size, capped by `MAXALIGN`. Under
191    /// [`WcharFraming::GiopLengthPrefixed`] it is the count in the
192    /// leading octet and the alignment is one.
193    #[must_use]
194    pub const fn bytes(self) -> usize {
195        match self {
196            Self::Two => 2,
197            Self::Four => 4,
198        }
199    }
200}
201
202/// How many BCD digits a `fixed<P, S>` puts on the wire.
203///
204/// CORBA 3.3 part 2 §9.3.2.8 packs a `fixed` as BCD nibbles followed by a
205/// sign nibble, and leaves the digit count to the implementation. Two answers
206/// exist in the field and both are self-consistent, so this is a dialect axis
207/// and not a defect: omniORB writes the declared `P` whatever the value is,
208/// while JacORB and ACE/TAO write as many digits as the value has.
209///
210/// The same two values live in `zerodds_ast_layout::key::FixedDigitCount`,
211/// which is where a plan chooses between them. This copy exists because a
212/// `Writer` must not depend on the layout planner — the same reason
213/// [`WcharFraming`] sits here.
214#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
215pub enum FixedDigits {
216    /// `P` digits, whatever the value is. omniORB 4.3.3.
217    #[default]
218    Declared,
219    /// As many digits as the value has, never fewer than one. JacORB 3.9.
220    ValuePrecision,
221    /// The integer digits of the decimal as written, leading zeros
222    /// included, plus the `S` fraction digits, padded to an odd digit
223    /// count by adding one leading zero if the count is even. ACE/TAO
224    /// 2.5.24's `ACE_CDR::Fixed` for a decimal with `S` fraction digits,
225    /// through its own CDR operator. Measured in
226    /// `unspecified_constructs.md` §3. OpenDDS writes the declared count.
227    ValueStringDigits,
228}
229
230impl FixedDigits {
231    /// How many digit nibbles a value goes out with.
232    ///
233    /// `declared` is `P`; `significant` is the value's digit count with
234    /// its leading zeros removed; `string_digits` is the digit count of
235    /// the input string. The byte-layer half of
236    /// `zerodds_ast_layout::key::FixedDigitCount::wire_digits`, and
237    /// `crates/ast-layout/tests/fixed_digits_agree.rs` holds the two halves
238    /// against each other over every argument either can take — a copy that
239    /// nothing compares is a copy that drifts.
240    ///
241    /// Never zero under any value: a field of no digits could not carry
242    /// the sign nibble's pair.
243    #[must_use]
244    pub const fn wire_digits(self, declared: u8, significant: u8, string_digits: u8) -> u8 {
245        match self {
246            Self::Declared => declared,
247            Self::ValuePrecision => {
248                if significant == 0 {
249                    1
250                } else {
251                    significant
252                }
253            }
254            Self::ValueStringDigits => {
255                // ACE rule: if the string's digit count is even, add one
256                // leading zero to make it odd. The sign nibble then makes
257                // the total nibble count even (whole octets).
258                if string_digits == 0 {
259                    1
260                } else if string_digits % 2 == 0 {
261                    string_digits + 1
262                } else {
263                    string_digits
264                }
265            }
266        }
267    }
268}
269
270/// How a `wchar` is framed.
271///
272/// The byte-layer half of
273/// `zerodds_ast_layout::WcharFraming`, which carries the run: all five
274/// DDS implementations write XTypes 1.3 Table 31's fixed-width code
275/// unit, and omniORB 4.3.3, TAO 2.5.24 and JacORB 3.9 all write CORBA
276/// 3.3 part 2 §9.3.1.6's GIOP 1.2 form instead — an octet saying how
277/// many octets follow, then that many, aligned to one.
278///
279/// It is not a width. A `wchar` that carries its own count is one octet
280/// longer *and* aligns to one, so every member behind it moves; a reader
281/// that had the width right and the framing wrong would take the count
282/// octet for half a character and return a different one.
283///
284/// It is not a property of the type either. TAO writes the count octet
285/// for a `wchar` **member** and a bare unit for a `wchar` inside an
286/// array or a sequence, so the framing is answered per
287/// [`WcharPosition`] and every entry point that writes or reads one
288/// takes the position as an argument.
289#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
290pub enum WcharFraming {
291    /// A fixed-width code unit, aligned to its own width. Table 31.
292    #[default]
293    Fixed,
294    /// One octet of length, then that many octets, aligned to one, in
295    /// every position. CORBA §9.3.1.6's GIOP 1.2 `wchar`, as omniORB
296    /// and JacORB write it.
297    GiopLengthPrefixed,
298    /// The count octet for a member, a bare fixed-width unit for a
299    /// collection element. TAO 2.5.24.
300    GiopLengthPrefixedMemberOnly,
301}
302
303impl WcharFraming {
304    /// Whether a `wchar` in this position carries its own octet count.
305    #[must_use]
306    pub const fn has_length_octet(self, position: WcharPosition) -> bool {
307        match self {
308            Self::Fixed => false,
309            Self::GiopLengthPrefixed => true,
310            Self::GiopLengthPrefixedMemberOnly => matches!(position, WcharPosition::Member),
311        }
312    }
313}
314
315/// Where the value being written or read sits.
316///
317/// The byte-layer half of `zerodds_ast_layout::WirePosition`. It is an
318/// argument to [`Writer::write_wchar`],
319/// [`crate::reader::Reader::read_wchar`] and to [`PidFlagPolicy`]
320/// rather than a field of any of them, for the reason [`ParameterRole`]
321/// is an argument: one stream carries several positions at once, so a
322/// setting made once at construction could not answer for all of them.
323///
324/// Two axes read it, and each reads a different pair of variants:
325/// [`WcharFraming`] tells [`Self::Member`] from [`Self::Element`], and
326/// [`PidFlagPolicy`] tells [`Self::Member`] from [`Self::Sentinel`].
327#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
328pub enum WirePosition {
329    /// A member of a structure, a union branch, or the whole value.
330    ///
331    /// For [`PidFlagPolicy`] this is a member's own parameter id in a
332    /// PL_CDR1 list.
333    #[default]
334    Member,
335    /// An element of an array or a sequence.
336    Element,
337    /// The `PID_LIST_END` that closes a PL_CDR1 parameter list.
338    ///
339    /// Read by [`PidFlagPolicy`], which carries the measurement that
340    /// makes it a position rather than a property of the list.
341    Sentinel,
342    /// The `PID_EXTENDED` marker that opens a parameter's extended
343    /// header.
344    ///
345    /// Read by [`PidFlagPolicy`]. It was a field of
346    /// `zerodds_ast_layout::PlCdr1HeaderPolicy` on the argument that the
347    /// marker exists only under the always-extended form; it does not —
348    /// a member id above `PID_SHORT_MAX_ID` reaches it under the
349    /// specification's form too, and RTI sets the bit there.
350    ExtendedMarker,
351}
352
353/// The name the position enumeration shipped under, when it reached only
354/// `wchar`.
355///
356/// The alias stays because the argument for the rename is in the rename,
357/// not in breaking every caller in one commit. Its layout-crate half is
358/// aliased the same way.
359pub type WcharPosition = WirePosition;
360
361/// What flags a PL_CDR1 parameter id carries, at a position.
362///
363/// The byte-layer half of `zerodds_ast_layout::PidFlagPolicy`, which
364/// carries the measurement. XTypes 1.3 §7.4.1.2 splits the sixteen-bit
365/// field: the low fourteen bits are the id, bit 14 is
366/// `PID_MUST_UNDERSTAND` and bit 15 is [`PID_IMPL_EXTENSION`].
367///
368/// A member's *own* `must_understand` is not on this axis — it is
369/// [`PlCdr1Framing::member_must_understand`], and the writer ORs the two
370/// together. This one answers the part that does not depend on what the
371/// member declared, which is the whole of the sentinel's answer:
372/// `PID_LIST_END` is not a member and has nothing to declare.
373#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
374pub enum PidFlagPolicy {
375    /// A bare member id, and `PID_MUST_UNDERSTAND` on the sentinel.
376    /// Cyclone DDS and RTI Connext 7.7.
377    #[default]
378    SentinelMustUnderstand,
379    /// Bare everywhere. Fast DDS.
380    Bare,
381    /// `PID_IMPL_EXTENSION | PID_MUST_UNDERSTAND` on every member id
382    /// whatever the member declared, and a bare sentinel. OpenDDS.
383    ///
384    /// Measured on the **short** header only: OpenDDS writes no
385    /// extended header anywhere in the recorded corpus, so
386    /// [`Writer::write_pid_extended`] does not claim an answer for
387    /// `FLAG_1` of an `eMemberHeader`.
388    ImplExtensionOnMembers,
389}
390
391impl PidFlagPolicy {
392    /// The flag bits a parameter id carries at `position`.
393    ///
394    /// Only [`WirePosition::Member`], [`WirePosition::Sentinel`] and
395    /// [`WirePosition::ExtendedMarker`] have a parameter id at all;
396    /// [`WirePosition::Element`] answers zero because there is nothing
397    /// there to carry a flag.
398    ///
399    /// The marker's answers are Cyclone and RTI `7f01`, Fast DDS `3f01`,
400    /// OpenDDS `ff01` — the same three values, at a third position.
401    #[must_use]
402    pub const fn flags_at(self, position: WirePosition) -> u16 {
403        match (self, position) {
404            (
405                Self::SentinelMustUnderstand,
406                WirePosition::Sentinel | WirePosition::ExtendedMarker,
407            ) => PID_MUST_UNDERSTAND,
408            (Self::ImplExtensionOnMembers, WirePosition::Member | WirePosition::ExtendedMarker) => {
409                PID_IMPL_EXTENSION | PID_MUST_UNDERSTAND
410            }
411            _ => 0,
412        }
413    }
414}
415
416/// In what byte order a `wstring`'s code units go out — **C4**.
417///
418/// The byte-layer half of `zerodds_ast_layout::WstringUnitOrder`, which
419/// carries the runs. Read by [`Writer::write_wstring`] and
420/// [`Reader::read_wstring`](crate::Reader::read_wstring) for a `wstring`'s units, and by
421/// [`Writer::write_wchar`] for a scalar, where CORBA gives the mark no
422/// place to ride and [`Self::ByteOrderMark`] therefore degrades to
423/// big-endian.
424///
425/// A fourth axis and not a fourth coordinate of
426/// [`WstringLengthPolicy`]: it crosses the length convention rather
427/// than extending it. omniORB's mark rides on an octet count and
428/// nothing stops a GIOP 1.1 character count from carrying one too, so
429/// folding the order in would give eight variants for two questions —
430/// which is how `UnswappedUnits`, a byte order wearing a length
431/// policy's name, came to exist.
432#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
433pub enum WstringUnitOrder {
434    /// The stream's own byte order. XTypes §7.4.1.1.2 through `ESWAP`.
435    #[default]
436    Stream,
437    /// UTF-16 big-endian whatever the stream says. What omniORB and
438    /// JacORB decode in the absence of a byte-order mark.
439    BigEndianAlways,
440    /// A byte-order mark, then the units, both little-endian. omniORB.
441    ByteOrderMark,
442    /// The units always little-endian, no mark. Cyclone's big-endian
443    /// output, and `rosidl_typesupport_fastrtps`'s over four-octet
444    /// units. Arrived as a value of the length policy — a byte order
445    /// wearing a length's name — and belongs on this axis.
446    HostUnswapped,
447}
448
449impl WstringUnitOrder {
450    /// Whether a byte-order mark is written, and expected on read.
451    ///
452    /// The mark is a code unit like any other and its octets are
453    /// counted in the length: omniORB writes `0a000000 fffe 6100 34d8
454    /// 1edd 6200` for `"a𝄞b"`, ten octets over five units.
455    /// §7.4.1.1.2 forbids it outright — "shall not include a Byte Order
456    /// Mark" — so no DDS dialect selects it.
457    #[must_use]
458    pub const fn has_mark(self) -> bool {
459        matches!(self, Self::ByteOrderMark)
460    }
461
462    /// Whether one code unit goes out little-endian whatever the
463    /// stream's order is.
464    ///
465    /// Takes the position for the reason [`WstringLengthPolicy`]'s
466    /// accessors do. Measured to agree at the member and the collection
467    /// element in all nine implementations.
468    #[must_use]
469    pub const fn writes_little_endian_units(self, _position: WirePosition) -> bool {
470        matches!(self, Self::ByteOrderMark | Self::HostUnswapped)
471    }
472}
473
474/// **C1** — what a `wstring`'s length prefix counts.
475///
476/// The byte-layer half of `zerodds_ast_layout::WstringLengthCounts`,
477/// which carries the runs and the argument. Declared again here rather
478/// than imported for the reason [`PlCdr1Framing`] is declared again:
479/// this crate depends on nothing and is not going to start.
480#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
481pub enum WstringLengthCounts {
482    /// The number of octets the code units occupy.
483    ///
484    /// XTypes 1.3 §7.4.1.1.2, and CORBA 3.3 part 2 §9.3.1.6's GIOP 1.2
485    /// `wstring`. Cyclone, OpenDDS, omniORB, TAO and JacORB.
486    #[default]
487    Octets,
488    /// The number of code units, whatever each one weighs.
489    ///
490    /// CORBA §9.3.1.6's GIOP 1.1 `wstring`, and Fast CDR's `wcslen`.
491    /// RTI under XCDR1, Fast DDS, and both ROS type supports.
492    Characters,
493}
494
495/// **C2** — whether a terminating NUL code unit is written.
496///
497/// It is written *and* counted or neither: no measured implementation
498/// writes a terminator the length does not cover, which is why this is
499/// one question and not two.
500#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
501pub enum WstringTerminator {
502    /// No terminator. Eight of the nine measured implementations.
503    #[default]
504    Absent,
505    /// A NUL code unit at the end, inside the length. CORBA §9.3.1.6's
506    /// GIOP 1.1 rule, and RTI under XCDR1 alone among the nine.
507    Present,
508}
509
510/// **C3** — how wide one UTF-16 code unit is on the wire.
511///
512/// The transmission code set's width. No GIOP version fixes it and
513/// XTypes Table 31 fixes it at two, so it varies independently of C1
514/// and C2 — which is why these are three types and not one enumeration
515/// of named forms.
516///
517/// Not [`WcharWidth`], and the difference is measured:
518/// `rmw_cyclonedds_cpp` 1.3.4 writes a two-octet scalar `wchar` and
519/// four-octet `wstring` code units, so the scalar's width is its own
520/// axis. See `zerodds_ast_layout::WcharWidth`.
521#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
522pub enum WstringUnitWidth {
523    /// Two octets. XTypes Table 31, Cyclone, OpenDDS, Fast DDS,
524    /// omniORB, JacORB.
525    #[default]
526    Two,
527    /// Four octets, a code unit zero-extended — **not** a code point,
528    /// so `U+1D11E` stays the surrogate pair `34d80000 1edd0000`. RTI
529    /// under XCDR1, TAO, and both ROS type supports.
530    Four,
531}
532
533impl WstringUnitWidth {
534    /// The width in octets.
535    #[must_use]
536    pub const fn octets(self) -> usize {
537        match self {
538            Self::Two => 2,
539            Self::Four => 4,
540        }
541    }
542}
543
544/// How a `wstring`'s length is written and how wide its code units are
545/// — **C1 × C2 × C3**, a product space and not a list.
546///
547/// The byte-layer half of `zerodds_ast_layout::WstringLengthPolicy`,
548/// which carries the measurements. Eight cells, of which five have a
549/// name because an implementation was measured writing them:
550///
551/// | implementation | C1 counts | C2 terminator | C3 unit |
552/// |---|---|---|--:|
553/// | Cyclone, OpenDDS, RTI XCDR2, omniORB, JacORB | octets | absent | 2 |
554/// | Fast DDS | characters | absent | 2 |
555/// | RTI, XCDR1 | characters | **present** | 4 |
556/// | TAO | octets | absent | 4 |
557/// | ros-cyclone, ros-fastrtps | characters | absent | **4** |
558///
559/// The other three cells are as constructible and as nameable as the
560/// five: an enumeration standing in for a product space is what left
561/// the ROS cell — characters, no terminator, four-octet units — with no
562/// name for as long as the names were the values, and two
563/// implementations writing a form this project could not express.
564///
565/// The **order** the units go out in is not here. It is
566/// [`WstringUnitOrder`], a fourth axis, because it crosses the length
567/// convention rather than extending it: omniORB's mark rides on an
568/// octet count and nothing stops a GIOP 1.1 count from carrying one
569/// too. Two settings that both decide one byte order, with a precedence
570/// rule between them, is the shape that produces a wrong character and
571/// no error.
572#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
573pub struct WstringLengthPolicy {
574    /// C1 — what the length prefix counts.
575    pub counts: WstringLengthCounts,
576    /// C2 — whether a terminating NUL is written and counted.
577    pub terminator: WstringTerminator,
578    /// C3 — how wide one code unit is.
579    pub unit: WstringUnitWidth,
580}
581
582impl WstringLengthPolicy {
583    /// Build a cell of the product directly.
584    ///
585    /// Public and `const` so that a cell nobody has named is as easy to
586    /// reach as one that is named. The named constants below are
587    /// conveniences for the cells an implementation was measured in;
588    /// they are not the set of legal values.
589    #[must_use]
590    pub const fn new(
591        counts: WstringLengthCounts,
592        terminator: WstringTerminator,
593        unit: WstringUnitWidth,
594    ) -> Self {
595        Self {
596            counts,
597            terminator,
598            unit,
599        }
600    }
601
602    /// Octets, no terminator, two-octet units. XTypes 1.3 §7.4.1.1.2
603    /// and CORBA's GIOP 1.2 `wstring` over a two-octet code set. The
604    /// default.
605    pub const SPECIFICATION: Self = Self::new(
606        WstringLengthCounts::Octets,
607        WstringTerminator::Absent,
608        WstringUnitWidth::Two,
609    );
610
611    /// Characters, no terminator, two-octet units. Fast DDS under both
612    /// versions.
613    ///
614    /// [`Writer::write_wstring`] refuses a code point outside the BMP
615    /// in this cell rather than truncating it the way Fast CDR does.
616    pub const CHARACTER_COUNT: Self = Self::new(
617        WstringLengthCounts::Characters,
618        WstringTerminator::Absent,
619        WstringUnitWidth::Two,
620    );
621
622    /// Characters including the terminator, four-octet units. CORBA 3.3
623    /// part 2 §9.3.1.6's GIOP **1.1** `wstring`, and RTI Connext 7.7
624    /// under XCDR1.
625    pub const CHARACTER_COUNT_TERMINATED: Self = Self::new(
626        WstringLengthCounts::Characters,
627        WstringTerminator::Present,
628        WstringUnitWidth::Four,
629    );
630
631    /// Octets, no terminator, four-octet units. CORBA §9.3.1.6's GIOP
632    /// 1.2 `wstring` over a four-octet transmission code set — TAO
633    /// 2.5.24.
634    pub const SPECIFICATION_WIDE_UNITS: Self = Self::new(
635        WstringLengthCounts::Octets,
636        WstringTerminator::Absent,
637        WstringUnitWidth::Four,
638    );
639
640    /// Characters, no terminator, four-octet units. **The cell the old
641    /// enumeration could not reach.** Both ROS 2 type supports write
642    /// it.
643    pub const ROS_TYPE_SUPPORT: Self = Self::new(
644        WstringLengthCounts::Characters,
645        WstringTerminator::Absent,
646        WstringUnitWidth::Four,
647    );
648
649    /// Every cell of C1 × C2 × C3, measured or not.
650    ///
651    /// The reader sweeps this and not the measured five when it has to
652    /// name the cell a refused length *does* fit: a cell left out of the
653    /// sweep is a cell the diagnostic can never name.
654    pub const ALL_CELLS: [Self; 8] = [
655        Self::new(
656            WstringLengthCounts::Octets,
657            WstringTerminator::Absent,
658            WstringUnitWidth::Two,
659        ),
660        Self::new(
661            WstringLengthCounts::Octets,
662            WstringTerminator::Absent,
663            WstringUnitWidth::Four,
664        ),
665        Self::new(
666            WstringLengthCounts::Octets,
667            WstringTerminator::Present,
668            WstringUnitWidth::Two,
669        ),
670        Self::new(
671            WstringLengthCounts::Octets,
672            WstringTerminator::Present,
673            WstringUnitWidth::Four,
674        ),
675        Self::new(
676            WstringLengthCounts::Characters,
677            WstringTerminator::Absent,
678            WstringUnitWidth::Two,
679        ),
680        Self::new(
681            WstringLengthCounts::Characters,
682            WstringTerminator::Absent,
683            WstringUnitWidth::Four,
684        ),
685        Self::new(
686            WstringLengthCounts::Characters,
687            WstringTerminator::Present,
688            WstringUnitWidth::Two,
689        ),
690        Self::new(
691            WstringLengthCounts::Characters,
692            WstringTerminator::Present,
693            WstringUnitWidth::Four,
694        ),
695    ];
696
697    /// Whether this is the default cell, [`Self::SPECIFICATION`].
698    #[must_use]
699    pub const fn is_default(self) -> bool {
700        matches!(
701            (self.counts, self.terminator, self.unit),
702            (
703                WstringLengthCounts::Octets,
704                WstringTerminator::Absent,
705                WstringUnitWidth::Two
706            )
707        )
708    }
709
710    /// The name a configuration uses, the same spelling
711    /// `zerodds_ast_layout::WstringLengthPolicy::as_str` produces.
712    ///
713    /// A cell an implementation was measured in renders under the name
714    /// it shipped with; the three nobody writes render as their
715    /// coordinates, `counts/terminator/width`. Every one of the eight
716    /// has a name, because a value that cannot be printed cannot be
717    /// reported in a diagnostic.
718    #[must_use]
719    pub const fn as_str(self) -> &'static str {
720        match (self.counts, self.terminator, self.unit) {
721            (WstringLengthCounts::Octets, WstringTerminator::Absent, WstringUnitWidth::Two) => {
722                "specification"
723            }
724            (WstringLengthCounts::Octets, WstringTerminator::Absent, WstringUnitWidth::Four) => {
725                "specification-wide-units"
726            }
727            (WstringLengthCounts::Characters, WstringTerminator::Absent, WstringUnitWidth::Two) => {
728                "character-count"
729            }
730            (
731                WstringLengthCounts::Characters,
732                WstringTerminator::Absent,
733                WstringUnitWidth::Four,
734            ) => "ros-type-support",
735            (
736                WstringLengthCounts::Characters,
737                WstringTerminator::Present,
738                WstringUnitWidth::Four,
739            ) => "character-count-terminated",
740            (WstringLengthCounts::Octets, WstringTerminator::Present, WstringUnitWidth::Two) => {
741                "octets/terminated/2"
742            }
743            (WstringLengthCounts::Octets, WstringTerminator::Present, WstringUnitWidth::Four) => {
744                "octets/terminated/4"
745            }
746            (
747                WstringLengthCounts::Characters,
748                WstringTerminator::Present,
749                WstringUnitWidth::Two,
750            ) => "characters/terminated/2",
751        }
752    }
753
754    /// The command-line setting that selects this cell, whole.
755    ///
756    /// A `&'static str` and not a formatted one because
757    /// [`WireError::WrongFraming`]'s `fits` is a `&'static str`: the
758    /// point of that field is that a peer misconfiguration reports the
759    /// flag that fixes it instead of "an odd octet count", and a field
760    /// that had to allocate would not have been there on the no-`std`
761    /// path.
762    #[must_use]
763    pub const fn flag(self) -> &'static str {
764        match (self.counts, self.terminator, self.unit) {
765            (WstringLengthCounts::Octets, WstringTerminator::Absent, WstringUnitWidth::Two) => {
766                "--wstring-length specification"
767            }
768            (WstringLengthCounts::Octets, WstringTerminator::Absent, WstringUnitWidth::Four) => {
769                "--wstring-length specification-wide-units"
770            }
771            (WstringLengthCounts::Characters, WstringTerminator::Absent, WstringUnitWidth::Two) => {
772                "--wstring-length character-count"
773            }
774            (
775                WstringLengthCounts::Characters,
776                WstringTerminator::Absent,
777                WstringUnitWidth::Four,
778            ) => "--wstring-length ros-type-support",
779            (
780                WstringLengthCounts::Characters,
781                WstringTerminator::Present,
782                WstringUnitWidth::Four,
783            ) => "--wstring-length character-count-terminated",
784            (WstringLengthCounts::Octets, WstringTerminator::Present, WstringUnitWidth::Two) => {
785                "--wstring-length octets/terminated/2"
786            }
787            (WstringLengthCounts::Octets, WstringTerminator::Present, WstringUnitWidth::Four) => {
788                "--wstring-length octets/terminated/4"
789            }
790            (
791                WstringLengthCounts::Characters,
792                WstringTerminator::Present,
793                WstringUnitWidth::Two,
794            ) => "--wstring-length characters/terminated/2",
795        }
796    }
797
798    /// **C1** at a position — what the length prefix counts.
799    ///
800    /// An accessor and not the field: a caller that read `policy.counts`
801    /// directly would have answered for every position by not asking
802    /// about any, which is exactly the shape that let a member's `wchar`
803    /// framing stand in for an element's through five emitters. The nine
804    /// implementations answer both positions alike, measured 2026-08-12,
805    /// so the argument is unused — and it is the argument that keeps
806    /// that a measurement rather than an assumption.
807    #[must_use]
808    pub const fn counts_at(self, _position: WirePosition) -> WstringLengthCounts {
809        self.counts
810    }
811
812    /// **C2** at a position — whether a terminating NUL is written.
813    #[must_use]
814    pub const fn terminator_at(self, _position: WirePosition) -> WstringTerminator {
815        self.terminator
816    }
817
818    /// **C3** at a position — how wide one code unit is.
819    #[must_use]
820    pub const fn unit_at(self, _position: WirePosition) -> WstringUnitWidth {
821        self.unit
822    }
823
824    /// How many octets one code unit takes at a position.
825    #[must_use]
826    pub const fn character_width(self, position: WirePosition) -> usize {
827        self.unit_at(position).octets()
828    }
829
830    /// The length field this cell declares for a run of `units` code
831    /// units, or `None` when it does not fit the `UInt32` the field is.
832    ///
833    /// The run is the whole run: the text's units, plus C2's terminator
834    /// and C4's mark where those are written. One expression rather than
835    /// an arm per named cell, so a cell nobody has measured computes its
836    /// length the same way as the five that are.
837    ///
838    /// `None` and not a wrapped value. A length that names a prefix of
839    /// its own content is the silent class: the peer reads a well-formed
840    /// shorter `wstring` and then every following member from inside
841    /// this one, and nothing in the frame says so. A separate function
842    /// from [`Writer::write_wstring`] so that the refusal can be tested
843    /// at `usize::MAX` without a caller having to hold two gigabytes of
844    /// text to reach it.
845    #[must_use]
846    pub const fn declared_length(self, units: usize, position: WirePosition) -> Option<u32> {
847        let counted = match self.counts_at(position) {
848            WstringLengthCounts::Octets => {
849                match units.checked_mul(self.character_width(position)) {
850                    Some(counted) => counted,
851                    None => return None,
852                }
853            }
854            WstringLengthCounts::Characters => units,
855        };
856        if counted > u32::MAX as usize {
857            return None;
858        }
859        #[allow(clippy::cast_possible_truncation)]
860        Some(counted as u32)
861    }
862
863    /// Whether `declared` is a length this cell could have written.
864    ///
865    /// Only the shape of the number, which is all a refused read has to
866    /// go on: an octet count has to divide by the unit width, and a
867    /// count that includes a terminator cannot be zero. A cell nobody
868    /// has measured is checked by the same two rules.
869    ///
870    /// A byte count and one written under
871    /// [`WstringUnitOrder::HostUnswapped`] are the same number — that
872    /// point of the order axis differs in the *content*'s byte order,
873    /// which no length can show — so a diagnostic that names one names
874    /// both.
875    #[must_use]
876    pub const fn admits_length(self, declared: usize) -> bool {
877        if matches!(self.counts, WstringLengthCounts::Octets) && declared % self.unit.octets() != 0
878        {
879            return false;
880        }
881        if matches!(self.terminator, WstringTerminator::Present) && declared < 1 {
882            return false;
883        }
884        true
885    }
886}
887
888/// The UTF-16 byte-order mark, `U+FEFF`.
889///
890/// Named because it appears on both sides of the wire and a bare
891/// `0xFEFF` in a reader would read as a character of the string.
892pub(crate) const BYTE_ORDER_MARK: u16 = 0xFEFF;
893
894/// `U+FFFE`, which is not a character: a mark read this way says the
895/// units behind it are the other way round.
896pub(crate) const SWAPPED_BYTE_ORDER_MARK: u16 = 0xFFFE;
897
898/// Which of §7.4.1.2's two roles a parameter header stands in.
899///
900/// The header is the same header — the id, the length, the short and
901/// extended forms are all one clause. The role decides what the length
902/// field counts, and whether a member that took no value writes a header
903/// at all.
904#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
905pub enum ParameterRole {
906    /// A member of a `@mutable` type. Rules (24) and (25). Absence is
907    /// written by leaving the parameter out.
908    #[default]
909    Member,
910    /// An `@optional` member of a type that is not `@mutable`. Rule (19).
911    /// Absence is written as a header with a length of zero.
912    Optional,
913}
914
915impl Default for PlCdr1Framing {
916    fn default() -> Self {
917        Self::DEFAULT
918    }
919}
920
921/// The must-understand bit of a parameter id. DDSI-RTPS 2.5 §9.4.2.11.2.
922const PID_MUST_UNDERSTAND: u16 = 0x4000;
923
924/// `FLAG_2` of the extended `eMemberHeader`, which is the **member's**
925/// must-understand. XTypes 1.3 §7.4.1.2.1.
926pub const E_FLAG_MUST_UNDERSTAND: u32 = 0x4000_0000;
927
928/// `FLAG_1` of the extended `eMemberHeader`, which is
929/// `FLAG_IMPL_EXTENSION`. XTypes 1.3 §7.4.1.2.1, and the same property
930/// as [`PID_IMPL_EXTENSION`] in the short form.
931pub const E_FLAG_IMPL_EXTENSION: u32 = 0x8000_0000;
932
933/// `FLAG_IMPL_EXTENSION`, bit 15 of a short parameter id.
934///
935/// XTypes 1.3 §7.4.1.2.2: a parameter that sets it is the writing
936/// implementation's own and user-defined members "will never set the
937/// FLAG_IMPL_EXTENSION bit". A reader that masks it off takes such a
938/// parameter for the user member with the same low fourteen bits and
939/// reads a vendor's private payload into it.
940pub const PID_IMPL_EXTENSION: u16 = 0x8000;
941
942/// `PID_IGNORE`. XTypes 1.3 §7.4.1.2.2: "All consumers of this Data
943/// Representation shall ignore parameters with this ID" — including a
944/// consumer that would otherwise discard the sample over the
945/// must-understand bit.
946pub const PID_IGNORE: u16 = 0x3F03;
947
948/// The low twenty-eight bits of an extended `eMemberHeader`, which are
949/// the member id. The top four are `FLAG_1` to `FLAG_4`.
950pub const E_MEMBER_ID_MASK: u32 = 0x0FFF_FFFF;
951
952/// A `fixed<P, S>` value split into its parts, with every rule about what is
953/// a legal value for that declaration already applied.
954///
955/// The digits come back trimmed of the leading zeros the declaration would
956/// pad back on, because that is what both callers need and neither can
957/// recover once they are gone: [`Writer::write_fixed`] counts them to answer
958/// [`FixedDigits::ValuePrecision`], and `zerodds-wire-codec`'s canonical form
959/// writes them out as the one spelling a sample has.
960#[derive(Debug, Clone, Copy, PartialEq, Eq)]
961pub struct FixedParts<'a> {
962    /// Whether the value carries a `-`. A negative zero is still `true` here:
963    /// [`Writer::write_fixed`] gives it the positive sign nibble because
964    /// §9.3.2.8 has one zero, but that is the packing's rule and not the
965    /// parse's, and the canonical form has to make the same decision itself.
966    pub negative: bool,
967    /// The integer digits, leading zeros removed. Empty for a value below one.
968    pub whole: &'a str,
969    /// The fraction digits as written, never more than `S` and possibly fewer.
970    pub fraction: &'a str,
971}
972
973/// Split a `fixed<P, S>` value, refusing everything the declaration cannot
974/// carry.
975///
976/// The one place the rule for a legal `fixed` value lives. Three call sites
977/// had a copy of it before — this writer, `zerodds-wire-codec`'s encoder and
978/// its canonical form — and a copy that disagreed with the others would
979/// accept a value one path writes and another rejects, on the same sample.
980///
981/// What it refuses, and why each is a refusal and not a silent repair:
982///
983/// * A declaration outside IDL 4.2 rule (42): `P` of zero carries no digit
984///   to pair the sign nibble with, `P` above 31 has no packing defined, and
985///   an `S` wider than `P` describes a fraction with no type to hold it.
986/// * A value that is not a decimal, or has no digits at all.
987/// * An integer part wider than `P - S`, or a fraction wider than `S`. A
988///   truncated decimal is the wrong number with nothing to notice it by,
989///   which is the failure `fixed` exists to prevent.
990pub fn parse_fixed(
991    text: &str,
992    digits: u8,
993    scale: u8,
994    at: usize,
995) -> Result<FixedParts<'_>, WireError> {
996    if digits == 0 {
997        return Err(WireError::refused(at, "a fixed with at least one digit"));
998    }
999    // OMG IDL 4.2 rule (42) caps the BCD form at 31 digits, and `fixed<31, S>`
1000    // is the widest declaration IDL admits. A wider one has no packing defined,
1001    // so it is refused rather than packed into octets whose width no reader can
1002    // be told.
1003    if digits > 31 {
1004        return Err(WireError::refused(at, "a fixed of at most 31 digits"));
1005    }
1006    if scale > digits {
1007        return Err(WireError::refused(at, "a scale no larger than the digits"));
1008    }
1009
1010    let (negative, magnitude) = match text.strip_prefix('-') {
1011        Some(rest) => (true, rest),
1012        None => (false, text.strip_prefix('+').unwrap_or(text)),
1013    };
1014    let (whole, fraction) = match magnitude.split_once('.') {
1015        Some((w, f)) => (w, f),
1016        None => (magnitude, ""),
1017    };
1018    if whole.is_empty() && fraction.is_empty() {
1019        return Err(WireError::refused(at, "a decimal with at least one digit"));
1020    }
1021    if !whole
1022        .bytes()
1023        .chain(fraction.bytes())
1024        .all(|b| b.is_ascii_digit())
1025    {
1026        return Err(WireError::refused(at, "a decimal of digits and one point"));
1027    }
1028
1029    // The integer part has P - S digits and the fraction has S. Neither is
1030    // padded silently in the direction that would lose one: a value too long
1031    // for its half is a value this type cannot represent.
1032    let whole = whole.trim_start_matches('0');
1033    if whole.len() > usize::from(digits - scale) {
1034        return Err(WireError::refused(
1035            at,
1036            "a decimal whose integer part fits the declared digits",
1037        ));
1038    }
1039    if fraction.len() > usize::from(scale) {
1040        return Err(WireError::refused(
1041            at,
1042            "a decimal whose fraction fits the declared scale",
1043        ));
1044    }
1045    Ok(FixedParts {
1046        negative,
1047        whole,
1048        fraction,
1049    })
1050}
1051
1052/// The BCD nibbles of `text` under `fixed<digits, scale>`.
1053///
1054/// Split from [`Writer::write_fixed`] for the reason the same split exists in
1055/// `zerodds-wire-codec`: the byte packing is mechanical, and every rule about
1056/// what is a legal value for `fixed<P, S>` is in [`parse_fixed`].
1057fn fixed_nibbles(
1058    text: &str,
1059    digits: u8,
1060    scale: u8,
1061    policy: FixedDigits,
1062    at: usize,
1063) -> Result<Vec<u8>, WireError> {
1064    let parts = parse_fixed(text, digits, scale, at)?;
1065    let integral_width = usize::from(digits - scale);
1066
1067    let mut all = Vec::with_capacity(usize::from(digits));
1068    all.extend(core::iter::repeat_n(
1069        0u8,
1070        integral_width - parts.whole.len(),
1071    ));
1072    all.extend(parts.whole.bytes().map(|byte| byte - b'0'));
1073    all.extend(parts.fraction.bytes().map(|byte| byte - b'0'));
1074    all.extend(core::iter::repeat_n(
1075        0u8,
1076        usize::from(scale) - parts.fraction.len(),
1077    ));
1078    let significant =
1079        u8::try_from(all.len() - all.iter().take_while(|n| **n == 0).count()).unwrap_or(u8::MAX);
1080    // What `ValueStringDigits` counts: the integer digits as written,
1081    // leading zeros included, plus `S` fraction digits. The fraction as
1082    // written would cut `7` under `fixed<5,2>` to its last nibble, `0c`.
1083    // Past 31 on the wire no reader can be told the field's width, and a
1084    // count that does not fit a `u8` would wrap into a different field.
1085    let magnitude = text.trim_start_matches(['+', '-']);
1086    let whole_as_written = magnitude.split('.').next().map_or(0, str::len);
1087    let written = whole_as_written + usize::from(scale);
1088    if policy == FixedDigits::ValueStringDigits && written + (1 - written % 2) > 31 {
1089        return Err(WireError::refused(
1090            at,
1091            "a fixed of at most 31 digits on the wire, as written",
1092        ));
1093    }
1094    // Only `ValueStringDigits` reads the count, and the check above holds
1095    // it to 31 there.
1096    let string_digits = if policy == FixedDigits::ValueStringDigits {
1097        u8::try_from(written).map_err(|_| {
1098            WireError::refused(at, "a fixed of at most 31 digits on the wire, as written")
1099        })?
1100    } else {
1101        0
1102    };
1103    let wire = usize::from(policy.wire_digits(digits, significant, string_digits));
1104    let kept = &all[all.len().saturating_sub(wire)..];
1105
1106    let mut nibbles = Vec::with_capacity(wire + 2);
1107    if (wire + 1) % 2 == 1 {
1108        nibbles.push(0);
1109    }
1110    // A count wider than the value's `P` digits — ACE's reading of
1111    // `0.12345` under `fixed<5,5>` is seven — is filled with leading zeros;
1112    // without them the sign nibble fell off the end of the packing.
1113    nibbles.extend(core::iter::repeat_n(0u8, wire.saturating_sub(kept.len())));
1114    nibbles.extend_from_slice(kept);
1115    // §9.3.2.8: `0xd` marks a negative value and `0xc` everything else, zero
1116    // included. A negative zero is written positive: there is one zero.
1117    nibbles.push(if parts.negative && nibbles.iter().any(|n| *n != 0) {
1118        0x0d
1119    } else {
1120        0x0c
1121    });
1122    Ok(nibbles)
1123}
1124
1125/// Accumulates the bytes of one sample.
1126#[derive(Debug, Clone)]
1127pub struct Writer {
1128    out: Vec<u8>,
1129    endianness: Endianness,
1130    /// How this writer frames a PL_CDR1 parameter list.
1131    pl_cdr1: PlCdr1Framing,
1132    /// Where alignment is counted from.
1133    ///
1134    /// Zero for a sample; for a nested value inside a DHEADER it stays
1135    /// zero as well, because XCDR counts alignment from the
1136    /// encapsulation and not from the nested value's own start.
1137    origin: usize,
1138    /// The encoding's `MAXALIGN`, which caps every alignment request.
1139    ///
1140    /// Eight under XCDR1, four under XCDR2. XTypes 1.3 §7.4.1:
1141    /// `ALIGN(N)` aligns to `MIN(N, XCDR.maxalign)`. A writer that did
1142    /// not know this padded an eight-byte primitive to eight under
1143    /// XCDR2 and put four bytes on the wire that no peer expects.
1144    max_align: usize,
1145    /// How wide a `Char16` is. See [`WcharWidth`].
1146    wchar_width: WcharWidth,
1147    /// How a scalar `Char16` is framed. See [`WcharFraming`].
1148    wchar_framing: WcharFraming,
1149    /// In what byte order wide code units go out. See
1150    /// [`WstringUnitOrder`], and [`Writer::write_wchar`] for the one
1151    /// place a *scalar* consults it.
1152    wstring_unit_order: WstringUnitOrder,
1153    /// Whether an LC4 member whose length turns out to be 1, 2, 4 or 8
1154    /// is rewritten with a short length code.
1155    short_length_codes: bool,
1156    /// Every span of alignment padding this writer emitted, when a
1157    /// caller asked for the record.
1158    ///
1159    /// `None` by default: nothing on the data path pays for it. The one
1160    /// caller is the interop comparison, which needs the answer to
1161    /// "which offsets of this byte string carry no value" computed from
1162    /// the *layout* rather than read off a byte-for-byte difference
1163    /// with a peer — see [`Writer::recording_padding`].
1164    /// Whether each `MinimalStructMember`/`MinimalUnionMember` carries a
1165    /// trailing octet of padding inside its own `DHEADER`.
1166    ///
1167    /// The XTypes 1.3 §7.3.4.5 minimal `EquivalenceHash` is taken over
1168    /// the serialized minimal `TypeObject`, and two vendor camps write
1169    /// an aggregated member differently: Cyclone/Fast DDS/HDDS write the
1170    /// member's `DHEADER` body and stop; RTI Connext and CoreDX append
1171    /// one octet inside that body. `docs/omg-xtypes-issues.md` §C2
1172    /// records the two byte strings. `false` is the Cyclone camp and
1173    /// the default; a peer talking to RTI/CoreDX sets it from
1174    /// `zerodds_ast_layout::Dialect`.
1175    minimal_member_trailing_octet: bool,
1176    padding: Option<Vec<(usize, usize)>>,
1177    /// The first error an infallible framing call ([`Writer::finish_dheader`])
1178    /// met. The bytes are not a valid stream once it is set; see
1179    /// [`Writer::deferred_error`].
1180    deferred: Option<WireError>,
1181}
1182
1183impl Writer {
1184    /// A writer in the given byte order.
1185    #[must_use]
1186    pub fn new(endianness: Endianness) -> Self {
1187        Self {
1188            out: Vec::new(),
1189            endianness,
1190            pl_cdr1: PlCdr1Framing::default(),
1191            origin: 0,
1192            max_align: 8,
1193            wchar_width: WcharWidth::Two,
1194            wchar_framing: WcharFraming::Fixed,
1195            wstring_unit_order: WstringUnitOrder::Stream,
1196            short_length_codes: false,
1197            minimal_member_trailing_octet: false,
1198            padding: None,
1199            deferred: None,
1200        }
1201    }
1202
1203    /// The same writer, keeping a record of every span of alignment
1204    /// padding it emits.
1205    ///
1206    /// The record is the answer §7.4.1's alignment rules give for this
1207    /// type under this key: [`Writer::align_to`] is the one place the
1208    /// walk emits a byte that carries no value, so the spans it notes
1209    /// are the fill positions of the plan and not an observation about
1210    /// the octets that came out. Two byte-shifting rewrites happen
1211    /// after the fact — `Writer::promote_pid` inserts eight octets
1212    /// and `shorten_emheader` removes four — and both move the spans
1213    /// above them, so the record stays in the finished stream's
1214    /// coordinates.
1215    ///
1216    /// Off by default and allocating only when asked for.
1217    #[must_use]
1218    pub fn recording_padding(mut self) -> Self {
1219        self.padding = Some(Vec::new());
1220        self
1221    }
1222
1223    /// The recorded padding spans as `(offset, length)`, in the
1224    /// finished stream's coordinates and in ascending order.
1225    ///
1226    /// Empty when [`Writer::recording_padding`] was not asked for,
1227    /// which is indistinguishable from a stream with no padding. A
1228    /// caller that needs to tell the two apart asks for the record.
1229    #[must_use]
1230    pub fn padding_spans(&self) -> &[(usize, usize)] {
1231        self.padding.as_deref().unwrap_or(&[])
1232    }
1233
1234    /// Whether this writer is keeping the record.
1235    #[must_use]
1236    pub const fn is_recording_padding(&self) -> bool {
1237        self.padding.is_some()
1238    }
1239
1240    /// Move every recorded padding span at or above `from` by `delta`.
1241    ///
1242    /// The two rewrites that change a byte's offset after it is written
1243    /// call this. Neither can move a span *partially*: the eight octets
1244    /// [`Writer::promote_pid`] inserts go between a parameter header
1245    /// and its value, and the four `shorten_emheader` removes are the
1246    /// NEXTINT it wrote itself — no padding span begins inside either
1247    /// range, so shifting whole spans is the whole correction.
1248    ///
1249    /// A no-op when nothing is being recorded.
1250    fn shift_padding(&mut self, from: usize, delta: isize) {
1251        let Some(spans) = self.padding.as_mut() else {
1252            return;
1253        };
1254        for span in spans.iter_mut() {
1255            if span.0 >= from {
1256                span.0 = span.0.saturating_add_signed(delta);
1257            }
1258        }
1259    }
1260
1261    /// The same writer framing a scalar `Char16` another way.
1262    ///
1263    /// A preset and not a patch, like every other `with_*` here: the
1264    /// framing and the width are separate because CORBA makes them
1265    /// separate — omniORB and TAO write the same framing over two
1266    /// octets and four.
1267    #[must_use]
1268    pub const fn with_wchar_framing(mut self, framing: WcharFraming) -> Self {
1269        self.wchar_framing = framing;
1270        self
1271    }
1272
1273    /// The `Char16` framing in force.
1274    #[must_use]
1275    pub const fn wchar_framing(&self) -> WcharFraming {
1276        self.wchar_framing
1277    }
1278
1279    /// The same writer ordering wide code units another way.
1280    #[must_use]
1281    pub const fn with_wstring_unit_order(mut self, order: WstringUnitOrder) -> Self {
1282        self.wstring_unit_order = order;
1283        self
1284    }
1285
1286    /// The wide code-unit order in force.
1287    #[must_use]
1288    pub const fn wstring_unit_order(&self) -> WstringUnitOrder {
1289        self.wstring_unit_order
1290    }
1291
1292    /// The same writer putting a `Char16` on the wire at another width.
1293    ///
1294    /// Set it from
1295    /// `zerodds_ast_layout::WstringLengthPolicy::character_width`, which
1296    /// is the same source the layout plan sized the member from. A
1297    /// writer wider than its plan writes a member the plan did not
1298    /// reserve room for and every offset after it is wrong.
1299    #[must_use]
1300    pub const fn with_wchar_width(mut self, width: WcharWidth) -> Self {
1301        self.wchar_width = width;
1302        self
1303    }
1304
1305    /// The `Char16` width in force.
1306    #[must_use]
1307    pub const fn wchar_width(&self) -> WcharWidth {
1308        self.wchar_width
1309    }
1310
1311    /// The same writer shortening an LC4 header whose member turned out
1312    /// to be 1, 2, 4 or 8 bytes long.
1313    ///
1314    /// Set it from
1315    /// `zerodds_ast_layout::LengthCodePolicy::short_code_where_it_fits`.
1316    /// The plan already picks a short code for every member whose *type*
1317    /// fixes the size; this is the other half, for a sequence or a map
1318    /// whose length is in the value. Fast DDS makes the same choice the
1319    /// same way — its `sequence<short>` of two elements is eight bytes
1320    /// and goes out as `02000030`, where three elements would be ten and
1321    /// go out as LC4.
1322    #[must_use]
1323    pub const fn with_short_length_codes(mut self, shorten: bool) -> Self {
1324        self.short_length_codes = shorten;
1325        self
1326    }
1327
1328    /// Whether an LC4 header is shortened when its member fits a code.
1329    #[must_use]
1330    pub const fn short_length_codes(&self) -> bool {
1331        self.short_length_codes
1332    }
1333
1334    /// The same writer framing a PL_CDR1 parameter list another way.
1335    #[must_use]
1336    pub const fn with_pl_cdr1_framing(mut self, framing: PlCdr1Framing) -> Self {
1337        self.pl_cdr1 = framing;
1338        self
1339    }
1340
1341    /// The framing in force.
1342    #[must_use]
1343    pub const fn pl_cdr1_framing(&self) -> PlCdr1Framing {
1344        self.pl_cdr1
1345    }
1346
1347    /// The writer, set to append (or not) the RTI/CoreDX minimal-member
1348    /// trailing octet. See [`Writer::minimal_member_trailing_octet`].
1349    #[must_use]
1350    pub const fn with_minimal_member_trailing_octet(mut self, on: bool) -> Self {
1351        self.minimal_member_trailing_octet = on;
1352        self
1353    }
1354
1355    /// Whether this writer appends the RTI/CoreDX minimal-member trailing
1356    /// octet (XTypes 1.3 §7.3.4.5; `docs/omg-xtypes-issues.md` §C2).
1357    #[must_use]
1358    pub const fn minimal_member_trailing_octet(&self) -> bool {
1359        self.minimal_member_trailing_octet
1360    }
1361
1362    /// A writer whose alignment is capped at `max_align`.
1363    ///
1364    /// Four for XCDR2, eight for XCDR1 — take it from
1365    /// `zerodds_ast_layout::Encoding::max_alignment` rather than
1366    /// writing a number here.
1367    #[must_use]
1368    pub fn with_max_align(endianness: Endianness, max_align: usize) -> Self {
1369        Self {
1370            max_align: max_align.max(1),
1371            ..Self::new(endianness)
1372        }
1373    }
1374
1375    /// A writer that appends to `out`, with its alignment cap at
1376    /// `max_align` and alignment counted from where `out` ends.
1377    ///
1378    /// For an encoder handed a buffer to fill: a writer of its own grows
1379    /// from empty and its bytes are then copied over, and for a 1 kB sample
1380    /// that was four reallocations and a copy per write (measured on the
1381    /// Linux test host). Counting from `out`'s end makes the bytes the same
1382    /// as a fresh writer's whose bytes were appended to `out`.
1383    /// [`Self::finish`] hands `out` back with the sample behind what it
1384    /// held.
1385    #[must_use]
1386    pub fn appending(endianness: Endianness, max_align: usize, out: Vec<u8>) -> Self {
1387        Self {
1388            origin: out.len(),
1389            out,
1390            ..Self::with_max_align(endianness, max_align)
1391        }
1392    }
1393
1394    /// A writer that continues a stream whose first octets are `prefix`:
1395    /// it appends behind them and counts alignment from the start of
1396    /// `prefix`, not from where `prefix` ends (which is what
1397    /// [`Self::appending`] does).
1398    ///
1399    /// For a value that is a member of a larger object whose earlier
1400    /// members were written by someone else — DDS-RPC 1.0 §7.5.1.1.6 puts
1401    /// the `Call` union behind a `RequestHeader` of variable length, and
1402    /// under XCDR1 the union's 8-octet members align to the sample, not to
1403    /// the union. [`Self::finish`] hands back `prefix` with the value
1404    /// behind it.
1405    #[must_use]
1406    pub fn continuing(endianness: Endianness, max_align: usize, prefix: Vec<u8>) -> Self {
1407        Self {
1408            out: prefix,
1409            ..Self::with_max_align(endianness, max_align)
1410        }
1411    }
1412
1413    /// The alignment cap in force.
1414    #[must_use]
1415    pub const fn max_align(&self) -> usize {
1416        self.max_align
1417    }
1418
1419    /// The byte order this writer was given.
1420    ///
1421    /// The counterpart of [`crate::reader::Reader::endianness`], for the
1422    /// callers that have to put the order they are writing in *into* the
1423    /// stream — a CDR encapsulation opens with a byte-order octet that
1424    /// says which order its body is in.
1425    #[must_use]
1426    pub const fn endianness(&self) -> Endianness {
1427        self.endianness
1428    }
1429
1430    /// A big-endian writer, which is what a KeyHash holder is.
1431    #[must_use]
1432    pub fn big_endian() -> Self {
1433        Self::new(Endianness::Big)
1434    }
1435
1436    /// A little-endian writer.
1437    #[must_use]
1438    pub fn little_endian() -> Self {
1439        Self::new(Endianness::Little)
1440    }
1441
1442    /// The bytes written so far.
1443    #[must_use]
1444    pub fn as_bytes(&self) -> &[u8] {
1445        &self.out
1446    }
1447
1448    /// The bytes, with the writer consumed.
1449    #[must_use]
1450    pub fn finish(self) -> Vec<u8> {
1451        self.out
1452    }
1453
1454    /// How many bytes have been written.
1455    #[must_use]
1456    pub fn position(&self) -> usize {
1457        self.out.len()
1458    }
1459
1460    /// Pad to `align`, counted from the stream's origin.
1461    pub fn align_to(&mut self, align: usize) {
1462        let align = align.min(self.max_align);
1463        if align <= 1 {
1464            return;
1465        }
1466        let offset = self.out.len().saturating_sub(self.origin);
1467        let padding = (align - (offset % align)) % align;
1468        if padding == 0 {
1469            return;
1470        }
1471        if let Some(spans) = self.padding.as_mut() {
1472            spans.push((self.out.len(), padding));
1473        }
1474        for _ in 0..padding {
1475            self.out.push(0);
1476        }
1477    }
1478
1479    /// Begin a collection element whose alignment is counted from its
1480    /// own first octet, returning the origin it replaced.
1481    ///
1482    /// XTypes 1.3 §7.4.1 counts a value's alignment from the
1483    /// encapsulation, so an element of an array or a sequence begins at
1484    /// the next multiple of its alignment and the collection pads
1485    /// *between* elements. `zerodds_ast_layout::ElementPadding::Packed`
1486    /// is the measured departure: the element begins where the previous
1487    /// one ended and its own members align from there. Resetting the
1488    /// origin here is the whole of it; [`Self::finish_packed_element`]
1489    /// puts the stream's origin back so the collection's tail aligns
1490    /// from the encapsulation again.
1491    pub fn begin_packed_element(&mut self) -> usize {
1492        let saved = self.origin;
1493        self.origin = self.out.len();
1494        saved
1495    }
1496
1497    /// Restore the stream origin a packed element displaced. The pair of
1498    /// [`Self::begin_packed_element`].
1499    pub fn finish_packed_element(&mut self, saved: usize) {
1500        self.origin = saved;
1501    }
1502
1503    // -- primitives ------------------------------------------------------
1504
1505    /// `boolean`, one byte, `0` or `1`.
1506    pub fn write_bool(&mut self, value: bool) {
1507        self.out.push(u8::from(value));
1508    }
1509
1510    /// `octet` and `uint8`.
1511    pub fn write_u8(&mut self, value: u8) {
1512        self.out.push(value);
1513    }
1514
1515    /// `fixed<digits, scale>`, as BCD — CORBA 3.3 part 2 §9.3.2.8.
1516    ///
1517    /// The value arrives as a decimal string and not as a number, because a
1518    /// `fixed<31, 2>` is exactly what a `f64` cannot carry: the type exists to
1519    /// hold a decimal exactly, and a binary float on the way in would defeat
1520    /// it before the first nibble.
1521    ///
1522    /// Two nibbles to the byte, high first, sign last: `0xd` negative, `0xc`
1523    /// otherwise, zero included. A negative zero goes out positive, because
1524    /// there is one zero to write.
1525    ///
1526    /// No DDS vendor generates code for `fixed` — Cyclone, Fast DDS and RTI
1527    /// emit nothing for it and OpenDDS does not compile it, measured across
1528    /// the corpus — so there is no vendor oracle here, only the clause and
1529    /// the twenty other columns of this project that already write it.
1530    ///
1531    /// # Errors
1532    /// [`WireError`] when the declaration is outside IDL rule (42)'s 31
1533    /// digits, when `scale` exceeds `digits`, or when the text is not a
1534    /// decimal that fits `fixed<digits, scale>`. A value that does not fit is
1535    /// refused rather than truncated: a silently shortened decimal is the
1536    /// wrong number with no way to notice.
1537    pub fn write_fixed(
1538        &mut self,
1539        text: &str,
1540        digits: u8,
1541        scale: u8,
1542        policy: FixedDigits,
1543    ) -> Result<(), WireError> {
1544        let nibbles = fixed_nibbles(text, digits, scale, policy, self.position())?;
1545        // The count is even by construction, so the last byte carries a digit
1546        // and the sign and never half a byte of nothing.
1547        for pair in nibbles.chunks_exact(2) {
1548            self.write_u8((pair[0] << 4) | pair[1]);
1549        }
1550        Ok(())
1551    }
1552
1553    /// `int8`.
1554    pub fn write_i8(&mut self, value: i8) {
1555        #[allow(clippy::cast_sign_loss)]
1556        self.out.push(value as u8);
1557    }
1558
1559    /// `char`, one byte. DDS-XTypes 1.3 §7.4.1.1.1, Table 31.
1560    ///
1561    /// §7.2.2.2.1.2.2 gives the whole rule: "This specification does not
1562    /// define an encoding for the `CHAR_8_TYPE`. The only constraint is
1563    /// that it shall be representable using 8 bits." A code point above
1564    /// U+00FF is not, and no byte stands for it.
1565    ///
1566    /// # Errors
1567    /// [`WireError::OutOfRange`] above U+00FF. This used to be
1568    /// `value as u32 as u8`, which drops the high bits and puts a
1569    /// different character on the wire with nothing said.
1570    pub fn write_char(&mut self, value: char) -> Result<(), WireError> {
1571        let point = u32::from(value);
1572        let byte = u8::try_from(point).map_err(|_| WireError::OutOfRange {
1573            at: self.out.len(),
1574            expected: format!("a character below U+0100, not U+{point:04X}"),
1575        })?;
1576        self.out.push(byte);
1577        Ok(())
1578    }
1579
1580    /// `wchar`: one UTF-16 code unit, at the width
1581    /// [`Writer::wchar_width`] names. DDS-XTypes 1.3 §7.4.1.1.1,
1582    /// Table 31 gives two bytes; [`WcharWidth`] carries the one measured
1583    /// implementation that writes four and where it does so.
1584    ///
1585    /// §7.2.2.2.1.2.5: "The `CHAR_16_TYPE` shall be restricted to
1586    /// representing Unicode codepoints in the Basic Multilingual Plane.
1587    /// That is Unicode codepoints from 0x0000 to U+FFFF." A code point
1588    /// above the BMP takes two UTF-16 code units and has no `wchar`.
1589    /// That is a property of the type and not of the width, so the
1590    /// four-byte form refuses it too: RTI zero-extends a code *unit*,
1591    /// it does not carry a code point.
1592    ///
1593    /// `position` says whether this `wchar` is a member or a collection
1594    /// element. Under [`WcharFraming::GiopLengthPrefixedMemberOnly`] the
1595    /// two take different octets, and there is no default: a caller that
1596    /// could leave it out would take the member's framing for an
1597    /// element, which is the shape of the defect that put the argument
1598    /// here. `tao_idl` writes `04 e4000000` for a `wchar` member of
1599    /// `primitive-wchar` and a bare `e4000000` for the first element of
1600    /// `array-1d-primitives`.
1601    ///
1602    /// # Errors
1603    /// [`WireError::OutOfRange`] above U+FFFF, for the reason
1604    /// [`Writer::write_char`] gives at its own width.
1605    pub fn write_wchar(&mut self, value: char, position: WcharPosition) -> Result<(), WireError> {
1606        let point = u32::from(value);
1607        let unit = u16::try_from(point).map_err(|_| WireError::OutOfRange {
1608            at: self.out.len(),
1609            expected: format!("a BMP character, not U+{point:04X}"),
1610        })?;
1611        if self.wchar_framing.has_length_octet(position) {
1612            // CORBA §9.3.1.6's GIOP 1.2 form: the octet count first,
1613            // then the code unit's octets in the stream's order, all
1614            // aligned to one. omniORB writes `02 00e4` for U+00E4 in
1615            // either stream order and TAO `04 e4000000` on this
1616            // little-endian host; the count is the transmission code
1617            // set's width and not the value's, so it is the same octet
1618            // for every character.
1619            let width = self.wchar_width.bytes();
1620            #[allow(clippy::cast_possible_truncation)]
1621            self.out.push(width as u8);
1622            // The octets follow the same axis a `wstring`'s code
1623            // units do, with one difference the clause makes: a
1624            // scalar cannot carry a byte-order mark, so CORBA's
1625            // default applies and `ByteOrderMark` degrades to
1626            // big-endian here. Measured 2026-08-10 in a
1627            // *little-endian* stream: omniORB writes `02 00e4` and
1628            // JacORB `02 00e4`, both big-endian; TAO writes
1629            // `04 e4000000`, the stream's order.
1630            let big = !matches!(self.wstring_unit_order, WstringUnitOrder::Stream);
1631            match (self.wchar_width, big) {
1632                (WcharWidth::Two, false) => self.extend(&self.order2(unit)),
1633                (WcharWidth::Two, true) => self.extend(&unit.to_be_bytes()),
1634                (WcharWidth::Four, false) => self.extend(&self.order4(u32::from(unit))),
1635                (WcharWidth::Four, true) => self.extend(&u32::from(unit).to_be_bytes()),
1636            }
1637        } else {
1638            // Table 31's fixed-width unit, aligned to its own width.
1639            // Reached by every DDS implementation in every position and
1640            // by TAO inside a collection, where its generated code
1641            // marshals the element array directly and never passes the
1642            // per-character path that writes the count.
1643            match self.wchar_width {
1644                WcharWidth::Two => self.write_u16(unit),
1645                WcharWidth::Four => self.write_u32(u32::from(unit)),
1646            }
1647        }
1648        Ok(())
1649    }
1650
1651    /// [`Writer::write_wchar`] at the member position.
1652    ///
1653    /// The generated codecs call this and
1654    /// [`Writer::write_wchar_element`] rather than passing a position
1655    /// along: a collection combinator takes `Fn(&mut Writer, &char)`,
1656    /// and a two-argument method cannot be handed to one as a path
1657    /// without a closure that would hide which position it chose.
1658    ///
1659    /// # Errors
1660    /// As [`Writer::write_wchar`].
1661    pub fn write_wchar_member(&mut self, value: char) -> Result<(), WireError> {
1662        self.write_wchar(value, WcharPosition::Member)
1663    }
1664
1665    /// [`Writer::write_wchar`] at the collection-element position.
1666    ///
1667    /// # Errors
1668    /// As [`Writer::write_wchar`].
1669    pub fn write_wchar_element(&mut self, value: &char) -> Result<(), WireError> {
1670        self.write_wchar(*value, WcharPosition::Element)
1671    }
1672
1673    /// `short`.
1674    pub fn write_i16(&mut self, value: i16) {
1675        #[allow(clippy::cast_sign_loss)]
1676        self.write_u16(value as u16);
1677    }
1678
1679    /// A bitmask, in the holder Table 33 of XTypes 1.3 §7.4.1.1.3.2
1680    /// gives for its `@bit_bound`.
1681    ///
1682    /// The bound and not the language's holder: `@bit_bound(12)` is a
1683    /// `uint16_t` in C and a `u16` here, and the two octets it writes
1684    /// carry twelve bits that mean something and four that do not. The
1685    /// argument arrives from `zerodds_ast_layout::plan::scalar_write`,
1686    /// which is the same crossing every other column asks — the C
1687    /// runtime's `zdw_write_bitmask` takes the same pair in the same
1688    /// order.
1689    ///
1690    /// # Refused, not masked
1691    ///
1692    /// A value with a bit set above the bound is refused rather than
1693    /// truncated to the bound. Masking would put a value on the wire
1694    /// nobody wrote and report success, and the peer would read a set
1695    /// the sender never held. `runtime/c/src/writer.c` refuses the same
1696    /// value with `ZDW_RANGE`.
1697    ///
1698    /// # Errors
1699    /// [`WireError::OutOfRange`] for a bound of zero or above 64, and
1700    /// for a value that does not fit the bound.
1701    pub fn write_bitmask(&mut self, bit_bound: u16, bits: u64) -> Result<(), WireError> {
1702        if bit_bound == 0 || bit_bound > 64 {
1703            return Err(WireError::OutOfRange {
1704                at: self.out.len(),
1705                expected: alloc::format!(
1706                    "a bitmask bound between 1 and 64, and this one is {bit_bound}"
1707                ),
1708            });
1709        }
1710        if bit_bound < 64 && (bits >> bit_bound) != 0 {
1711            return Err(WireError::OutOfRange {
1712                at: self.out.len(),
1713                expected: alloc::format!(
1714                    "a bitmask value inside its bound of {bit_bound} bits, and this one is {bits}"
1715                ),
1716            });
1717        }
1718        // The check above is what makes each narrowing exact: a value
1719        // that did not fit its bound never reaches this match.
1720        #[allow(clippy::cast_possible_truncation)]
1721        match bit_bound {
1722            0..=8 => self.write_u8(bits as u8),
1723            9..=16 => self.write_u16(bits as u16),
1724            17..=32 => self.write_u32(bits as u32),
1725            _ => self.write_u64(bits),
1726        }
1727        Ok(())
1728    }
1729
1730    /// A bitset, as the anchor integer its bitfields pack into.
1731    ///
1732    /// The anchor width is the layout plan's answer and arrives as a
1733    /// byte count, which is what `zerodds_ast_layout::xcdr::plan` gives
1734    /// as the type's extent and what `zdw_write_bitset` takes.
1735    ///
1736    /// XTypes 1.3 has no serialisation rule for a bitset — §7.4.3.5.3
1737    /// runs from (1) to (30) and none matches one. What is implemented
1738    /// is the only reading under which an IDL bitset round-trips
1739    /// through the TypeObject of §7.3.4.6: positions run cumulatively
1740    /// from zero in declaration order, anonymous padding included. The
1741    /// C runtime carries the same paragraph.
1742    ///
1743    /// # Errors
1744    /// [`WireError::OutOfRange`] for an anchor that is not one, two,
1745    /// four or eight octets.
1746    pub fn write_bitset(&mut self, anchor_bytes: u32, bits: u64) -> Result<(), WireError> {
1747        // Exact for the same reason `write_bitmask`'s narrowings are:
1748        // `bitfield` refused every value that does not fit its width,
1749        // and the plan sized the anchor over the widths.
1750        #[allow(clippy::cast_possible_truncation)]
1751        match anchor_bytes {
1752            1 => self.write_u8(bits as u8),
1753            2 => self.write_u16(bits as u16),
1754            4 => self.write_u32(bits as u32),
1755            8 => self.write_u64(bits),
1756            other => {
1757                return Err(WireError::OutOfRange {
1758                    at: self.out.len(),
1759                    expected: alloc::format!(
1760                        "a bitset anchor of 1, 2, 4 or 8 octets, and this one is {other}"
1761                    ),
1762                });
1763            }
1764        }
1765        Ok(())
1766    }
1767
1768    /// One bitfield's contribution to its bitset's anchor: the value
1769    /// shifted to its position.
1770    ///
1771    /// A value wider than the field is **refused** rather than masked.
1772    /// Masking would put a different value on the wire than the caller
1773    /// held and report success — the generated field is as wide as its
1774    /// holder and not as wide as its `bitfield<N>`, so `bitfield<3> a`
1775    /// is a `u8` that can hold 200 and a bitset that can not.
1776    /// `zdw_bitfield` refuses the same value with `ZDW_RANGE`.
1777    ///
1778    /// # Errors
1779    /// [`WireError::OutOfRange`] for a width outside 1..=64, a field
1780    /// that runs past the sixty-fourth bit, or a value the width cannot
1781    /// hold.
1782    pub fn bitfield(&mut self, value: u64, width: u32, position: u32) -> Result<u64, WireError> {
1783        if width == 0 || width > 64 || position.saturating_add(width) > 64 {
1784            return Err(WireError::OutOfRange {
1785                at: self.out.len(),
1786                expected: alloc::format!(
1787                    "a bitfield inside the anchor's 64 bits, and this one is {width} bits at {position}"
1788                ),
1789            });
1790        }
1791        let limit = if width >= 64 {
1792            u64::MAX
1793        } else {
1794            (1u64 << width) - 1
1795        };
1796        if value > limit {
1797            return Err(WireError::OutOfRange {
1798                at: self.out.len(),
1799                expected: alloc::format!(
1800                    "a bitfield value inside its {width} bits, and this one is {value}"
1801                ),
1802            });
1803        }
1804        Ok(value << position)
1805    }
1806
1807    /// `unsigned short`.
1808    pub fn write_u16(&mut self, value: u16) {
1809        self.align_to(2);
1810        self.extend(&self.order2(value));
1811    }
1812
1813    /// A 16-bit value in **little-endian** order whatever the stream's
1814    /// order is, aligned to two like any other.
1815    ///
1816    /// One caller and one reason: Cyclone DDS swaps a `wstring`'s length
1817    /// word for a big-endian stream and does not swap its code units.
1818    /// `zerodds_ast_layout::WstringLengthPolicy::UnswappedUnits` carries
1819    /// the measurement and the argument for reproducing it on request.
1820    /// Nothing else on the wire has this shape, and a member that used
1821    /// it without that reason would be a member no peer reads.
1822    pub fn write_u16_little(&mut self, value: u16) {
1823        self.align_to(2);
1824        self.extend(&value.to_le_bytes());
1825    }
1826
1827    /// A 16-bit value in **big-endian** order whatever the stream's
1828    /// order is, aligned to two like any other.
1829    ///
1830    /// The counterpart of [`Writer::write_u16_little`] on the other
1831    /// side of the same axis. Both omniORB and JacORB decode a
1832    /// `wstring`'s code units as UTF-16 big-endian whatever the stream
1833    /// says — the OMG code-set rule for a GIOP 1.2 `wstring` that
1834    /// carries no byte-order mark — so this is the form a `wstring`
1835    /// written for an ORB takes in a little-endian stream.
1836    pub fn write_u16_big(&mut self, value: u16) {
1837        self.align_to(2);
1838        self.extend(&value.to_be_bytes());
1839    }
1840
1841    /// A 32-bit value in **little-endian** order whatever the stream's
1842    /// order is.
1843    ///
1844    /// A four-octet transmission code set crossed with a fixed unit
1845    /// order: reachable as `--wstring-length specification-wide-units
1846    /// --wstring-units byte-order-mark` and by no dialect, because no
1847    /// measured implementation writes wide units in a fixed order. It
1848    /// exists so that two orthogonal settings have an answer for every
1849    /// combination rather than a hole one of them falls into.
1850    pub fn write_u32_little(&mut self, value: u32) {
1851        self.align_to(4);
1852        self.extend(&value.to_le_bytes());
1853    }
1854
1855    /// A 32-bit value in **big-endian** order whatever the stream's
1856    /// order is. The counterpart of [`Writer::write_u32_little`].
1857    pub fn write_u32_big(&mut self, value: u32) {
1858        self.align_to(4);
1859        self.extend(&value.to_be_bytes());
1860    }
1861
1862    /// `long`.
1863    pub fn write_i32(&mut self, value: i32) {
1864        #[allow(clippy::cast_sign_loss)]
1865        self.write_u32(value as u32);
1866    }
1867
1868    /// `unsigned long`.
1869    pub fn write_u32(&mut self, value: u32) {
1870        self.align_to(4);
1871        self.extend(&self.order4(value));
1872    }
1873
1874    /// `long long`.
1875    pub fn write_i64(&mut self, value: i64) {
1876        #[allow(clippy::cast_sign_loss)]
1877        self.write_u64(value as u64);
1878    }
1879
1880    /// `unsigned long long`.
1881    pub fn write_u64(&mut self, value: u64) {
1882        self.align_to(8);
1883        self.extend(&self.order8(value));
1884    }
1885
1886    /// `float`.
1887    pub fn write_f32(&mut self, value: f32) {
1888        self.write_u32(value.to_bits());
1889    }
1890
1891    /// `double`.
1892    pub fn write_f64(&mut self, value: f64) {
1893        self.write_u64(value.to_bits());
1894    }
1895
1896    /// `long double`: sixteen bytes of IEEE 754 binary128.
1897    ///
1898    /// CORBA 3.3 part 2 §9.3.1.3 with Figure 9.2 gives the layout — one
1899    /// sign bit, a fifteen-bit exponent, a 112-bit fraction — and shows
1900    /// the two byte orders as the whole sixteen bytes reversed, not two
1901    /// eight-byte halves swapped in place. DDS-XTypes 1.3 Table 31 gives
1902    /// the alignment: eight, capped to four by `MAXALIGN` under XCDR2,
1903    /// which is what [`Writer::align_to`] already does.
1904    ///
1905    /// This used to take an `f64` and write it followed by eight zero
1906    /// bytes, because Rust has no stable `f128`. That was the right
1907    /// length and the wrong value in every column and it reported
1908    /// success; `zerodds_float128::Float128` carries the value instead.
1909    /// Nothing here computes with it — a wire runtime never needs to.
1910    pub fn write_f128(&mut self, value: Float128) {
1911        self.align_to(8);
1912        let bytes = if self.endianness.is_little() {
1913            value.to_le_bytes()
1914        } else {
1915            value.to_be_bytes()
1916        };
1917        self.extend(&bytes);
1918    }
1919
1920    // -- strings ---------------------------------------------------------
1921
1922    /// A string: a four-byte length including the terminator, the bytes,
1923    /// then a NUL. DDS-XTypes 1.3 §7.4.1.1.
1924    ///
1925    /// The length counts the terminator, which is the one place XCDR
1926    /// differs from every other length-prefixed encoding.
1927    pub fn write_string(&mut self, value: &str) {
1928        #[allow(clippy::cast_possible_truncation)]
1929        let length = (value.len() + 1) as u32;
1930        self.write_u32(length);
1931        self.extend(value.as_bytes());
1932        self.out.push(0);
1933    }
1934
1935    /// A bounded string.
1936    ///
1937    /// The same bytes as an unbounded one: the bound is a promise about
1938    /// the source, not a difference on the wire. It is checked here
1939    /// rather than trusted, because a value that overruns its bound is
1940    /// one no peer expected.
1941    ///
1942    /// The bound counts **octets**, which is what `string<N>` means to
1943    /// every peer that has to hold the value — `char[N + 1]` in the C
1944    /// mapping of all four DDS implementations, `fixed_string<N>` in
1945    /// Fast DDS's.
1946    ///
1947    /// # Refused, not truncated
1948    ///
1949    /// This truncated at a character boundary and reported nothing,
1950    /// which is `AL-14` of `docs/wire-feature-inventory.md`: acceptable
1951    /// only while nothing called it. Something does now — the generated
1952    /// codec emits `write_string_bounded` for every bounded `string`
1953    /// member — and a writer that truncated would put a value on the
1954    /// wire nobody wrote. §7.2.2.7's TRIM is a *read*-side behaviour and
1955    /// belongs to the receiver's bound, not the sender's;
1956    /// `wire-probe/src/encode.rs::write_string` refuses the same values
1957    /// (D6), so the two columns now agree.
1958    ///
1959    /// # Errors
1960    /// [`WireError::OutOfRange`] for a value longer than the bound.
1961    pub fn write_string_bounded(&mut self, value: &str, bound: usize) -> Result<(), WireError> {
1962        if value.len() > bound {
1963            return Err(WireError::OutOfRange {
1964                at: self.out.len(),
1965                expected: alloc::format!(
1966                    "at most {bound} octets in a bounded string, and this one has {}",
1967                    value.len()
1968                ),
1969            });
1970        }
1971        self.write_string(value);
1972        Ok(())
1973    }
1974
1975    /// A `wstring`: a four-octet length, then UTF-16 code units, in the
1976    /// cell `policy` and `order` name.
1977    ///
1978    /// XTypes 1.3 §7.4.3.5.3 rule (4) writes `O.ssize` as a `UInt32`
1979    /// and then the `Char16` units, annotated "No NUL" against rule
1980    /// (3)'s "includes NUL" for a narrow string. What `O.ssize` counts
1981    /// is settled in §7.4.1.1.2: "The serialized length of an object of
1982    /// type `String<Char16>` shall be the number of bytes in the CDR
1983    /// buffer taken by the `String<Char16>` characters. This is twice the
1984    /// number of characters in the string" — code units, not
1985    /// characters, so a `"𝄞"` is a length of four and not of one or
1986    /// two. The same clause forbids the two things a UTF-16 encoder
1987    /// would otherwise emit: "The UTF-16 representation … shall not
1988    /// include a Byte Order Mark (BOM). The representation shall also
1989    /// not include any terminating NUL character(s)."
1990    ///
1991    /// That is one cell of eight. [`WstringLengthPolicy`] carries the
1992    /// other seven and the runs that measured five of them, and
1993    /// [`WstringUnitOrder`] carries the fourth axis. Both arrive as
1994    /// arguments, resolved from `zerodds_ast_layout::LayoutKey`, for the
1995    /// reason `MapDelimiter` arrives as the choice between
1996    /// [`Writer::write_map`] and [`Writer::write_delimited_map`]: a
1997    /// default in the runtime would be a second place the wire format
1998    /// lives, and the two places would disagree the first time either
1999    /// moved.
2000    ///
2001    /// `position` says whether this `wstring` is reached as a member or
2002    /// as a collection element. The nine measured implementations
2003    /// answer both alike, so it changes nothing today — and it is the
2004    /// argument that keeps that a measurement rather than an
2005    /// assumption, the way [`Writer::write_wchar`]'s position is the
2006    /// argument that caught `tao_idl` writing two different framings.
2007    ///
2008    /// # Errors
2009    ///
2010    /// [`WireError::OutOfRange`] for a code point outside the Basic
2011    /// Multilingual Plane under a **character count over two-octet
2012    /// units** — the [`WstringLengthPolicy::CHARACTER_COUNT`] cell.
2013    /// Fast CDR counts `wcslen` characters and then writes each
2014    /// four-octet `wchar_t` as two octets, so `U+1D11E` leaves as
2015    /// `d11e`, which is `U+D11E`, a Hangul syllable and a well-formed
2016    /// code point. The peer reads a different character and has nothing
2017    /// to object to, and the sender left no trace in the bytes.
2018    /// Reproducing a vendor's length convention is a policy;
2019    /// reproducing a data loss no reader can see is not.
2020    ///
2021    /// The refusal is on the pair (characters, two octets) and not on a
2022    /// named form: it is the narrow unit that loses the high bits, so
2023    /// [`WstringLengthPolicy::ROS_TYPE_SUPPORT`] — a character count
2024    /// over *four*-octet units — carries the surrogate pair intact and
2025    /// is not refused.
2026    pub fn write_wstring(
2027        &mut self,
2028        value: &str,
2029        policy: WstringLengthPolicy,
2030        order: WstringUnitOrder,
2031        position: WirePosition,
2032    ) -> Result<(), WireError> {
2033        let units = self.wstring_run(value, policy, order, position)?;
2034        let width = policy.character_width(position);
2035        // C1's answer, over units that already include C2's terminator
2036        // and C4's mark. `WstringLengthPolicy::declared_length` is the
2037        // one expression, so a cell nobody has measured computes its
2038        // length the same way as the five that are.
2039        //
2040        // `None` is refused rather than written wrapped. `as u32` stood
2041        // here: a length that names a prefix of its own content is the
2042        // silent class, and the peer would have read a well-formed
2043        // shorter `wstring` and then every following member from inside
2044        // this one. Unreachable from a `String` on a 32-bit target,
2045        // where the value cannot be that long in the first place;
2046        // reachable on a 64-bit one, at 2 GiB of text for the narrow
2047        // width and 1 GiB for the wide.
2048        let counts = match policy.counts_at(position) {
2049            WstringLengthCounts::Octets => "octet count",
2050            WstringLengthCounts::Characters => "code-unit count",
2051        };
2052        let run = units.len();
2053        let declared =
2054            policy
2055                .declared_length(run, position)
2056                .ok_or_else(|| WireError::OutOfRange {
2057                    at: self.out.len(),
2058                    expected: format!(
2059                        "a wstring whose {counts} fits the UInt32 length field, and this one \
2060                     has {run} code units at {width} octets each"
2061                    ),
2062                })?;
2063        self.write_u32(declared);
2064        for unit in units {
2065            self.write_wstring_unit(unit, width, order);
2066        }
2067        Ok(())
2068    }
2069
2070    /// A bounded `wstring`.
2071    ///
2072    /// The same octets as an unbounded one of the same length: the
2073    /// bound is a promise about the source, not a difference on the
2074    /// wire.
2075    ///
2076    /// The bound counts **UTF-16 code units**, which is what
2077    /// `wstring<N>` means to every peer that has to hold the value — an
2078    /// `N`-unit buffer in all four DDS implementations' mappings, and
2079    /// what §7.4.1.1.2's own count is in. Counting Unicode scalar
2080    /// values instead would accept a five-character value of six code
2081    /// units that no peer can hold; counting octets would make the same
2082    /// declaration mean two different capacities under C3's two widths.
2083    ///
2084    /// The terminator [`WstringTerminator::Present`] adds is **not**
2085    /// counted against the bound. `wstring<5>` names a capacity of five
2086    /// characters and RTI's XCDR1 form writes six units for five
2087    /// characters; counting the terminator would refuse a value the
2088    /// declaration admits.
2089    ///
2090    /// # Errors
2091    ///
2092    /// [`WireError::OutOfRange`] for a value longer than the bound,
2093    /// refused and not truncated for the reason
2094    /// [`Writer::write_string_bounded`] gives — §7.2.2.7's TRIM is a
2095    /// *read*-side behaviour and belongs to the receiver's bound. And
2096    /// whatever [`Writer::write_wstring`] refuses.
2097    pub fn write_wstring_bounded(
2098        &mut self,
2099        value: &str,
2100        bound: usize,
2101        policy: WstringLengthPolicy,
2102        order: WstringUnitOrder,
2103        position: WirePosition,
2104    ) -> Result<(), WireError> {
2105        let units = value.encode_utf16().count();
2106        if units > bound {
2107            return Err(WireError::OutOfRange {
2108                at: self.out.len(),
2109                expected: format!(
2110                    "at most {bound} UTF-16 code units in a bounded wstring, \
2111                     and this one has {units}"
2112                ),
2113            });
2114        }
2115        self.write_wstring(value, policy, order, position)
2116    }
2117
2118    /// A `wstring` whose characters go out as their own UTF-8 octets.
2119    ///
2120    /// The write side of `WstringCodeUnits::Utf8`, the measured departure
2121    /// of one product's generated serialiser: XTypes 1.3 Table 31 gives a
2122    /// `wstring`'s code unit as `Char16` and §7.4.1.1.2 makes the length
2123    /// "twice the number of characters", so the clause has no UTF-8 form
2124    /// at all. The length is the UTF-8 octet count plus C2's terminator
2125    /// when the cell carries one: under this value C1 and C3 have nothing
2126    /// to say, because there is no fixed-width code unit for a character
2127    /// count to count and no width for an octet count to multiply by.
2128    ///
2129    /// The order axis is not consulted: UTF-8 octets have no byte order
2130    /// of their own.
2131    ///
2132    /// # Errors
2133    /// [`WireError::OutOfRange`] for an octet count that does not fit the
2134    /// UInt32 length field, refused rather than written wrapped — a
2135    /// length that names a prefix of its own content is the silent class,
2136    /// and the peer would read every following member from inside this
2137    /// one.
2138    pub fn write_wstring_utf8(
2139        &mut self,
2140        value: &str,
2141        policy: WstringLengthPolicy,
2142        position: WirePosition,
2143    ) -> Result<(), WireError> {
2144        let bytes = value.as_bytes();
2145        let terminator = matches!(policy.terminator_at(position), WstringTerminator::Present);
2146        let content = bytes.len() + usize::from(terminator);
2147        let declared = u32::try_from(content).map_err(|_| WireError::OutOfRange {
2148            at: self.out.len(),
2149            expected: format!(
2150                "a wstring whose UTF-8 octet count fits the UInt32 length field, and this one \
2151                 has {content} octets"
2152            ),
2153        })?;
2154        self.write_u32(declared);
2155        for byte in bytes {
2156            self.write_u8(*byte);
2157        }
2158        if terminator {
2159            self.write_u8(0);
2160        }
2161        Ok(())
2162    }
2163
2164    /// A bounded `wstring` whose characters go out as UTF-8 octets.
2165    ///
2166    /// The same octets as the unbounded one of the same length; the bound
2167    /// is a promise about the source and counts UTF-16 code units, as
2168    /// [`Writer::write_wstring_bounded`] does, because that is what
2169    /// `wstring<N>` means to a peer whatever the content's encoding.
2170    ///
2171    /// # Errors
2172    /// [`WireError::OutOfRange`] for a value longer than the bound, and
2173    /// whatever [`Writer::write_wstring_utf8`] refuses.
2174    pub fn write_wstring_utf8_bounded(
2175        &mut self,
2176        value: &str,
2177        bound: usize,
2178        policy: WstringLengthPolicy,
2179        position: WirePosition,
2180    ) -> Result<(), WireError> {
2181        let units = value.encode_utf16().count();
2182        if units > bound {
2183            return Err(WireError::OutOfRange {
2184                at: self.out.len(),
2185                expected: format!(
2186                    "at most {bound} UTF-16 code units in a bounded wstring, \
2187                     and this one has {units}"
2188                ),
2189            });
2190        }
2191        self.write_wstring_utf8(value, policy, position)
2192    }
2193
2194    /// The code units one `wstring` puts on the wire: the text's, plus
2195    /// C2's terminator, plus C4's mark.
2196    ///
2197    /// One function for all three because the three compose in one
2198    /// order and the length counts what comes out of it. A caller that
2199    /// assembled them itself could count the terminator and forget the
2200    /// mark, which is a length that disagrees with its own content.
2201    fn wstring_run(
2202        &self,
2203        value: &str,
2204        policy: WstringLengthPolicy,
2205        order: WstringUnitOrder,
2206        position: WirePosition,
2207    ) -> Result<Vec<u16>, WireError> {
2208        let narrow_count = matches!(policy.counts_at(position), WstringLengthCounts::Characters)
2209            && policy.unit_at(position) == WstringUnitWidth::Two;
2210        let mut units: Vec<u16> = if narrow_count {
2211            // The BMP refusal. See this method's caller for why it is
2212            // an error and not a truncation, and why it is on the pair
2213            // and not on a named form.
2214            if let Some(point) = value.chars().find(|c| *c as u32 > 0xFFFF) {
2215                let point = u32::from(point);
2216                return Err(WireError::OutOfRange {
2217                    at: self.out.len(),
2218                    expected: format!(
2219                        "a code point inside the BMP, not U+{point:04X}: a character count \
2220                         over two-octet units has no way to carry one outside it, and Fast \
2221                         CDR truncates it to sixteen bits and sends a different character"
2222                    ),
2223                });
2224            }
2225            #[allow(clippy::cast_possible_truncation)]
2226            value.chars().map(|c| c as u32 as u16).collect()
2227        } else {
2228            value.encode_utf16().collect()
2229        };
2230        if matches!(policy.terminator_at(position), WstringTerminator::Present) {
2231            // CORBA §9.3.1.6's GIOP 1.1 rule, and RTI's XCDR1 form. The
2232            // NUL is a unit of the run and is inside whatever the
2233            // length counts.
2234            units.push(0);
2235        }
2236        // An **empty** `wstring` carries no mark: omniORB writes
2237        // `00000000` for one, measured 2026-08-10 as
2238        // `wstring-unbounded-empty`, which is byte-identical to every
2239        // other implementation. A mark on an empty string would be a
2240        // two-octet string that decodes to nothing, and no peer writes
2241        // it.
2242        if order.has_mark() && !units.is_empty() {
2243            units.insert(0, BYTE_ORDER_MARK);
2244        }
2245        Ok(units)
2246    }
2247
2248    /// One `wstring` code unit, at its width and in its order.
2249    ///
2250    /// C3 picks the writer and C4 picks its endianness, in one place, so
2251    /// that a cell cannot be written one way in the length field and
2252    /// another in the content. There is no arm for a length policy that
2253    /// decides a byte order: `HostUnswapped` is a point of the order
2254    /// axis, so Cyclone's form and `rosidl_typesupport_fastrtps`'s are
2255    /// the same answer at two different widths rather than two
2256    /// unrelated special cases.
2257    fn write_wstring_unit(&mut self, unit: u16, width: usize, order: WstringUnitOrder) {
2258        match (width, order) {
2259            (2, WstringUnitOrder::Stream) => self.write_u16(unit),
2260            (2, WstringUnitOrder::BigEndianAlways) => self.write_u16_big(unit),
2261            (2, WstringUnitOrder::ByteOrderMark | WstringUnitOrder::HostUnswapped) => {
2262                self.write_u16_little(unit);
2263            }
2264            (_, WstringUnitOrder::Stream) => self.write_u32(u32::from(unit)),
2265            (_, WstringUnitOrder::BigEndianAlways) => self.write_u32_big(u32::from(unit)),
2266            (_, WstringUnitOrder::ByteOrderMark | WstringUnitOrder::HostUnswapped) => {
2267                self.write_u32_little(u32::from(unit));
2268            }
2269        }
2270    }
2271
2272    // -- framing ---------------------------------------------------------
2273
2274    /// A DHEADER: a four-byte length the members that follow occupy.
2275    ///
2276    /// Written as a placeholder and patched when the members are done,
2277    /// because the length is not known until then.
2278    #[must_use]
2279    pub fn write_dheader(&mut self) -> DHeader {
2280        self.align_to(4);
2281        let at = self.out.len();
2282        self.extend(&[0u8; 4]);
2283        DHeader { at }
2284    }
2285
2286    /// Patch a DHEADER with the length of what followed it.
2287    ///
2288    /// A DHEADER is a `UInt32` and "O.ssize is constrained to being smaller
2289    /// than 4 Giga Bytes" (XTypes 1.3 §7.4.3.4.1): an object past that has
2290    /// no DHEADER. This form has no way to return the error, so it records
2291    /// it: the placeholder stays unpatched, [`Self::deferred_error`] returns
2292    /// the error and [`Self::finish_checked`] and [`Self::check_deferred`]
2293    /// refuse the stream. A caller that can return an error uses
2294    /// [`Self::try_finish_dheader`] and gets it at the call.
2295    pub fn finish_dheader(&mut self, header: DHeader) {
2296        let span = self.out.len() - header.at - 4;
2297        self.finish_dheader_spanning(header, span);
2298    }
2299
2300    /// [`Self::finish_dheader`] for members that occupy `span` octets.
2301    fn finish_dheader_spanning(&mut self, header: DHeader, span: usize) {
2302        if let Err(error) = self.patch_dheader(header, span) {
2303            self.defer(error);
2304        }
2305    }
2306
2307    /// Patch `header` with `span`, or refuse a `span` a `UInt32` does not
2308    /// hold.
2309    fn patch_dheader(&mut self, header: DHeader, span: usize) -> Result<(), WireError> {
2310        let length = checked_dheader_length(span, header.at)?;
2311        let bytes = self.order4(length);
2312        self.out[header.at..header.at + 4].copy_from_slice(&bytes);
2313        Ok(())
2314    }
2315
2316    /// Remember the first error of a call that cannot return one.
2317    fn defer(&mut self, error: WireError) {
2318        if self.deferred.is_none() {
2319            self.deferred = Some(error);
2320        }
2321    }
2322
2323    /// The first error an infallible framing call met, if one did.
2324    ///
2325    /// Set by [`Self::finish_dheader`] for an object of 4 GiB or more.
2326    #[must_use]
2327    pub fn deferred_error(&self) -> Option<&WireError> {
2328        self.deferred.as_ref()
2329    }
2330
2331    /// [`Self::deferred_error`] as a result.
2332    ///
2333    /// # Errors
2334    /// The first error an infallible framing call met.
2335    pub fn check_deferred(&self) -> Result<(), WireError> {
2336        match &self.deferred {
2337            Some(error) => Err(error.clone()),
2338            None => Ok(()),
2339        }
2340    }
2341
2342    /// The bytes, refused when an infallible framing call met an error.
2343    ///
2344    /// # Errors
2345    /// [`Self::deferred_error`]'s error.
2346    pub fn finish_checked(self) -> Result<Vec<u8>, WireError> {
2347        match self.deferred {
2348            Some(error) => Err(error),
2349            None => Ok(self.out),
2350        }
2351    }
2352
2353    /// [`Self::finish_dheader`], refusing a length a `UInt32` does not
2354    /// hold.
2355    ///
2356    /// # Errors
2357    /// [`WireError::OutOfRange`] when the members that followed the
2358    /// DHEADER occupy 4 GiB or more (XTypes 1.3 §7.4.3.4.1). The buffer is
2359    /// left as it was, with the placeholder unpatched, for the caller to
2360    /// discard.
2361    pub fn try_finish_dheader(&mut self, header: DHeader) -> Result<(), WireError> {
2362        let span = self.out.len() - header.at - 4;
2363        self.patch_dheader(header, span)
2364    }
2365
2366    /// An XCDR2 EMHEADER: the member id, its length code and the
2367    /// must-understand flag in one 32-bit word.
2368    ///
2369    /// The length code is the layout plan's decision and arrives here as
2370    /// a number; this crate does not choose one. XTypes 1.3 §7.4.3.4.2.
2371    ///
2372    /// # Errors
2373    /// [`WireError::OutOfRange`] for a member id above
2374    /// [`E_MEMBER_ID_MASK`]. §7.2.2.4.4.4.4 runs the ids from 0 to
2375    /// 0x0FFFFFFF and §7.4.3.4.2 leaves them the low 28 bits of the word.
2376    /// This used to be `member_id & 0x0FFF_FFFF`, which writes a
2377    /// well-formed header for a member the caller did not name — the peer
2378    /// reads the wrong member and nothing at either end says so.
2379    pub fn write_emheader(
2380        &mut self,
2381        member_id: u32,
2382        length_code: u8,
2383        must_understand: bool,
2384    ) -> Result<EmHeader, WireError> {
2385        if member_id > E_MEMBER_ID_MASK {
2386            return Err(WireError::OutOfRange {
2387                at: self.out.len(),
2388                expected: format!("a member id inside the 28 bits of an EMHEADER, not {member_id}"),
2389            });
2390        }
2391        self.align_to(4);
2392        let word = (u32::from(must_understand) << 31)
2393            | (u32::from(length_code & 0x7) << 28)
2394            | (member_id & 0x0FFF_FFFF);
2395        self.write_u32(word);
2396
2397        // LC0 through LC3 carry the length in the code itself.
2398        //
2399        // LC4 is followed by a NEXTINT holding the member's length.
2400        //
2401        // LC5 through LC7 also have a NEXTINT, but §7.4.3.4.2 says it
2402        // "shall be reused also as part of the serialized member ...
2403        // because the serialization of certain members also starts with
2404        // an integer length, which would take exactly the same value as
2405        // NEXTINT. Therefore the use of length codes 5 to 7 saves 4
2406        // bytes in the serialization." So no placeholder is reserved:
2407        // the member's own leading word lands in that position and *is*
2408        // the NEXTINT. Reserving one here would write the length twice.
2409        let word_at = self.out.len() - 4;
2410        if length_code == 4 {
2411            let at = self.out.len();
2412            self.extend(&[0u8; 4]);
2413            return Ok(EmHeader {
2414                word: word_at,
2415                next_int: Some(at),
2416                length_code,
2417            });
2418        }
2419        Ok(EmHeader {
2420            word: word_at,
2421            next_int: None,
2422            length_code,
2423        })
2424    }
2425
2426    /// Patch an EMHEADER's NEXTINT with the length of what followed.
2427    ///
2428    /// Under [`Writer::short_length_codes`] it may instead delete the
2429    /// NEXTINT and put the length in the code, which is what Fast DDS
2430    /// does for a member whose serialized size turns out to be 1, 2, 4
2431    /// or 8 bytes. See [`Writer::with_short_length_codes`].
2432    /// # Errors
2433    /// [`WireError::OutOfRange`] when the member written under a code of
2434    /// 0 to 3 is not the length that code declares. §7.4.3.4.2 makes the
2435    /// code *be* the length there — 1, 2, 4 or 8 octets — so a value of
2436    /// another size misdeclares it and the reader's skip lands inside
2437    /// data. Codes 4 to 7 carry a NEXTINT and declare nothing of their
2438    /// own, so they cannot be wrong that way.
2439    pub fn finish_emheader(&mut self, header: EmHeader) -> Result<(), WireError> {
2440        let Some(at) = header.next_int else {
2441            if header.length_code <= 3 && header.word + 4 <= self.out.len() {
2442                let declared = 1usize << header.length_code;
2443                let body = self.out.len() - header.word - 4;
2444                if body != declared {
2445                    let code = header.length_code;
2446                    return Err(WireError::OutOfRange {
2447                        at: self.out.len(),
2448                        expected: format!(
2449                            "a member of {declared} octets under length code {code}, not {body}"
2450                        ),
2451                    });
2452                }
2453            }
2454            return Ok(());
2455        };
2456        #[allow(clippy::cast_possible_truncation)]
2457        let length = (self.out.len() - at - 4) as u32;
2458        if self.short_length_codes && matches!(length, 1 | 2 | 4 | 8) {
2459            self.shorten_emheader(header.word, at, length);
2460            return Ok(());
2461        }
2462        let bytes = self.order4(length);
2463        self.out[at..at + 4].copy_from_slice(&bytes);
2464        Ok(())
2465    }
2466
2467    /// Replace an LC4 header's NEXTINT with a short length code.
2468    ///
2469    /// The four placeholder bytes are removed and the code in the header
2470    /// word is raised from 4 to the one that spells `length`. Every byte
2471    /// of the member's value is unchanged and only its position moves,
2472    /// which is safe for one reason and only under XCDR2: `MAXALIGN` is
2473    /// four there, the value began at a four-aligned offset with the
2474    /// NEXTINT in front of it and begins at a four-aligned offset
2475    /// without it, so no padding inside it was computed against an
2476    /// offset that has moved.
2477    ///
2478    /// Nothing outside the member has been written yet — `finish_emheader`
2479    /// runs immediately after the value — so the only handles pointing
2480    /// past `at` are this one's, and both are consumed here. An enclosing
2481    /// DHEADER or parameter header sits at a lower offset and is patched
2482    /// afterwards from `self.out.len()`, which the removal has already
2483    /// corrected.
2484    fn shorten_emheader(&mut self, word_at: usize, next_int_at: usize, length: u32) {
2485        let code: u32 = match length {
2486            1 => 0,
2487            2 => 1,
2488            4 => 2,
2489            _ => 3,
2490        };
2491        self.out.drain(next_int_at..next_int_at + 4);
2492        self.shift_padding(next_int_at, -4_isize);
2493        // `word_at` is four bytes below `next_int_at` by construction —
2494        // `write_emheader` set both from the same `out.len()` — and the
2495        // drain removed bytes above it, so the slice below is in range
2496        // for the same reason `finish_dheader`'s is. Indexed rather than
2497        // fetched with a fallback: a fallback here would leave the code
2498        // at 4 over a stream whose NEXTINT had already been removed,
2499        // which is four bytes of the member read as its own length.
2500        let previous = if self.endianness.is_little() {
2501            u32::from_le_bytes([
2502                self.out[word_at],
2503                self.out[word_at + 1],
2504                self.out[word_at + 2],
2505                self.out[word_at + 3],
2506            ])
2507        } else {
2508            u32::from_be_bytes([
2509                self.out[word_at],
2510                self.out[word_at + 1],
2511                self.out[word_at + 2],
2512                self.out[word_at + 3],
2513            ])
2514        };
2515        let patched = (previous & !(0x7 << 28)) | (code << 28);
2516        let out = self.order4(patched);
2517        self.out[word_at..word_at + 4].copy_from_slice(&out);
2518    }
2519
2520    /// A PL_CDR1 parameter header, short form: `[id:u16][length:u16]`.
2521    ///
2522    /// `role` says what the length field will count when
2523    /// [`Writer::finish_pid`] patches it; see [`ParameterRole`].
2524    ///
2525    /// `must_understand` is bit 14 of the id. §7.4.1.2.2: the flag
2526    /// "shall be set if and only if the must_understand property of the
2527    /// member is set to true". A member declared `@must_understand`
2528    /// whose header does not say so goes out looking skippable, and a
2529    /// peer that does not know it skips it.
2530    ///
2531    /// # Errors
2532    /// [`WireError::OutOfRange`] for an id above
2533    /// [`PlCdr1Framing::highest_short_id`]. §7.4.1.2.2 gives the short
2534    /// form the ids 0 to that value inclusive and reserves what is above
2535    /// it for the protocol's own parameters — [`PID_EXTENDED`] is 0x3F01
2536    /// itself — so such an id takes [`Writer::write_pid_extended`] or it
2537    /// does not go out. The field was read only by the reader until this
2538    /// check; the plan chooses the form and this is the floor under a
2539    /// caller that did not.
2540    pub fn write_pid(
2541        &mut self,
2542        pid: u16,
2543        role: ParameterRole,
2544        must_understand: bool,
2545    ) -> Result<Pid, WireError> {
2546        if pid > self.pl_cdr1.highest_short_id {
2547            return Err(WireError::OutOfRange {
2548                at: self.out.len(),
2549                expected: format!("a parameter id the short header carries, not {pid}")
2550                    + " — the extended header takes it",
2551            });
2552        }
2553        self.align_to(4);
2554        // Two claimants on the same word, deliberately: the member's own
2555        // declaration, and whatever the framing puts on every id
2556        // regardless. OpenDDS's `c0010004` is the second alone, and its
2557        // `@must_understand` member is the same octets, which is why the
2558        // two are OR-ed rather than chosen between.
2559        let flags = self.pl_cdr1.pid_flags.flags_at(WirePosition::Member);
2560        let pid = if must_understand && self.pl_cdr1.member_must_understand {
2561            pid | PID_MUST_UNDERSTAND | flags
2562        } else {
2563            pid | flags
2564        };
2565        self.write_u16(pid);
2566        let at = self.out.len();
2567        self.extend(&[0u8; 2]);
2568        Ok(Pid {
2569            at,
2570            extended: false,
2571            // The id without either flag. Masking only must-understand
2572            // left `PID_IMPL_EXTENSION` in the id a promotion to the
2573            // extended form would carry into the `eMemberHeader`, where
2574            // it is a different bit and would read as an id of
2575            // 0x8000-something.
2576            pid: u32::from(pid & !(PID_MUST_UNDERSTAND | PID_IMPL_EXTENSION)),
2577            must_understand,
2578            role,
2579            saved_origin: self.begin_parameter_value(),
2580        })
2581    }
2582
2583    /// A PL_CDR1 parameter header, extended form.
2584    ///
2585    /// `[PID_EXTENDED][8][id:u32][length:u32]`, required when the id
2586    /// reaches the reserved range or the value is longer than `0xFFFF`,
2587    /// and used for every member under
2588    /// `zerodds_ast_layout::PlCdr1HeaderPolicy::AlwaysExtended`.
2589    /// XTypes 1.3 §7.4.1.2.
2590    ///
2591    /// The marker carries the must-understand bit when the framing says
2592    /// so — Cyclone DDS writes `0x7f01`, and its reader would not be
2593    /// misled by `0x3f01` either, but the bytes differ.
2594    ///
2595    /// The **member's** must-understand is a different bit in a
2596    /// different word: `FLAG_2` of the 32-bit `eMemberHeader`,
2597    /// §7.4.1.2.1. Cyclone sets the marker's bit on every parameter it
2598    /// writes and `FLAG_2` only on the members that declare it —
2599    /// `017f0800 01000040` against `017f0800 02000000` in the same
2600    /// sample — so reading the member's property off the marker would
2601    /// make every Cyclone parameter must-understand.
2602    ///
2603    /// # Errors
2604    /// [`WireError::OutOfRange`] for a member id above
2605    /// [`E_MEMBER_ID_MASK`], for the reason
2606    /// [`Writer::write_emheader`] gives: the `eMemberHeader` of
2607    /// §7.4.1.2.1 has the same 28 bits, and masking aliases the parameter
2608    /// onto another member.
2609    pub fn write_pid_extended(
2610        &mut self,
2611        pid: u32,
2612        role: ParameterRole,
2613        must_understand: bool,
2614    ) -> Result<Pid, WireError> {
2615        if pid > E_MEMBER_ID_MASK {
2616            return Err(WireError::OutOfRange {
2617                at: self.out.len(),
2618                expected: format!("a member id inside the 28 bits of an eMemberHeader, not {pid}"),
2619            });
2620        }
2621        self.align_to(4);
2622        let marker = PID_EXTENDED
2623            | self
2624                .pl_cdr1
2625                .pid_flags
2626                .flags_at(WirePosition::ExtendedMarker);
2627        self.write_u16(marker);
2628        self.write_u16(8);
2629        let member_header = if must_understand && self.pl_cdr1.member_must_understand {
2630            (pid & E_MEMBER_ID_MASK) | E_FLAG_MUST_UNDERSTAND
2631        } else {
2632            pid & E_MEMBER_ID_MASK
2633        };
2634        self.write_u32(member_header);
2635        let at = self.out.len();
2636        self.extend(&[0u8; 4]);
2637        Ok(Pid {
2638            at,
2639            extended: true,
2640            pid,
2641            must_understand,
2642            role,
2643            saved_origin: self.begin_parameter_value(),
2644        })
2645    }
2646
2647    /// Start a parameter's value, returning the origin it replaces.
2648    ///
2649    /// "Each element, or parameter, within a parameter list data
2650    /// structure is simply a CDR-encapsulated block of data" —
2651    /// XTypes 1.3 §7.4.1.2. A block counts alignment from its own start,
2652    /// so an eight-byte member at the head of one is already aligned.
2653    ///
2654    /// Without this a `double` took four bytes of padding *inside* its
2655    /// block, and `finish_pid` counted them into the declared length:
2656    /// `01000c00` where Cyclone, RTI and Fast DDS all write `01000800`.
2657    /// A reader that trusts the length reads four bytes too many and is
2658    /// wrong from there on.
2659    fn begin_parameter_value(&mut self) -> usize {
2660        let saved = self.origin;
2661        self.origin = self.out.len();
2662        saved
2663    }
2664
2665    /// Patch a parameter header with its value's length.
2666    ///
2667    /// The length field never counts the alignment padding *inside* the
2668    /// block: a parameter's value is its own CDR-encapsulated block and
2669    /// counts alignment from its own start, XTypes 1.3 §7.4.1.2, which
2670    /// is why an eight-byte member at the head of one declares eight and
2671    /// not twelve. All three measured implementations agree on that.
2672    ///
2673    /// Whether it counts the padding that *follows* the value is
2674    /// `PlCdr1Framing::pads_declared_length`, asked per
2675    /// [`ParameterRole`]: Cyclone and RTI round a `@mutable` member's
2676    /// declared length up to four and Fast DDS stops at the value, and
2677    /// for an `@optional` member of a non-mutable type only RTI rounds.
2678    ///
2679    /// An absent `@optional` member finishes here with nothing written
2680    /// between the header and this call, which declares the length of
2681    /// zero §7.4.1.2 asks for.
2682    pub fn finish_pid(&mut self, header: Pid) {
2683        let width = if header.extended { 4 } else { 2 };
2684        let mut length = self.out.len() - header.at - width;
2685        if self.pl_cdr1.pads_declared_length(header.role) {
2686            // Counted from the origin the padding will be measured
2687            // from, which is the enclosing stream's and not the
2688            // parameter block's — `align_to` runs below, after the
2689            // restore. Computing it against the block's own origin
2690            // would agree today, because a parameter value starts
2691            // four-aligned, and would stop agreeing the moment that
2692            // changed.
2693            let past = (self.out.len() - header.saved_origin) % 4;
2694            if past != 0 {
2695                length += 4 - past;
2696            }
2697        }
2698        if header.extended {
2699            #[allow(clippy::cast_possible_truncation)]
2700            let bytes = self.order4(length as u32);
2701            self.out[header.at..header.at + 4].copy_from_slice(&bytes);
2702        } else if length > PID_SHORT_MAX_LEN {
2703            self.promote_pid(&header, length);
2704        } else {
2705            #[allow(clippy::cast_possible_truncation)]
2706            let bytes = self.order2(length as u16);
2707            self.out[header.at..header.at + 2].copy_from_slice(&bytes);
2708        }
2709        // The padding follows the value and is outside the length. It
2710        // starts the next parameter ID at a four-byte offset relative to
2711        // the previous one, so it is measured from the enclosing stream
2712        // and not from the block that just ended.
2713        self.origin = header.saved_origin;
2714        // Under Fast DDS's framing the parameter writes no padding of
2715        // its own — the next header aligns itself, and a final
2716        // parameter ends the stream where its value does.
2717        if self.pl_cdr1.pad_after_value {
2718            self.align_to(4);
2719        }
2720    }
2721
2722    /// Rewrite a short parameter header as the extended form.
2723    ///
2724    /// §7.4.1.2.2's `ssize` is sixteen bits, so a value longer than
2725    /// `PID_SHORT_MAX_LEN` cannot state its length there and §7.4.1.2.3
2726    /// requires the extended form. Which values those are is a property
2727    /// of the *sample* and not of the type: a `string` member is short
2728    /// in almost every sample and over 64 KiB in some.
2729    ///
2730    /// The plan used to answer for it by giving every unbounded member
2731    /// the extended header, on the reasoning that a plan cannot promise
2732    /// the value will fit. That is true of a plan and not of a writer:
2733    /// the length is patched here, after the value, and so is the form.
2734    /// Consequence, measured: `mut_string` is 24 bytes from RTI and from
2735    /// Fast DDS and was 32 from this project under either dialect —
2736    /// twelve bytes of header where the peer writes four, on every
2737    /// `string` and every `sequence` of every `@mutable` XCDR1 type.
2738    ///
2739    /// Eight bytes are inserted between the header and the value and the
2740    /// four fields are written over them. The value's own bytes do not
2741    /// change: XCDR1's `MAXALIGN` is eight, the insertion is eight, so
2742    /// every offset inside the value keeps its remainder and no padding
2743    /// it already contains was computed against an offset that moved.
2744    /// The trailing `align_to(4)` below is likewise unaffected, eight
2745    /// being a multiple of four.
2746    fn promote_pid(&mut self, header: &Pid, length: usize) {
2747        let start = header.at - 2;
2748        self.out.splice(header.at + 2..header.at + 2, [0u8; 8]);
2749        self.shift_padding(header.at + 2, 8_isize);
2750
2751        let marker = PID_EXTENDED
2752            | self
2753                .pl_cdr1
2754                .pid_flags
2755                .flags_at(WirePosition::ExtendedMarker);
2756        let marker = self.order2(marker);
2757        self.out[start..start + 2].copy_from_slice(&marker);
2758        let slength = self.order2(8);
2759        self.out[start + 2..start + 4].copy_from_slice(&slength);
2760
2761        let member_header = if header.must_understand && self.pl_cdr1.member_must_understand {
2762            (header.pid & E_MEMBER_ID_MASK) | E_FLAG_MUST_UNDERSTAND
2763        } else {
2764            header.pid & E_MEMBER_ID_MASK
2765        };
2766        let member_header = self.order4(member_header);
2767        self.out[start + 4..start + 8].copy_from_slice(&member_header);
2768
2769        #[allow(clippy::cast_possible_truncation)]
2770        let llength = self.order4(length as u32);
2771        self.out[start + 8..start + 12].copy_from_slice(&llength);
2772    }
2773
2774    /// The `PID_LIST_END` sentinel a PL_CDR1 parameter list ends with.
2775    ///
2776    /// A reader that stops at the last member it recognises instead of
2777    /// at the sentinel misreads whatever follows.
2778    /// The flags come from [`PlCdr1Framing::pid_flags`] asked at
2779    /// [`WirePosition::Sentinel`], which is the only thing that decides
2780    /// them: `PID_LIST_END` is not a member and has no declaration of
2781    /// its own to carry.
2782    pub fn write_sentinel(&mut self) {
2783        self.align_to(4);
2784        let sentinel = PID_LIST_END | self.pl_cdr1.pid_flags.flags_at(WirePosition::Sentinel);
2785        self.write_u16(sentinel);
2786        self.write_u16(0);
2787    }
2788
2789    // -- collections -----------------------------------------------------
2790
2791    /// A sequence: a four-byte count, then each element.
2792    ///
2793    /// The element function is fallible, and every element function is,
2794    /// whether or not the element itself can refuse. Three of the four
2795    /// element kinds a collection can hold *can* — a `char` outside
2796    /// Latin-1 (§7.2.2.2.1.2.2), a `wchar` outside the BMP, and a
2797    /// nested aggregate whose own `encode` reaches one — and a second
2798    /// infallible overload beside this one would be a shape for the
2799    /// generator to pick between, one construct at a time. `Ok(…)`
2800    /// around an infallible write costs nothing and reads as what it
2801    /// is.
2802    ///
2803    /// # Errors
2804    /// Whatever the element function returns.
2805    pub fn write_sequence<T, F: Fn(&mut Self, &T) -> Result<(), WireError>>(
2806        &mut self,
2807        values: &[T],
2808        element: F,
2809    ) -> Result<(), WireError> {
2810        #[allow(clippy::cast_possible_truncation)]
2811        self.write_u32(values.len() as u32);
2812        for value in values {
2813            element(self, value)?;
2814        }
2815        Ok(())
2816    }
2817
2818    /// An array: each element, with no count.
2819    ///
2820    /// The element count is in the type, not on the wire.
2821    /// XTypes 1.3 §7.4.3.5.
2822    ///
2823    /// # Errors
2824    /// As [`Writer::write_sequence`].
2825    pub fn write_array<T, F: Fn(&mut Self, &T) -> Result<(), WireError>>(
2826        &mut self,
2827        values: &[T],
2828        element: F,
2829    ) -> Result<(), WireError> {
2830        for value in values {
2831            element(self, value)?;
2832        }
2833        Ok(())
2834    }
2835
2836    /// A sequence behind a delimiter.
2837    ///
2838    /// XTypes 1.3 §7.4.3.5 rule (12): under XCDR2 a sequence whose
2839    /// element type is not primitive carries a `DHEADER` ahead of the
2840    /// length. Which sequences those are is
2841    /// `zerodds_ast_layout::collection_is_delimited`, not a decision
2842    /// taken here.
2843    ///
2844    /// # Errors
2845    /// As [`Writer::write_sequence`]. The delimiter is patched before
2846    /// the error is returned only when there is no error: a refused
2847    /// element leaves a partial buffer, which the caller discards.
2848    pub fn write_delimited_sequence<T, F: Fn(&mut Self, &T) -> Result<(), WireError>>(
2849        &mut self,
2850        values: &[T],
2851        element: F,
2852    ) -> Result<(), WireError> {
2853        let dheader = self.write_dheader();
2854        self.write_sequence(values, element)?;
2855        self.try_finish_dheader(dheader)
2856    }
2857
2858    /// An array behind a delimiter. Rule (9), the array counterpart.
2859    ///
2860    /// # Errors
2861    /// As [`Writer::write_delimited_sequence`].
2862    pub fn write_delimited_array<T, F: Fn(&mut Self, &T) -> Result<(), WireError>>(
2863        &mut self,
2864        values: &[T],
2865        element: F,
2866    ) -> Result<(), WireError> {
2867        let dheader = self.write_dheader();
2868        self.write_array(values, element)?;
2869        self.try_finish_dheader(dheader)
2870    }
2871
2872    /// A `sequence<T, N>`, whose count is checked against `N`.
2873    ///
2874    /// The bytes are a plain sequence's: Table 17's
2875    /// `O2.length <= T1.length` is a property of the *value*, and the
2876    /// wire carries no bound to compare it with. Refused rather than
2877    /// written short, for the reason [`Writer::write_string_bounded`]
2878    /// gives — the receiver's own `sequence<T, N>` has room for `N`, and
2879    /// a sender that wrote more has written a value of a type nobody
2880    /// declared.
2881    ///
2882    /// # Errors
2883    /// [`WireError::OutOfRange`] for more than `bound` elements, and
2884    /// whatever the element function returns.
2885    pub fn write_sequence_bounded<T, F: Fn(&mut Self, &T) -> Result<(), WireError>>(
2886        &mut self,
2887        values: &[T],
2888        bound: usize,
2889        element: F,
2890    ) -> Result<(), WireError> {
2891        self.check_sequence_bound(values.len(), bound)?;
2892        self.write_sequence(values, element)
2893    }
2894
2895    /// A bounded sequence behind a delimiter. Rule (12).
2896    ///
2897    /// # Errors
2898    /// As [`Writer::write_sequence_bounded`].
2899    pub fn write_delimited_sequence_bounded<T, F: Fn(&mut Self, &T) -> Result<(), WireError>>(
2900        &mut self,
2901        values: &[T],
2902        bound: usize,
2903        element: F,
2904    ) -> Result<(), WireError> {
2905        self.check_sequence_bound(values.len(), bound)?;
2906        self.write_delimited_sequence(values, element)
2907    }
2908
2909    /// The bound check both bounded-sequence writers make, before any
2910    /// byte of the value is written.
2911    fn check_sequence_bound(&self, length: usize, bound: usize) -> Result<(), WireError> {
2912        if length > bound {
2913            return Err(WireError::OutOfRange {
2914                at: self.out.len(),
2915                expected: alloc::format!(
2916                    "at most {bound} elements in a bounded sequence, and this one has {length}"
2917                ),
2918            });
2919        }
2920        Ok(())
2921    }
2922
2923    /// A map: a four-byte **entry** count, then each entry's key
2924    /// followed by its value.
2925    ///
2926    /// # The count is entries and not halves
2927    ///
2928    /// XTypes 1.3 §7.4.1.1.4 defines a `map<K, V>` by equivalence to a
2929    /// `sequence<Entry>` whose element is the pair, so the `O.length`
2930    /// that precedes it counts pairs. Two independent readings say the
2931    /// same:
2932    ///
2933    /// - the layout plan. `zerodds_ast_layout::xcdr`'s `TypeKind::Map`
2934    ///   arm gives the map `max(key, value, 4)` alignment and one
2935    ///   `SEQUENCE_LENGTH_ALIGNMENT` length word, not one per half;
2936    /// - a recorded foreign measurement.
2937    ///   `tests/interop-results/sources/vendor-corpus.tsv`, case
2938    ///   `map-key-string` under `fastdds`, XCDR1 big-endian, is
2939    ///   `00000003` followed by three `(string, long)` pairs and then
2940    ///   the `ffffffff` of the trailing `long tail`. Three entries, six
2941    ///   halves, and the word on the wire is `3`.
2942    ///
2943    /// # The entries go out in the order the container holds them
2944    ///
2945    /// This runtime takes a `BTreeMap` and walks it, so the order is
2946    /// ascending by key — which is what
2947    /// `zerodds_ast_layout::MapEntryOrder::AscendingKey` is defined as:
2948    /// what a binding that holds a `map` in an ordered container writes.
2949    /// The order is a property of the container the caller already
2950    /// chose, not a second decision taken here.
2951    ///
2952    /// Whether a delimited map *also* carries this count is the one
2953    /// thing the specification says twice and differently — §7.4.1.1.4
2954    /// against rule (15) of §7.4.3.5.3 — and it is the caller's, through
2955    /// which of these four functions it calls. Fast DDS writes both,
2956    /// which is `zerodds_wire_codec::value::MapLengthPolicy`'s default
2957    /// and what [`Writer::write_delimited_map`] does.
2958    ///
2959    /// # Errors
2960    /// Whatever either half's function returns.
2961    pub fn write_map<M, K, V, FK, FV>(
2962        &mut self,
2963        entries: &M,
2964        key: FK,
2965        value: FV,
2966    ) -> Result<(), WireError>
2967    where
2968        M: crate::WireMap<K, V> + ?Sized,
2969        FK: Fn(&mut Self, &K) -> Result<(), WireError>,
2970        FV: Fn(&mut Self, &V) -> Result<(), WireError>,
2971    {
2972        #[allow(clippy::cast_possible_truncation)]
2973        self.write_u32(entries.entry_count() as u32);
2974        entries.try_visit(|k, v| {
2975            key(self, k)?;
2976            value(self, v)
2977        })
2978    }
2979
2980    /// A map behind a delimiter.
2981    ///
2982    /// XTypes 1.3 §7.4.3.5 rule (14) exempts a map from the `DHEADER`
2983    /// only when **both** halves are primitive, and rule (15) delimits
2984    /// every other map under XCDR2. Which maps those are is
2985    /// `zerodds_ast_layout`'s `TypeLayout::needs_dheader`, read through
2986    /// `MapDelimiter`, and not a decision taken here.
2987    ///
2988    /// # Errors
2989    /// As [`Writer::write_map`].
2990    pub fn write_delimited_map<M, K, V, FK, FV>(
2991        &mut self,
2992        entries: &M,
2993        key: FK,
2994        value: FV,
2995    ) -> Result<(), WireError>
2996    where
2997        M: crate::WireMap<K, V> + ?Sized,
2998        FK: Fn(&mut Self, &K) -> Result<(), WireError>,
2999        FV: Fn(&mut Self, &V) -> Result<(), WireError>,
3000    {
3001        let dheader = self.write_dheader();
3002        self.write_map(entries, key, value)?;
3003        self.try_finish_dheader(dheader)
3004    }
3005
3006    /// A `map<K, V, N>`, whose entry count is checked against `N`.
3007    ///
3008    /// The bytes are a plain map's, for the reason
3009    /// [`Writer::write_sequence_bounded`] gives: the wire carries no
3010    /// bound to compare with, so an over-long value is refused rather
3011    /// than written short.
3012    ///
3013    /// # Errors
3014    /// [`WireError::OutOfRange`] for more than `bound` entries, and
3015    /// whatever either half's function returns.
3016    pub fn write_map_bounded<M, K, V, FK, FV>(
3017        &mut self,
3018        entries: &M,
3019        bound: usize,
3020        key: FK,
3021        value: FV,
3022    ) -> Result<(), WireError>
3023    where
3024        M: crate::WireMap<K, V> + ?Sized,
3025        FK: Fn(&mut Self, &K) -> Result<(), WireError>,
3026        FV: Fn(&mut Self, &V) -> Result<(), WireError>,
3027    {
3028        self.check_map_bound(entries.entry_count(), bound)?;
3029        self.write_map(entries, key, value)
3030    }
3031
3032    /// A bounded map behind a delimiter. Rule (15).
3033    ///
3034    /// # Errors
3035    /// As [`Writer::write_map_bounded`].
3036    pub fn write_delimited_map_bounded<M, K, V, FK, FV>(
3037        &mut self,
3038        entries: &M,
3039        bound: usize,
3040        key: FK,
3041        value: FV,
3042    ) -> Result<(), WireError>
3043    where
3044        M: crate::WireMap<K, V> + ?Sized,
3045        FK: Fn(&mut Self, &K) -> Result<(), WireError>,
3046        FV: Fn(&mut Self, &V) -> Result<(), WireError>,
3047    {
3048        self.check_map_bound(entries.entry_count(), bound)?;
3049        self.write_delimited_map(entries, key, value)
3050    }
3051
3052    /// The bound check both bounded-map writers make, before any byte of
3053    /// the value is written.
3054    ///
3055    /// Its own message and not `check_sequence_bound`'s: the
3056    /// unit is an entry and not an element, and a diagnostic that
3057    /// counted a `map<string, long>` in elements would name a number
3058    /// twice the one the declaration bounds.
3059    fn check_map_bound(&self, entries: usize, bound: usize) -> Result<(), WireError> {
3060        if entries > bound {
3061            return Err(WireError::OutOfRange {
3062                at: self.out.len(),
3063                expected: alloc::format!(
3064                    "at most {bound} entries in a bounded map, and this one has {entries}"
3065                ),
3066            });
3067        }
3068        Ok(())
3069    }
3070
3071    // -- the KeyHash -----------------------------------------------------
3072
3073    /// The holder as a KeyHash, zero-padded to sixteen bytes.
3074    ///
3075    /// For a holder of sixteen bytes or fewer. DDSI-RTPS 2.5 §9.6.3.8.
3076    #[must_use]
3077    pub fn zero_padded(&self) -> [u8; 16] {
3078        let mut out = [0u8; 16];
3079        let take = self.out.len().min(16);
3080        out[..take].copy_from_slice(&self.out[..take]);
3081        out
3082    }
3083
3084    /// The MD5 of the holder, for one longer than sixteen bytes.
3085    #[must_use]
3086    pub fn md5(&self) -> [u8; 16] {
3087        crate::md5(&self.out)
3088    }
3089
3090    /// `bytes` as they are, with no alignment and no length in front.
3091    ///
3092    /// The counterpart of [`crate::reader::Reader::read_bytes`], and the
3093    /// one way an already-encoded byte string goes into a stream: a
3094    /// CDR encapsulation built by a second writer, a GIOP request body
3095    /// whose type only the calling stub knows, the octets of a
3096    /// `sequence<octet>` behind their own length.
3097    pub fn write_bytes(&mut self, bytes: &[u8]) {
3098        self.out.extend_from_slice(bytes);
3099    }
3100
3101    // -- internals -------------------------------------------------------
3102
3103    fn extend(&mut self, bytes: &[u8]) {
3104        self.out.extend_from_slice(bytes);
3105    }
3106
3107    fn order2(&self, value: u16) -> [u8; 2] {
3108        if self.endianness.is_little() {
3109            value.to_le_bytes()
3110        } else {
3111            value.to_be_bytes()
3112        }
3113    }
3114
3115    fn order4(&self, value: u32) -> [u8; 4] {
3116        if self.endianness.is_little() {
3117            value.to_le_bytes()
3118        } else {
3119            value.to_be_bytes()
3120        }
3121    }
3122
3123    fn order8(&self, value: u64) -> [u8; 8] {
3124        if self.endianness.is_little() {
3125            value.to_le_bytes()
3126        } else {
3127            value.to_be_bytes()
3128        }
3129    }
3130}
3131
3132/// The value of a DHEADER for members that occupy `span` octets.
3133///
3134/// XTypes 1.3 §7.4.3.4.1: "O.ssize is constrained to being smaller than
3135/// 4 Giga Bytes", because a DHEADER is a `UInt32`.
3136///
3137/// # Errors
3138/// [`WireError::OutOfRange`] naming the offset of the DHEADER, for a span
3139/// of 4 GiB or more.
3140pub fn checked_dheader_length(span: usize, at: usize) -> Result<u32, WireError> {
3141    u32::try_from(span).map_err(|_| WireError::OutOfRange {
3142        at,
3143        expected: alloc::string::String::from(
3144            "an object smaller than 4 GiB, which a DHEADER can hold (XTypes 1.3 7.4.3.4.1)",
3145        ),
3146    })
3147}
3148
3149/// A DHEADER waiting for its length.
3150#[derive(Debug, Clone, Copy)]
3151pub struct DHeader {
3152    at: usize,
3153}
3154
3155/// An EMHEADER waiting for its NEXTINT, when it has one.
3156#[derive(Debug, Clone, Copy)]
3157pub struct EmHeader {
3158    /// Where the EMHEADER word itself sits, so a short code can be
3159    /// patched into it once the value's length is known.
3160    word: usize,
3161    next_int: Option<usize>,
3162    /// The code the header was written with, so that
3163    /// [`Writer::finish_emheader`] can hold the member's value to the
3164    /// length the code declares. Codes 0 to 3 are lengths; anything else
3165    /// declares nothing of its own.
3166    length_code: u8,
3167}
3168
3169/// A parameter header waiting for its length.
3170#[derive(Debug, Clone, Copy)]
3171pub struct Pid {
3172    at: usize,
3173    extended: bool,
3174    /// The member id, kept so that a short header can be rewritten as an
3175    /// extended one when the value turns out not to fit.
3176    pid: u32,
3177    /// Whether the member declared `must_understand`, for the same
3178    /// reason: the bit is in a different word in the two forms.
3179    must_understand: bool,
3180    /// Which length convention the patch will follow.
3181    role: ParameterRole,
3182    /// The stream origin this parameter's block displaced.
3183    saved_origin: usize,
3184}
3185
3186/// The parameter id that introduces the extended form.
3187pub const PID_EXTENDED: u16 = 0x3F01;
3188
3189/// The parameter id that ends a parameter list.
3190pub const PID_LIST_END: u16 = 0x3F02;
3191
3192/// The highest member id a short parameter header can carry.
3193///
3194/// §7.4.1.2.2 gives the short form's range as "from 0 to 16,128
3195/// (0x3F00) inclusive", and `PID_EXTENDED` is 0x3F01 — so 0x3F00 is the
3196/// last usable id and not the first reserved one. It was named for an
3197/// exclusive threshold and read as one, which gave a member with that
3198/// id the twelve-octet header where RTI Connext and Fast DDS both write
3199/// the four-octet one (case `pid_3f00`, 2026-08-10).
3200pub const PID_SHORT_MAX_ID: u16 = 0x3F00;
3201
3202/// The longest value a short parameter header can state the length of.
3203///
3204/// §7.4.1.2.2 gives `ssize` sixteen bits. A value longer than this
3205/// cannot be described by the short form whatever its member id is, and
3206/// `Writer::finish_pid` rewrites the header rather than truncating the
3207/// length into two bytes. `zerodds_ast_layout` has the same constant for
3208/// the plan's half of the same rule.
3209pub const PID_SHORT_MAX_LEN: usize = 0xFFFF;
3210
3211#[cfg(test)]
3212mod tests {
3213    #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
3214    use super::*;
3215
3216    /// XTypes 1.3 §7.4.3.4.1: "O.ssize is constrained to being smaller than
3217    /// 4 Giga Bytes". `finish_dheader` cannot return the error, so it
3218    /// records it, and the placeholder is not patched with a length modulo
3219    /// 2^32.
3220    #[test]
3221    #[cfg(target_pointer_width = "64")]
3222    fn an_infallible_finish_dheader_over_4_gib_is_deferred_and_not_truncated() {
3223        let mut w = Writer::little_endian();
3224        let header = w.write_dheader();
3225        w.write_u32(7);
3226        w.finish_dheader_spanning(header, u32::MAX as usize + 1);
3227        assert!(
3228            matches!(
3229                w.deferred_error(),
3230                Some(WireError::OutOfRange { at: 0, .. })
3231            ),
3232            "{:?}",
3233            w.deferred_error()
3234        );
3235        assert!(w.check_deferred().is_err());
3236        assert_eq!(
3237            &w.as_bytes()[..4],
3238            &[0, 0, 0, 0],
3239            "a length modulo 2^32 was written"
3240        );
3241        assert!(matches!(
3242            w.finish_checked(),
3243            Err(WireError::OutOfRange { .. })
3244        ));
3245    }
3246
3247    #[test]
3248    fn a_dheader_below_4_gib_is_patched_and_nothing_is_deferred() {
3249        let mut w = Writer::little_endian();
3250        let header = w.write_dheader();
3251        w.write_u32(7);
3252        w.finish_dheader(header);
3253        assert!(w.deferred_error().is_none());
3254        assert_eq!(
3255            w.finish_checked().expect("no error"),
3256            [4, 0, 0, 0, 7, 0, 0, 0]
3257        );
3258    }
3259}