Skip to main content

zerodds_wire/
try_construct.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! What a receiving type does with a value it cannot hold.
4//!
5//! DDS-XTypes 1.3 §7.2.2.7 and §7.3.1.2.1.12. The behaviour belongs to
6//! the *position* — a member, a collection element, a map key, a union
7//! case, the discriminator — and not to the type, because one
8//! `typedef string<5>` may be used at two positions that want different
9//! answers. So it arrives here as an argument, exactly as it arrives at
10//! the C runtime as `zdw_try_construct`.
11//!
12//! The bound is not on the wire. Both sides serialise and deserialise
13//! the same bytes and the whole difference is what the receiving side
14//! decides, which is why
15//! `crates/ast-layout/tests/fixtures/try_construct_behaviour.md` records
16//! behaviours rather than byte strings. **OpenDDS is the only one of the
17//! five implementations that implements USE_DEFAULT and TRIM**; Cyclone,
18//! RTI 7.7 and omniORB discard whatever the annotation says, and Fast
19//! DDS cannot generate code for the annotation in any position.
20//!
21//! Every caller reads the whole value off the wire *before* it gets
22//! here. A reader that stopped at its own bound would leave the rest in
23//! the stream and put every following member at the wrong offset —
24//! OpenDDS consumes the whole twelve-character string before it defaults
25//! or truncates, and the members after it come back correct, which is
26//! the property that matters.
27
28use crate::WireError;
29use alloc::collections::BTreeMap;
30use alloc::format;
31use alloc::string::String;
32use alloc::vec::Vec;
33
34/// The three behaviours §7.3.1.2.1.12 names.
35///
36/// The annotation's own parameter defaults to `USE_DEFAULT`, so a bare
37/// `@try_construct` is not an absent one: an *absent* annotation is
38/// DISCARD (§7.2.2.7) and a *valueless* one is USE_DEFAULT
39/// (§7.3.1.2.1.12). Which of the two a declaration wrote is the front
40/// end's question; by the time it reaches here it is one of these.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42pub enum TryConstruct {
43    /// The whole sample is discarded. The default when no annotation is
44    /// written.
45    Discard,
46    /// The member takes its default and the sample survives.
47    UseDefault,
48    /// An over-long string, wstring, sequence or map is truncated to
49    /// what the receiver's type holds.
50    Trim,
51}
52
53/// What a narrowing decided.
54///
55/// `Default` and not the default value itself: Table 9's default for a
56/// type is a *language* value the runtime cannot spell — an empty
57/// `Vec`, a struct literal with a value per member, an enumeration's
58/// first literal — and the generated code has it already, from the same
59/// table the other three columns derive theirs from.
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub enum Narrowed<T> {
62    /// Keep this value. It either fitted or was trimmed to fit.
63    Value(T),
64    /// Substitute the receiving member's default value.
65    Default,
66}
67
68/// A string that has been read whole, against the receiver's bound.
69///
70/// The bound counts **octets**, which is what `string<N>` means to every
71/// peer that has to hold the value: `char[N + 1]` in the C mapping of
72/// all four DDS implementations, `fixed_string<N>` in Fast DDS's.
73/// Counting Unicode scalar values instead would accept a five-character
74/// value of ten octets that no C peer can hold.
75///
76/// A `wstring<N>` bound counts UTF-16 code units instead and goes to
77/// [`fit_wstring`], which is a second function and not an argument: the
78/// two bounds count different things, and one function taking "octets
79/// or units" would be a call site deciding which by getting an
80/// argument right.
81///
82/// # Errors
83/// [`WireError::Discarded`] under [`TryConstruct::Discard`], and
84/// [`WireError::OutOfRange`] for a [`TryConstruct::Trim`] the front end
85/// should have refused — Table 11 makes TRIM on a type that cannot be
86/// trimmed "unspecified and may be treated as an error", Cyclone refuses
87/// such a declaration by name, and reaching here means a model that did
88/// not come through the front end.
89pub fn fit_string(
90    at: usize,
91    value: String,
92    bound: usize,
93    behaviour: TryConstruct,
94) -> Result<Narrowed<String>, WireError> {
95    if value.len() <= bound {
96        return Ok(Narrowed::Value(value));
97    }
98    match behaviour {
99        TryConstruct::Discard => Err(WireError::Discarded {
100            at,
101            expected: format!("at most {bound} octets"),
102            found: format!("{} of them", value.len()),
103        }),
104        TryConstruct::UseDefault => Ok(Narrowed::Default),
105        // §7.3.1.2.1.12.2 keeps the order and takes a prefix: "the
106        // string 'Hello'" out of "Hello World". Never inside a
107        // character — half a UTF-8 sequence is not text, and a C peer
108        // holding `char[bound + 1]` cannot store half of one either.
109        TryConstruct::Trim => {
110            let mut end = bound;
111            while end > 0 && !value.is_char_boundary(end) {
112                end -= 1;
113            }
114            let mut trimmed = value;
115            trimmed.truncate(end);
116            Ok(Narrowed::Value(trimmed))
117        }
118    }
119}
120
121/// A sequence that has been read whole, against the receiver's bound.
122///
123/// Table 17, the `SEQUENCE_TYPE` row: "An object O2 of type T2 can
124/// construct T1 if and only if O2.length <= T1.length."
125/// §7.3.1.2.1.12.3 is the TRIM case and it takes the **first** elements
126/// — "a sequence of 4 longs containing the first four elements of
127/// O2.member".
128///
129/// # Errors
130/// As [`fit_string`].
131pub fn fit_sequence<T>(
132    at: usize,
133    value: Vec<T>,
134    bound: usize,
135    behaviour: TryConstruct,
136) -> Result<Narrowed<Vec<T>>, WireError> {
137    if value.len() <= bound {
138        return Ok(Narrowed::Value(value));
139    }
140    match behaviour {
141        TryConstruct::Discard => Err(WireError::Discarded {
142            at,
143            expected: format!("at most {bound} elements"),
144            found: format!("{}", value.len()),
145        }),
146        TryConstruct::UseDefault => Ok(Narrowed::Default),
147        TryConstruct::Trim => {
148            let mut trimmed = value;
149            trimmed.truncate(bound);
150            Ok(Narrowed::Value(trimmed))
151        }
152    }
153}
154
155/// A map that has been read whole, against the receiver's bound.
156///
157/// Table 11 names `map` among the forms a TRIM may be written on —
158/// "string, wide string, sequence, or map" — and Table 17's
159/// `MAP_TYPE` row carries the same `O2.length <= T1.length` the
160/// `SEQUENCE_TYPE` row does, counted in **entries**.
161///
162/// §7.3.1.2.1.12.3 takes the *first* elements. A map has no declaration
163/// order once it is in an ordered container, so the first `bound`
164/// entries are the `bound` smallest keys — which is the same rule
165/// [`crate::Writer::write_map`] writes under and not a second one.
166///
167/// # Errors
168/// As [`fit_string`].
169pub fn fit_map<K: Ord, V>(
170    at: usize,
171    value: BTreeMap<K, V>,
172    bound: usize,
173    behaviour: TryConstruct,
174) -> Result<Narrowed<BTreeMap<K, V>>, WireError> {
175    if value.len() <= bound {
176        return Ok(Narrowed::Value(value));
177    }
178    match behaviour {
179        TryConstruct::Discard => Err(WireError::Discarded {
180            at,
181            expected: format!("at most {bound} entries"),
182            found: format!("{}", value.len()),
183        }),
184        TryConstruct::UseDefault => Ok(Narrowed::Default),
185        TryConstruct::Trim => Ok(Narrowed::Value(
186            value.into_iter().take(bound).collect::<BTreeMap<K, V>>(),
187        )),
188    }
189}
190
191/// A map held as its entries (XTypes 1.3 §7.5.1.3.6) that has been read whole, against the
192/// receiver's bound, which counts entries.
193///
194/// A trim keeps the first `bound` entries, in the order the stream carried them.
195///
196/// # Errors
197/// As [`fit_string`].
198pub fn fit_map_entries<K, V>(
199    at: usize,
200    mut value: alloc::vec::Vec<crate::MapEntry<K, V>>,
201    bound: usize,
202    behaviour: TryConstruct,
203) -> Result<Narrowed<alloc::vec::Vec<crate::MapEntry<K, V>>>, WireError> {
204    if value.len() <= bound {
205        return Ok(Narrowed::Value(value));
206    }
207    match behaviour {
208        TryConstruct::Discard => Err(WireError::Discarded {
209            at,
210            expected: format!("at most {bound} entries"),
211            found: format!("{}", value.len()),
212        }),
213        TryConstruct::UseDefault => Ok(Narrowed::Default),
214        TryConstruct::Trim => {
215            value.truncate(bound);
216            Ok(Narrowed::Value(value))
217        }
218    }
219}
220
221/// A `wstring` that has been read whole, against the receiver's bound.
222///
223/// The bound counts **UTF-16 code units**, which is what `wstring<N>`
224/// means to every peer that has to hold the value — an `N`-unit buffer
225/// in all four DDS implementations' mappings, and what XTypes 1.3
226/// §7.4.1.1.2's own length is in. Counting Unicode scalar values
227/// instead would accept a five-character value of six code units that
228/// no peer can hold; counting octets would make one declaration mean
229/// two different capacities under the two widths
230/// `zerodds_wire::WstringUnitWidth` carries.
231///
232/// The value arrives as a `String` because that is what
233/// [`crate::Reader::read_wstring`] answers and what the generated field
234/// holds; the count is over its UTF-16 encoding and not over its
235/// octets.
236///
237/// # Errors
238/// As [`fit_string`].
239pub fn fit_wstring(
240    at: usize,
241    value: String,
242    bound: usize,
243    behaviour: TryConstruct,
244) -> Result<Narrowed<String>, WireError> {
245    let units = value.encode_utf16().count();
246    if units <= bound {
247        return Ok(Narrowed::Value(value));
248    }
249    match behaviour {
250        TryConstruct::Discard => Err(WireError::Discarded {
251            at,
252            expected: format!("at most {bound} UTF-16 code units"),
253            found: format!("{units} of them"),
254        }),
255        TryConstruct::UseDefault => Ok(Narrowed::Default),
256        // §7.3.1.2.1.12.2 keeps the order and takes a prefix. Never
257        // inside a character: a lone surrogate is not text, and a peer
258        // holding an `N`-unit buffer cannot store half of a pair
259        // either. So a bound that falls between the two halves of a
260        // surrogate pair takes `bound - 1` units — one short of the
261        // bound, and a well-formed string, which is the pair of
262        // properties a peer can act on.
263        TryConstruct::Trim => {
264            let mut kept: Vec<u16> = value.encode_utf16().take(bound).collect();
265            if kept
266                .last()
267                .is_some_and(|unit| (0xD800..0xDC00).contains(unit))
268            {
269                kept.pop();
270            }
271            // Every unit came out of a `String` and no surrogate pair is
272            // left half-taken, so the re-decode cannot fail; the error
273            // arm is the one the type demands rather than a case that
274            // can be reached.
275            let trimmed = String::from_utf16(&kept).map_err(|_| WireError::OutOfRange {
276                at,
277                expected: String::from("well-formed UTF-16 after a trim to the receiver's bound"),
278            })?;
279            Ok(Narrowed::Value(trimmed))
280        }
281    }
282}