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}