Skip to main content

zerodds_xrce/
object_repr.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! `ObjectVariant`-Representations (Spec §7.7).
5//!
6//! Jede Object-CREATE-Operation (Spec §8.4.6) traegt eine
7//! `ObjectVariant`, die Object-spezifische Daten haelt. Diese werden
8//! ueber drei Wire-Repraesentationen gefuehrt:
9//!
10//! - `REPRESENTATION_BY_REFERENCE`  — Identifier-String, der auf eine
11//!   im Agent registrierte Vorlage zeigt.
12//! - `REPRESENTATION_AS_XML_STRING` — Inline-XML, das den Object-State
13//!   beschreibt (z.B. Topic-XML, QoS-Profile-XML).
14//! - `REPRESENTATION_IN_BINARY`     — Inline-Binary (XCDR2-Encoding der
15//!   strong-typed-Variante, z.B. fuer `OBJK_TYPE`).
16//!
17//! Wire-Discriminator (1 Byte, Spec §7.7.2):
18//! - `0x01` = ByReference
19//! - `0x02` = ByXmlString
20//! - `0x03` = InBinary
21//!
22//! Strings sind XCDR2-`string<>` (4-Byte LE-Length + UTF-8 + null-terminator).
23//! Bytes sind XCDR2-`sequence<octet>` (4-Byte LE-Length + Bytes).
24//!
25//! Hier wird das Outer-Wrap und der Discriminator strukturell validiert;
26//! die Inner-XML-/XCDR-Validierung ist out-of-scope (uebernimmt der
27//! Agent-Process spaeter).
28
29extern crate alloc;
30use alloc::string::String;
31use alloc::vec::Vec;
32
33use crate::encoding::{Endianness, read_u32, write_u32};
34use crate::error::XrceError;
35
36/// Discriminator-Bytes (Spec §7.7.2).
37pub mod repr_disc {
38    /// Reserved-Default; nicht spec-konform fuer Wire-Decode.
39    pub const INVALID: u8 = 0x00;
40    /// `REPRESENTATION_BY_REFERENCE`.
41    pub const BY_REFERENCE: u8 = 0x01;
42    /// `REPRESENTATION_AS_XML_STRING`.
43    pub const AS_XML_STRING: u8 = 0x02;
44    /// `REPRESENTATION_IN_BINARY`.
45    pub const IN_BINARY: u8 = 0x03;
46}
47
48/// XCDR2-Encoding-Cap fuer Inline-Strings/Bytes — schuetzt vor
49/// `length=u32::MAX`-Decoder-Bombs. 64 KiB reicht fuer alle
50/// realistischen Topic-XML-Beschreibungen.
51pub const REPR_MAX_INLINE_BYTES: usize = 65_536;
52
53/// `ObjectVariant`-Wire-Repraesentation.
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub enum ObjectVariant {
56    /// Reference-by-Name-String.
57    ByReference(String),
58    /// Inline-XML-Repraesentation.
59    ByXmlString(String),
60    /// Inline-Binary (XCDR2-encoded strong-type).
61    InBinary(Vec<u8>),
62}
63
64impl ObjectVariant {
65    /// Discriminator-Byte.
66    #[must_use]
67    pub fn discriminator(&self) -> u8 {
68        match self {
69            Self::ByReference(_) => repr_disc::BY_REFERENCE,
70            Self::ByXmlString(_) => repr_disc::AS_XML_STRING,
71            Self::InBinary(_) => repr_disc::IN_BINARY,
72        }
73    }
74
75    /// Encodiert diese Variante in XCDR2-Form mit gegebener Endianness.
76    /// Layout:
77    ///
78    /// ```text
79    /// +----+----+----+----+----+ ... +----+
80    /// |disc| pad pad pad |  payload  |
81    /// +----+----+----+----+----+ ... +----+
82    /// ```
83    ///
84    /// `disc` (1 Byte) + 3 Padding-Bytes (XCDR2-Alignment auf 4); danach
85    /// die Length (u32) + Bytes der Variante.
86    ///
87    /// # Errors
88    /// `PayloadTooLarge`, wenn die Payload `> REPR_MAX_INLINE_BYTES`.
89    pub fn encode(&self, e: Endianness) -> Result<Vec<u8>, XrceError> {
90        let payload = match self {
91            Self::ByReference(s) | Self::ByXmlString(s) => s.as_bytes(),
92            Self::InBinary(b) => b.as_slice(),
93        };
94        if payload.len() > REPR_MAX_INLINE_BYTES {
95            return Err(XrceError::PayloadTooLarge {
96                limit: REPR_MAX_INLINE_BYTES,
97                actual: payload.len(),
98            });
99        }
100        // Strings haengen einen NUL-Terminator an (XCDR2 §7.4.4).
101        let extra_nul = matches!(self, Self::ByReference(_) | Self::ByXmlString(_));
102        let payload_len = if extra_nul {
103            payload.len() + 1
104        } else {
105            payload.len()
106        };
107        let len_u32 = u32::try_from(payload_len).map_err(|_| XrceError::ValueOutOfRange {
108            message: "object variant length exceeds u32",
109        })?;
110        let mut out = Vec::with_capacity(8 + payload_len);
111        out.push(self.discriminator());
112        out.extend_from_slice(&[0u8, 0, 0]); // 3 Byte Padding zu 4-Align
113        let mut len_buf = [0u8; 4];
114        write_u32(&mut len_buf, len_u32, e)?;
115        out.extend_from_slice(&len_buf);
116        out.extend_from_slice(payload);
117        if extra_nul {
118            out.push(0u8);
119        }
120        Ok(out)
121    }
122
123    /// Decodiert eine `ObjectVariant`. Liefert `(variant, bytes_consumed)`.
124    ///
125    /// # Errors
126    /// `UnexpectedEof`, `ValueOutOfRange`, `PayloadTooLarge`.
127    pub fn decode(bytes: &[u8], e: Endianness) -> Result<(Self, usize), XrceError> {
128        if bytes.len() < 8 {
129            return Err(XrceError::UnexpectedEof {
130                needed: 8,
131                offset: bytes.len(),
132            });
133        }
134        let disc = bytes[0];
135        // bytes[1..4] sind Padding, ignoriert
136        let len = read_u32(&bytes[4..8], e)?;
137        let len_us = usize::try_from(len).map_err(|_| XrceError::ValueOutOfRange {
138            message: "object variant length exceeds usize",
139        })?;
140        if len_us > REPR_MAX_INLINE_BYTES {
141            return Err(XrceError::PayloadTooLarge {
142                limit: REPR_MAX_INLINE_BYTES,
143                actual: len_us,
144            });
145        }
146        if bytes.len() < 8 + len_us {
147            return Err(XrceError::UnexpectedEof {
148                needed: 8 + len_us,
149                offset: bytes.len(),
150            });
151        }
152        let payload = &bytes[8..8 + len_us];
153        let consumed = 8 + len_us;
154        let variant = match disc {
155            repr_disc::BY_REFERENCE => {
156                let s = decode_xcdr_string(payload)?;
157                Self::ByReference(s)
158            }
159            repr_disc::AS_XML_STRING => {
160                let s = decode_xcdr_string(payload)?;
161                Self::ByXmlString(s)
162            }
163            repr_disc::IN_BINARY => Self::InBinary(payload.to_vec()),
164            _ => {
165                return Err(XrceError::ValueOutOfRange {
166                    message: "unknown ObjectVariant discriminator",
167                });
168            }
169        };
170        Ok((variant, consumed))
171    }
172}
173
174/// XCDR2 `string<>` haengt einen NUL-Terminator an. Beim Decode trimmen
175/// wir trailing-NULs und verlangen valides UTF-8.
176fn decode_xcdr_string(payload: &[u8]) -> Result<String, XrceError> {
177    let trimmed = if let Some((&last, rest)) = payload.split_last() {
178        if last == 0 { rest } else { payload }
179    } else {
180        payload
181    };
182    core::str::from_utf8(trimmed)
183        .map(alloc::string::ToString::to_string)
184        .map_err(|_| XrceError::ValueOutOfRange {
185            message: "object variant string is not valid utf-8",
186        })
187}
188
189#[cfg(test)]
190mod tests {
191    #![allow(clippy::expect_used, clippy::unwrap_used)]
192    use super::*;
193
194    #[test]
195    fn by_reference_roundtrip_le() {
196        let v = ObjectVariant::ByReference("MyTopicProfile".into());
197        let enc = v.encode(Endianness::Little).unwrap();
198        let (v2, n) = ObjectVariant::decode(&enc, Endianness::Little).unwrap();
199        assert_eq!(v, v2);
200        assert_eq!(n, enc.len());
201    }
202
203    #[test]
204    fn by_xml_string_roundtrip_be() {
205        let v = ObjectVariant::ByXmlString(
206            "<dds><topic name=\"Chat\" type=\"std::string\"/></dds>".into(),
207        );
208        let enc = v.encode(Endianness::Big).unwrap();
209        let (v2, _) = ObjectVariant::decode(&enc, Endianness::Big).unwrap();
210        assert_eq!(v, v2);
211    }
212
213    #[test]
214    fn in_binary_roundtrip() {
215        let v = ObjectVariant::InBinary(alloc::vec![0xCA, 0xFE, 0xBA, 0xBE, 0x00, 0x01, 0x02]);
216        let enc = v.encode(Endianness::Little).unwrap();
217        let (v2, _) = ObjectVariant::decode(&enc, Endianness::Little).unwrap();
218        assert_eq!(v, v2);
219    }
220
221    #[test]
222    fn unknown_discriminator_rejected() {
223        // Discriminator 0x42 ist nicht in der Spec
224        let mut bad = alloc::vec![0x42, 0, 0, 0];
225        bad.extend_from_slice(&0u32.to_le_bytes());
226        let res = ObjectVariant::decode(&bad, Endianness::Little);
227        assert!(matches!(res, Err(XrceError::ValueOutOfRange { .. })));
228    }
229
230    #[test]
231    fn truncated_header_returns_eof() {
232        let res = ObjectVariant::decode(&[0x01, 0, 0], Endianness::Little);
233        assert!(matches!(
234            res,
235            Err(XrceError::UnexpectedEof { needed: 8, .. })
236        ));
237    }
238
239    #[test]
240    fn truncated_payload_returns_eof() {
241        // disc=BY_REFERENCE, length=10 aber nur 3 Byte Payload
242        let mut bad = alloc::vec![0x01, 0, 0, 0];
243        bad.extend_from_slice(&10u32.to_le_bytes());
244        bad.extend_from_slice(&[0xAA, 0xBB, 0xCC]);
245        let res = ObjectVariant::decode(&bad, Endianness::Little);
246        assert!(matches!(res, Err(XrceError::UnexpectedEof { .. })));
247    }
248
249    #[test]
250    fn oversized_length_rejected_as_payload_too_large() {
251        let mut bad = alloc::vec![0x01, 0, 0, 0];
252        let huge = (REPR_MAX_INLINE_BYTES as u32) + 1;
253        bad.extend_from_slice(&huge.to_le_bytes());
254        let res = ObjectVariant::decode(&bad, Endianness::Little);
255        assert!(matches!(res, Err(XrceError::PayloadTooLarge { .. })));
256    }
257
258    #[test]
259    fn invalid_utf8_rejected() {
260        // Encoded as ByReference but with invalid UTF-8 bytes
261        let mut bad = alloc::vec![repr_disc::BY_REFERENCE, 0, 0, 0];
262        bad.extend_from_slice(&3u32.to_le_bytes());
263        bad.extend_from_slice(&[0xFF, 0xFE, 0xFD]);
264        let res = ObjectVariant::decode(&bad, Endianness::Little);
265        assert!(matches!(res, Err(XrceError::ValueOutOfRange { .. })));
266    }
267
268    #[test]
269    fn discriminator_returns_correct_byte() {
270        assert_eq!(
271            ObjectVariant::ByReference(String::new()).discriminator(),
272            repr_disc::BY_REFERENCE
273        );
274        assert_eq!(
275            ObjectVariant::ByXmlString(String::new()).discriminator(),
276            repr_disc::AS_XML_STRING
277        );
278        assert_eq!(
279            ObjectVariant::InBinary(Vec::new()).discriminator(),
280            repr_disc::IN_BINARY
281        );
282    }
283
284    /// Spec §7.7.3 verlangt eine Discriminated-Union mit 12 Object-
285    /// Kind-Varianten. ZeroDDS realisiert das via 2-Tier:
286    ///
287    /// 1. Outer-Wire: 3-Discriminator-`ObjectVariant`-Form
288    ///    (ByReference / ByXmlString / InBinary).
289    /// 2. Inner: pro Kind XCDR2-encoded Strong-Type (Topic/Type/QoS/
290    ///    Participant/Pub/Sub/DW/DR/Application/Agent/Client).
291    ///
292    /// Dieser Test zeigt, dass die Outer-Form fuer alle 12 Spec-OBJK-
293    /// Werte transparent funktioniert — das Inner-XCDR2 wird vom
294    /// Caller (xml_config / agent) generiert und ist out-of-scope
295    /// fuer den Wire-Codec.
296    #[test]
297    fn object_variant_carries_all_12_objk_kinds_through_outer_repr() {
298        use crate::object_kind::{
299            OBJK_AGENT, OBJK_APPLICATION, OBJK_CLIENT, OBJK_DATAREADER, OBJK_DATAWRITER,
300            OBJK_PARTICIPANT, OBJK_PUBLISHER, OBJK_QOSPROFILE, OBJK_SUBSCRIBER, OBJK_TOPIC,
301            OBJK_TYPE,
302        };
303        let kinds: &[u8] = &[
304            OBJK_PARTICIPANT,
305            OBJK_TOPIC,
306            OBJK_PUBLISHER,
307            OBJK_SUBSCRIBER,
308            OBJK_DATAWRITER,
309            OBJK_DATAREADER,
310            OBJK_TYPE,
311            OBJK_QOSPROFILE,
312            OBJK_APPLICATION,
313            OBJK_AGENT,
314            OBJK_CLIENT,
315        ];
316        for kind in kinds {
317            // Inner-Repr ist per Konvention 1 byte mit dem OBJK-Wert
318            // als Marker — das demonstriert die 2-Tier-Architektur.
319            let inner = alloc::vec![*kind];
320            let v = ObjectVariant::InBinary(inner.clone());
321            let bytes = v.encode(Endianness::Little).expect("encode");
322            let (v2, _) = ObjectVariant::decode(&bytes, Endianness::Little).expect("decode");
323            assert_eq!(v2, ObjectVariant::InBinary(inner));
324        }
325    }
326
327    #[test]
328    fn object_variant_xml_form_supports_topic_qosprofile_application() {
329        // Spec-Beispiel: Topic-XML-Repr.
330        for s in [
331            "<topic name=\"T\"/>",
332            "<qos_profile name=\"P\"/>",
333            "<application name=\"A\"/>",
334        ] {
335            let v = ObjectVariant::ByXmlString(s.into());
336            let bytes = v.encode(Endianness::Little).expect("encode");
337            let (v2, _) = ObjectVariant::decode(&bytes, Endianness::Little).expect("decode");
338            assert_eq!(v2, ObjectVariant::ByXmlString(s.into()));
339        }
340    }
341}