Skip to main content

zerodds_types/type_object/
common.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Shared wire types for TypeObject (Minimal + Complete).
4//!
5//! XTypes §7.3.4.5 (CommonStructMember, NameHash, MemberId).
6
7use alloc::string::String;
8use alloc::vec::Vec;
9
10use zerodds_cdr::{BufferReader, BufferWriter, DecodeError, EncodeError};
11
12use crate::type_identifier::TypeIdentifier;
13
14use super::flags::{StructMemberFlag, UnionMemberFlag};
15
16/// 32-bit member ID (§7.3.4.5). Either assigned explicitly via `@id(n)`
17/// or hashed from the member name (`@autoid(HASH)`).
18pub type MemberId = u32;
19
20/// 4-byte name hash (§7.3.4.5 — "MD5(name)[0..4]").
21///
22/// Stored in the MinimalTypeObject instead of the full name, to
23/// keep the payload small. In the CompleteTypeObject the full
24/// name is additionally present.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
26pub struct NameHash(pub [u8; 4]);
27
28impl NameHash {
29    /// Computes the 4-byte NameHash from a member/literal name.
30    ///
31    /// Spec §7.3.4.5: "the `name_hash` is computed as the first 4
32    /// octets of the MD5 hash of the name, interpreted as ASCII/UTF-8".
33    #[must_use]
34    pub fn from_name(name: &str) -> Self {
35        let digest = zerodds_foundation::md5(name.as_bytes());
36        let out: [u8; 4] = [digest[0], digest[1], digest[2], digest[3]];
37        Self(out)
38    }
39
40    /// Encoded as `octet[4]` (4 bytes, no length, no padding).
41    ///
42    /// # Errors
43    /// Buffer overflow.
44    pub fn encode_into(&self, w: &mut BufferWriter) -> Result<(), EncodeError> {
45        w.write_bytes(&self.0)
46    }
47
48    /// Decoder.
49    ///
50    /// # Errors
51    /// Buffer underflow.
52    pub fn decode_from(r: &mut BufferReader<'_>) -> Result<Self, DecodeError> {
53        let bytes = r.read_bytes(4)?;
54        // `read_bytes(n)` is guaranteed to return exactly n bytes, otherwise
55        // UnexpectedEof. The try_into to [u8; 4] is therefore infallible.
56        let Ok(out): Result<[u8; 4], _> = bytes.try_into() else {
57            return Err(DecodeError::UnexpectedEof {
58                needed: 4,
59                offset: 0,
60            });
61        };
62        Ok(Self(out))
63    }
64}
65
66/// CommonStructMember (§7.3.4.5.2).
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct CommonStructMember {
69    /// Member-ID (4 byte).
70    pub member_id: MemberId,
71    /// Flags (IS_KEY, IS_OPTIONAL, etc.).
72    pub member_flags: StructMemberFlag,
73    /// Type of the member (may be recursive).
74    pub member_type_id: TypeIdentifier,
75}
76
77impl CommonStructMember {
78    /// Encoded as `{ u32 member_id; u16 member_flags; TypeIdentifier member_type_id; }`.
79    ///
80    /// # Errors
81    /// Buffer overflow.
82    pub fn encode_into(&self, w: &mut BufferWriter) -> Result<(), EncodeError> {
83        w.write_u32(self.member_id)?;
84        w.write_u16(self.member_flags.0)?;
85        self.member_type_id.encode_into(w)
86    }
87
88    /// Decoder.
89    ///
90    /// # Errors
91    /// Buffer-Underflow.
92    pub fn decode_from(r: &mut BufferReader<'_>) -> Result<Self, DecodeError> {
93        let member_id = r.read_u32()?;
94        let member_flags = StructMemberFlag(r.read_u16()?);
95        let member_type_id = TypeIdentifier::decode_from(r)?;
96        Ok(Self {
97            member_id,
98            member_flags,
99            member_type_id,
100        })
101    }
102}
103
104/// CommonUnionMember (§7.3.4.5.3). Additionally contains the label list.
105#[derive(Debug, Clone, PartialEq, Eq)]
106pub struct CommonUnionMember {
107    /// Member ID.
108    pub member_id: MemberId,
109    /// Flags (IS_DEFAULT for the default case).
110    pub member_flags: UnionMemberFlag,
111    /// Type of the member.
112    pub type_id: TypeIdentifier,
113    /// Case labels as an `i32` sequence (Spec §7.3.4.5.3.2: `long[]`).
114    pub label_seq: alloc::vec::Vec<i32>,
115}
116
117impl CommonUnionMember {
118    /// Encode.
119    ///
120    /// # Errors
121    /// Buffer overflow.
122    pub fn encode_into(&self, w: &mut BufferWriter) -> Result<(), EncodeError> {
123        w.write_u32(self.member_id)?;
124        w.write_u16(self.member_flags.0)?;
125        self.type_id.encode_into(w)?;
126        // sequence<long>: u32 length + N*i32
127        let len =
128            u32::try_from(self.label_seq.len()).map_err(|_| EncodeError::ValueOutOfRange {
129                message: "union label sequence length exceeds u32::MAX",
130            })?;
131        w.write_u32(len)?;
132        for l in &self.label_seq {
133            w.write_u32(*l as u32)?;
134        }
135        Ok(())
136    }
137
138    /// Decoder.
139    ///
140    /// # Errors
141    /// Buffer underflow.
142    pub fn decode_from(r: &mut BufferReader<'_>) -> Result<Self, DecodeError> {
143        let member_id = r.read_u32()?;
144        let member_flags = UnionMemberFlag(r.read_u16()?);
145        let type_id = TypeIdentifier::decode_from(r)?;
146        let len = r.read_u32()? as usize;
147        let cap = safe_capacity(len, 4, r.remaining());
148        let mut label_seq = alloc::vec::Vec::with_capacity(cap);
149        for _ in 0..len {
150            label_seq.push(r.read_u32()? as i32);
151        }
152        Ok(Self {
153            member_id,
154            member_flags,
155            type_id,
156            label_seq,
157        })
158    }
159}
160
161// ============================================================================
162// Complete-TypeObject-Annotations (§7.3.4.5.4)
163// ============================================================================
164
165/// Full qualified type name, e.g. "::sensors::Chatter". Alias for
166/// `String` — on the wire as a CDR string (u32 length + UTF-8 + null-term).
167pub type QualifiedTypeName = String;
168
169/// Placement kind of a `@verbatim` annotation (§7.3.4.5.4 §PL_*).
170#[derive(Debug, Clone, PartialEq, Eq)]
171pub enum VerbatimPlacement {
172    /// Before the type declaration.
173    Before,
174    /// After the type declaration.
175    After,
176    /// Within the header block (e.g. `#include`).
177    BeginFile,
178    /// End of the file.
179    EndFile,
180    /// Other placement (forward-compat).
181    Other(String),
182}
183
184/// `@verbatim(language, text, placement)`.
185#[derive(Debug, Clone, PartialEq, Eq)]
186pub struct AppliedVerbatimAnnotation {
187    /// Platzierung im Code-Gen-Output.
188    pub placement: VerbatimPlacement,
189    /// Zielsprache (z.B. "c++").
190    pub language: String,
191    /// Literal-Text.
192    pub text: String,
193}
194
195impl AppliedVerbatimAnnotation {
196    fn encode_into(&self, w: &mut BufferWriter) -> Result<(), EncodeError> {
197        let placement_str = match &self.placement {
198            VerbatimPlacement::Before => "BEFORE_DECLARATION",
199            VerbatimPlacement::After => "AFTER_DECLARATION",
200            VerbatimPlacement::BeginFile => "BEGIN_FILE",
201            VerbatimPlacement::EndFile => "END_FILE",
202            VerbatimPlacement::Other(s) => s.as_str(),
203        };
204        w.write_string(placement_str)?;
205        w.write_string(&self.language)?;
206        w.write_string(&self.text)
207    }
208
209    fn decode_from(r: &mut BufferReader<'_>) -> Result<Self, DecodeError> {
210        let placement_str = r.read_string()?;
211        let placement = match placement_str.as_str() {
212            "BEFORE_DECLARATION" => VerbatimPlacement::Before,
213            "AFTER_DECLARATION" => VerbatimPlacement::After,
214            "BEGIN_FILE" => VerbatimPlacement::BeginFile,
215            "END_FILE" => VerbatimPlacement::EndFile,
216            _ => VerbatimPlacement::Other(placement_str),
217        };
218        let language = r.read_string()?;
219        let text = r.read_string()?;
220        Ok(Self {
221            placement,
222            language,
223            text,
224        })
225    }
226}
227
228/// AppliedBuiltinTypeAnnotations (§7.3.4.5.4): `@verbatim` at type level.
229///
230/// Wire: `sequence<AppliedVerbatimAnnotation, 1>` (0 or 1 entry =
231/// "absent"/"present"). Further builtin type annotations (`@unit`,
232/// `@min`, `@max`, `@hash_id`) belong to the member scope, not the type.
233#[derive(Debug, Clone, Default, PartialEq, Eq)]
234pub struct AppliedBuiltinTypeAnnotations {
235    /// Optional `@verbatim` directive.
236    pub verbatim: Option<AppliedVerbatimAnnotation>,
237}
238
239impl AppliedBuiltinTypeAnnotations {
240    /// Encode as `sequence<T, 1>`.
241    ///
242    /// # Errors
243    /// Buffer overflow.
244    pub fn encode_into(&self, w: &mut BufferWriter) -> Result<(), EncodeError> {
245        // `@optional` member (XTypes §7.3.4.5.4): a 1-byte XCDR2 presence
246        // boolean (§7.4.3.4.4), NOT a sequence count — byte-verified against
247        // CycloneDDS + FastDDS. Absent (no `@verbatim`) ⇒ a single `0`.
248        match &self.verbatim {
249            None => w.write_u8(0),
250            Some(v) => {
251                w.write_u8(1)?;
252                v.encode_into(w)
253            }
254        }
255    }
256
257    /// Decode.
258    ///
259    /// # Errors
260    /// Buffer underflow.
261    pub fn decode_from(r: &mut BufferReader<'_>) -> Result<Self, DecodeError> {
262        let present = r.read_u8()?;
263        let verbatim = if present == 0 {
264            None
265        } else {
266            Some(AppliedVerbatimAnnotation::decode_from(r)?)
267        };
268        Ok(Self { verbatim })
269    }
270}
271
272/// AppliedAnnotationParameter (§7.3.4.5.4): a named parameter
273/// of an annotation instance. The parameter name is stored as a 4-byte hash
274/// (saves payload).
275#[derive(Debug, Clone, PartialEq, Eq)]
276pub struct AppliedAnnotationParameter {
277    /// Hash of the parameter name.
278    pub paramname_hash: NameHash,
279    /// Parameter value as opaque bytes (discriminator-driven).
280    pub value: Vec<u8>,
281}
282
283impl AppliedAnnotationParameter {
284    fn encode_into(&self, w: &mut BufferWriter) -> Result<(), EncodeError> {
285        self.paramname_hash.encode_into(w)?;
286        let len = u32::try_from(self.value.len()).map_err(|_| EncodeError::ValueOutOfRange {
287            message: "annotation parameter value exceeds u32::MAX bytes",
288        })?;
289        w.write_u32(len)?;
290        w.write_bytes(&self.value)
291    }
292
293    fn decode_from(r: &mut BufferReader<'_>) -> Result<Self, DecodeError> {
294        let paramname_hash = NameHash::decode_from(r)?;
295        let len = r.read_u32()? as usize;
296        let value = r.read_bytes(len)?.to_vec();
297        Ok(Self {
298            paramname_hash,
299            value,
300        })
301    }
302}
303
304/// AppliedAnnotation: instance of a custom annotation on a type/member.
305#[derive(Debug, Clone, PartialEq, Eq)]
306pub struct AppliedAnnotation {
307    /// Type of the annotation (TypeIdentifier to the annotation definition).
308    pub annotation_typeid: TypeIdentifier,
309    /// Set parameters.
310    pub param_seq: Vec<AppliedAnnotationParameter>,
311}
312
313impl AppliedAnnotation {
314    /// Encode.
315    ///
316    /// # Errors
317    /// Buffer overflow.
318    pub fn encode_into(&self, w: &mut BufferWriter) -> Result<(), EncodeError> {
319        self.annotation_typeid.encode_into(w)?;
320        encode_seq(w, &self.param_seq, |w, p| p.encode_into(w))
321    }
322
323    /// Decode.
324    ///
325    /// # Errors
326    /// Buffer-Underflow.
327    pub fn decode_from(r: &mut BufferReader<'_>) -> Result<Self, DecodeError> {
328        let annotation_typeid = TypeIdentifier::decode_from(r)?;
329        let param_seq = decode_seq(r, AppliedAnnotationParameter::decode_from)?;
330        Ok(Self {
331            annotation_typeid,
332            param_seq,
333        })
334    }
335}
336
337/// Optionales `sequence<AppliedAnnotation>` — wire: `sequence<T, 1>`.
338#[derive(Debug, Clone, Default, PartialEq, Eq)]
339pub struct OptionalAppliedAnnotationSeq(pub Option<Vec<AppliedAnnotation>>);
340
341impl OptionalAppliedAnnotationSeq {
342    /// Encode.
343    ///
344    /// # Errors
345    /// Buffer overflow.
346    pub fn encode_into(&self, w: &mut BufferWriter) -> Result<(), EncodeError> {
347        // `@optional AppliedAnnotationSeq` (§7.3.4.5.4): a 1-byte XCDR2 presence
348        // boolean (§7.4.3.4.4); when present, the AppliedAnnotationSeq follows
349        // with its own 4-byte length. Absent ⇒ a single `0` (byte-verified
350        // against CycloneDDS + FastDDS).
351        match &self.0 {
352            None => w.write_u8(0),
353            Some(seq) => {
354                w.write_u8(1)?;
355                encode_seq(w, seq, |w, a| a.encode_into(w))
356            }
357        }
358    }
359
360    /// Decode.
361    ///
362    /// # Errors
363    /// Buffer-Underflow.
364    pub fn decode_from(r: &mut BufferReader<'_>) -> Result<Self, DecodeError> {
365        let present = r.read_u8()?;
366        if present == 0 {
367            return Ok(Self(None));
368        }
369        let seq = decode_seq(r, AppliedAnnotation::decode_from)?;
370        Ok(Self(Some(seq)))
371    }
372}
373
374/// CompleteTypeDetail (§7.3.4.5.4): ann_builtin + ann_custom + type_name.
375#[derive(Debug, Clone, PartialEq, Eq)]
376pub struct CompleteTypeDetail {
377    /// Builtin-Annotations (z.B. `@verbatim`).
378    pub ann_builtin: AppliedBuiltinTypeAnnotations,
379    /// Custom annotations (optional).
380    pub ann_custom: OptionalAppliedAnnotationSeq,
381    /// Fully qualified type name (e.g. "::sensors::Chatter").
382    pub type_name: QualifiedTypeName,
383}
384
385impl CompleteTypeDetail {
386    /// Encode.
387    ///
388    /// # Errors
389    /// Buffer overflow.
390    pub fn encode_into(&self, w: &mut BufferWriter) -> Result<(), EncodeError> {
391        self.ann_builtin.encode_into(w)?;
392        self.ann_custom.encode_into(w)?;
393        w.write_string(&self.type_name)
394    }
395
396    /// Decode.
397    ///
398    /// # Errors
399    /// Buffer-Underflow.
400    pub fn decode_from(r: &mut BufferReader<'_>) -> Result<Self, DecodeError> {
401        let ann_builtin = AppliedBuiltinTypeAnnotations::decode_from(r)?;
402        let ann_custom = OptionalAppliedAnnotationSeq::decode_from(r)?;
403        let type_name = r.read_string()?;
404        Ok(Self {
405            ann_builtin,
406            ann_custom,
407            type_name,
408        })
409    }
410}
411
412/// AppliedBuiltinMemberAnnotations (§7.3.4.5.4) — Member-spezifische
413/// Builtin-Annotations.
414#[derive(Debug, Clone, Default, PartialEq, Eq)]
415pub struct AppliedBuiltinMemberAnnotations {
416    /// `@unit("...")`.
417    pub unit: Option<String>,
418    /// `@min(val)` as opaque bytes (discriminator-led).
419    pub min: Option<Vec<u8>>,
420    /// `@max(val)`.
421    pub max: Option<Vec<u8>>,
422    /// `@hashid("...")`.
423    pub hash_id: Option<String>,
424    /// `@default(val)` (XTypes 1.3 §7.2.4.4.4.4.9). The value is stored as a
425    /// string (the caller converts to the member type); the wire form
426    /// is at the end of the AppliedBuiltinMemberAnnotations record, so that
427    /// decoders without `default_value` knowledge end correctly at the
428    /// second-to-last string field (`hash_id`) — new decoders read the
429    /// trailer; old decoders leave it.
430    pub default_value: Option<String>,
431}
432
433impl AppliedBuiltinMemberAnnotations {
434    fn write_opt_string(w: &mut BufferWriter, s: &Option<String>) -> Result<(), EncodeError> {
435        // `@optional` scalar (§7.3.4.8): a 1-byte XCDR2 presence boolean
436        // (§7.4.3.4.4), not a sequence count — byte-verified vs Cyclone/FastDDS.
437        match s {
438            None => w.write_u8(0),
439            Some(v) => {
440                w.write_u8(1)?;
441                w.write_string(v)
442            }
443        }
444    }
445
446    fn read_opt_string(r: &mut BufferReader<'_>) -> Result<Option<String>, DecodeError> {
447        let present = r.read_u8()?;
448        if present == 0 {
449            return Ok(None);
450        }
451        let out = r.read_string()?;
452        Ok(Some(out))
453    }
454
455    fn write_opt_bytes(w: &mut BufferWriter, b: &Option<Vec<u8>>) -> Result<(), EncodeError> {
456        // `@optional` (§7.3.4.8): 1-byte presence boolean; when present the
457        // value's own 4-byte length follows.
458        match b {
459            None => w.write_u8(0),
460            Some(v) => {
461                w.write_u8(1)?;
462                let len = u32::try_from(v.len()).map_err(|_| EncodeError::ValueOutOfRange {
463                    message: "annotation value exceeds u32::MAX",
464                })?;
465                w.write_u32(len)?;
466                w.write_bytes(v)
467            }
468        }
469    }
470
471    fn read_opt_bytes(r: &mut BufferReader<'_>) -> Result<Option<Vec<u8>>, DecodeError> {
472        let present = r.read_u8()?;
473        if present == 0 {
474            return Ok(None);
475        }
476        let inner_len = r.read_u32()? as usize;
477        let out = r.read_bytes(inner_len)?.to_vec();
478        Ok(Some(out))
479    }
480
481    /// `true` if no builtin member annotation is set — the whole record is
482    /// then absent (the `@optional` wrapper writes a single `0` byte).
483    #[must_use]
484    pub fn is_empty(&self) -> bool {
485        self.unit.is_none()
486            && self.min.is_none()
487            && self.max.is_none()
488            && self.hash_id.is_none()
489            && self.default_value.is_none()
490    }
491
492    /// Encode.
493    ///
494    /// # Errors
495    /// Buffer overflow.
496    pub fn encode_into(&self, w: &mut BufferWriter) -> Result<(), EncodeError> {
497        Self::write_opt_string(w, &self.unit)?;
498        Self::write_opt_bytes(w, &self.min)?;
499        Self::write_opt_bytes(w, &self.max)?;
500        Self::write_opt_string(w, &self.hash_id)?;
501        Self::write_opt_string(w, &self.default_value)
502    }
503
504    /// Decode.
505    ///
506    /// # Errors
507    /// Buffer-Underflow.
508    pub fn decode_from(r: &mut BufferReader<'_>) -> Result<Self, DecodeError> {
509        let unit = Self::read_opt_string(r)?;
510        let min = Self::read_opt_bytes(r)?;
511        let max = Self::read_opt_bytes(r)?;
512        let hash_id = Self::read_opt_string(r)?;
513        // `default_value` is a new trailer (§7.2.4.4.4.4.9). If the
514        // reader buffer is empty, it is a legacy encoder without the
515        // trailer — return None, no error.
516        let default_value = if r.remaining() >= 4 {
517            Self::read_opt_string(r).ok().flatten()
518        } else {
519            None
520        };
521        Ok(Self {
522            unit,
523            min,
524            max,
525            hash_id,
526            default_value,
527        })
528    }
529}
530
531/// CompleteMemberDetail: `name` + `ann_builtin` + `ann_custom`.
532#[derive(Debug, Clone, PartialEq, Eq)]
533pub struct CompleteMemberDetail {
534    /// Voller Member-Name.
535    pub name: String,
536    /// Builtin-Member-Annotations.
537    pub ann_builtin: AppliedBuiltinMemberAnnotations,
538    /// Custom-Annotations.
539    pub ann_custom: OptionalAppliedAnnotationSeq,
540}
541
542impl CompleteMemberDetail {
543    /// Encode.
544    ///
545    /// # Errors
546    /// Buffer overflow.
547    pub fn encode_into(&self, w: &mut BufferWriter) -> Result<(), EncodeError> {
548        w.write_string(&self.name)?;
549        // ann_builtin is `@optional` (§7.3.4.5): a 1-byte presence boolean,
550        // absent when no builtin member annotation is set (byte-verified vs
551        // Cyclone/FastDDS — the whole record collapses to a single `0`).
552        if self.ann_builtin.is_empty() {
553            w.write_u8(0)?;
554        } else {
555            w.write_u8(1)?;
556            self.ann_builtin.encode_into(w)?;
557        }
558        self.ann_custom.encode_into(w)
559    }
560
561    /// Decode.
562    ///
563    /// # Errors
564    /// Buffer-Underflow.
565    pub fn decode_from(r: &mut BufferReader<'_>) -> Result<Self, DecodeError> {
566        let name = r.read_string()?;
567        let present = r.read_u8()?;
568        let ann_builtin = if present == 0 {
569            AppliedBuiltinMemberAnnotations::default()
570        } else {
571            AppliedBuiltinMemberAnnotations::decode_from(r)?
572        };
573        let ann_custom = OptionalAppliedAnnotationSeq::decode_from(r)?;
574        Ok(Self {
575            name,
576            ann_builtin,
577            ann_custom,
578        })
579    }
580}
581
582/// Hilfsroutine: sequence<T> encode via Callback.
583pub(crate) fn encode_seq<T, F>(
584    w: &mut BufferWriter,
585    items: &[T],
586    mut f: F,
587) -> Result<(), EncodeError>
588where
589    F: FnMut(&mut BufferWriter, &T) -> Result<(), EncodeError>,
590{
591    let len = u32::try_from(items.len()).map_err(|_| EncodeError::ValueOutOfRange {
592        message: "sequence length exceeds u32::MAX",
593    })?;
594    w.write_u32(len)?;
595    for it in items {
596        f(w, it)?;
597    }
598    Ok(())
599}
600
601/// DoS cap for Vec pre-allocation during decode. The value is the
602/// upper bound in elements that we allocate initially. Large sequences
603/// are grown incrementally via `push()`.
604///
605/// 16 MiB / min_elem_size is a generous heuristic — no one
606/// legitimately sends 16M TypeIdentifiers in a getTypes reply, but
607/// Vec::with_capacity(u32_wire as usize) would reserve ~16 GB at u32::MAX
608/// = an OOM vector.
609pub const DECODE_PREALLOC_CAP: usize = 4096;
610
611/// Safe allocation: `Vec::with_capacity(len.min(remaining_bytes /
612/// min_elem_size).min(CAP))`. Prevents "30-byte PID forces 4 GB RAM".
613#[must_use]
614pub(crate) fn safe_capacity(len: usize, min_elem_size: usize, remaining_bytes: usize) -> usize {
615    let by_bytes = if min_elem_size == 0 {
616        DECODE_PREALLOC_CAP
617    } else {
618        remaining_bytes.saturating_div(min_elem_size)
619    };
620    len.min(by_bytes).min(DECODE_PREALLOC_CAP)
621}
622
623/// Helper routine: sequence<T> decode via a callback.
624///
625/// Uses [`safe_capacity`] for DoS protection: even if the
626/// wire length is `u32::MAX`, we allocate at most
627/// `DECODE_PREALLOC_CAP` entries initially. The actual loop still builds
628/// up to `len`, but aborts at the latest when `read_*` reads beyond the
629/// available buffer.
630pub(crate) fn decode_seq<T, F>(
631    r: &mut BufferReader<'_>,
632    mut f: F,
633) -> Result<alloc::vec::Vec<T>, DecodeError>
634where
635    F: FnMut(&mut BufferReader<'_>) -> Result<T, DecodeError>,
636{
637    let len = r.read_u32()? as usize;
638    let cap = safe_capacity(len, 1, r.remaining());
639    let mut out = alloc::vec::Vec::with_capacity(cap);
640    for _ in 0..len {
641        out.push(f(r)?);
642    }
643    Ok(out)
644}
645
646/// Like [`encode_seq`], but with XCDR2 `@appendable` framing: the whole
647/// sequence (length + elements) is wrapped in a 4-byte DHEADER. XTypes 1.3
648/// §7.3.4.5 — the TypeObject member/literal sequences are `@appendable`, so a
649/// peer (and the vendors) prefix the member-seq with its byte length. Each
650/// element additionally carries its own DHEADER via its `encode_into`.
651pub(crate) fn encode_seq_appendable<T, F>(
652    w: &mut BufferWriter,
653    items: &[T],
654    f: F,
655) -> Result<(), EncodeError>
656where
657    F: FnMut(&mut BufferWriter, &T) -> Result<(), EncodeError>,
658{
659    zerodds_cdr::struct_enc::encode_appendable(w, move |w| encode_seq(w, items, f))
660}
661
662/// Decode counterpart to [`encode_seq_appendable`]: strips the member-seq
663/// DHEADER, then decodes `length + elements`.
664pub(crate) fn decode_seq_appendable<T, F>(
665    r: &mut BufferReader<'_>,
666    f: F,
667) -> Result<alloc::vec::Vec<T>, DecodeError>
668where
669    F: FnMut(&mut BufferReader<'_>) -> Result<T, DecodeError>,
670{
671    zerodds_cdr::struct_enc::decode_appendable(r, move |r| decode_seq(r, f))
672}
673
674#[cfg(test)]
675#[allow(clippy::unwrap_used)]
676mod safe_capacity_tests {
677    use super::*;
678
679    #[test]
680    fn safe_capacity_clamps_by_remaining_bytes() {
681        assert_eq!(safe_capacity(1_000_000_000, 4, 100), 25);
682    }
683
684    #[test]
685    fn safe_capacity_caps_at_prealloc_cap() {
686        let cap = safe_capacity(usize::MAX, 1, usize::MAX);
687        assert_eq!(cap, DECODE_PREALLOC_CAP);
688    }
689
690    #[test]
691    fn safe_capacity_returns_len_when_small() {
692        assert_eq!(safe_capacity(10, 4, 1000), 10);
693    }
694
695    #[test]
696    fn safe_capacity_handles_zero_elem_size() {
697        assert_eq!(safe_capacity(usize::MAX, 0, 100), DECODE_PREALLOC_CAP);
698    }
699
700    #[test]
701    fn decode_seq_truncates_preallocation_for_large_lengths() {
702        let mut bytes = alloc::vec::Vec::new();
703        bytes.extend_from_slice(&u32::MAX.to_le_bytes());
704        let mut r = BufferReader::new(&bytes, zerodds_cdr::Endianness::Little);
705        let res: Result<alloc::vec::Vec<u8>, _> = decode_seq(&mut r, |rr| rr.read_u8());
706        assert!(res.is_err());
707    }
708
709    fn roundtrip_verbatim(placement: VerbatimPlacement) {
710        let a = AppliedVerbatimAnnotation {
711            placement: placement.clone(),
712            language: alloc::string::String::from("c++"),
713            text: alloc::string::String::from("// example"),
714        };
715        let mut w = BufferWriter::new(zerodds_cdr::Endianness::Little);
716        a.encode_into(&mut w).unwrap();
717        let bytes = w.into_bytes();
718        let mut r = BufferReader::new(&bytes, zerodds_cdr::Endianness::Little);
719        let decoded = AppliedVerbatimAnnotation::decode_from(&mut r).unwrap();
720        assert_eq!(decoded, a);
721    }
722
723    #[test]
724    fn verbatim_placement_roundtrip_before() {
725        roundtrip_verbatim(VerbatimPlacement::Before);
726    }
727
728    #[test]
729    fn verbatim_placement_roundtrip_after() {
730        roundtrip_verbatim(VerbatimPlacement::After);
731    }
732
733    #[test]
734    fn verbatim_placement_roundtrip_begin_file() {
735        roundtrip_verbatim(VerbatimPlacement::BeginFile);
736    }
737
738    #[test]
739    fn verbatim_placement_roundtrip_end_file() {
740        roundtrip_verbatim(VerbatimPlacement::EndFile);
741    }
742
743    #[test]
744    fn verbatim_placement_roundtrip_other_forward_compat() {
745        roundtrip_verbatim(VerbatimPlacement::Other(alloc::string::String::from(
746            "CUSTOM_PLACEMENT",
747        )));
748    }
749}