Skip to main content

zerodds_types/
qos.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! XTypes-bezogene QoS-Policies (T17, T18).
4//!
5//! - [`TypeConsistencyEnforcement`] (§7.6.3.7) — Strictness-Level fuer
6//!   Assignability-Checks.
7//! - [`DataRepresentation`] (§7.6.3.2.1) — XCDR1/XCDR2-Negotiation.
8
9use alloc::vec::Vec;
10
11/// Kind der TypeConsistencyEnforcement (§7.6.3.7).
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum TypeConsistencyKind {
14    /// Kein Type-Check beim Matching.
15    DisallowTypeCoercion,
16    /// Type-Coercion (int16→int32 etc.) erlaubt.
17    AllowTypeCoercion,
18    /// Volle Type-Validation + Force alle Checks.
19    ForceTypeValidation,
20}
21
22impl TypeConsistencyKind {
23    /// Wire-Encoded als u32 (§7.6.3.7).
24    #[must_use]
25    pub const fn to_u32(self) -> u32 {
26        match self {
27            Self::DisallowTypeCoercion => 0,
28            Self::AllowTypeCoercion => 1,
29            Self::ForceTypeValidation => 2,
30        }
31    }
32
33    /// Decoder.
34    #[must_use]
35    pub const fn from_u32(v: u32) -> Self {
36        match v {
37            1 => Self::AllowTypeCoercion,
38            2 => Self::ForceTypeValidation,
39            _ => Self::DisallowTypeCoercion,
40        }
41    }
42}
43
44/// TypeConsistencyEnforcement QoS-Policy (§7.6.3.7).
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub struct TypeConsistencyEnforcement {
47    /// Kind.
48    pub kind: TypeConsistencyKind,
49    /// IgnoreSequenceBounds — erlaubt unterschiedliche Bound-Werte.
50    pub ignore_sequence_bounds: bool,
51    /// IgnoreStringBounds.
52    pub ignore_string_bounds: bool,
53    /// IgnoreMemberNames (Match per @id, nicht per Name).
54    pub ignore_member_names: bool,
55    /// PreventTypeWidening (Narrowing-Ergebnisse blocken).
56    pub prevent_type_widening: bool,
57    /// ForceTypeValidation.
58    pub force_type_validation: bool,
59}
60
61impl Default for TypeConsistencyEnforcement {
62    fn default() -> Self {
63        Self {
64            // Default laut Spec: AllowTypeCoercion (§7.6.3.7.1).
65            kind: TypeConsistencyKind::AllowTypeCoercion,
66            ignore_sequence_bounds: true,
67            ignore_string_bounds: true,
68            ignore_member_names: false,
69            prevent_type_widening: false,
70            force_type_validation: false,
71        }
72    }
73}
74
75impl TypeConsistencyEnforcement {
76    /// Encode laut XTypes §7.6.3.7.3:
77    ///
78    /// ```text
79    /// struct TypeConsistencyEnforcementQosPolicy {
80    ///     TypeConsistencyKind kind;        // u32
81    ///     boolean ignore_sequence_bounds;  // 1 byte
82    ///     boolean ignore_string_bounds;    // 1 byte
83    ///     boolean ignore_member_names;     // 1 byte
84    ///     boolean prevent_type_widening;   // 1 byte
85    ///     boolean force_type_validation;   // 1 byte
86    /// };
87    /// ```
88    ///
89    /// Wire-Groesse = 4 + 5 = 9 byte, mit 3 byte Tail-Padding auf 12
90    /// byte Alignment (PID-Value ist 4-byte-aligned).
91    #[must_use]
92    pub fn to_bytes_le(self) -> Vec<u8> {
93        let mut out = Vec::with_capacity(12);
94        out.extend_from_slice(&self.kind.to_u32().to_le_bytes());
95        out.push(u8::from(self.ignore_sequence_bounds));
96        out.push(u8::from(self.ignore_string_bounds));
97        out.push(u8::from(self.ignore_member_names));
98        out.push(u8::from(self.prevent_type_widening));
99        out.push(u8::from(self.force_type_validation));
100        while out.len() % 4 != 0 {
101            out.push(0); // Padding auf 4-byte-Boundary.
102        }
103        out
104    }
105
106    /// Decode. Kuerzere Inputs nehmen Default fuer fehlende Flags
107    /// (forward-compat: aeltere Peers senden evtl. nur kind + 3
108    /// booleans, analog zu Cyclone DDS ≤ 0.10).
109    #[must_use]
110    pub fn from_bytes_le(bytes: &[u8]) -> Self {
111        if bytes.len() < 4 {
112            return Self::default();
113        }
114        // Infallible: len >= 4 oben geprueft.
115        let mut k = [0u8; 4];
116        k.copy_from_slice(&bytes[..4]);
117        let kind = TypeConsistencyKind::from_u32(u32::from_le_bytes(k));
118        Self {
119            kind,
120            ignore_sequence_bounds: bytes.get(4).copied().unwrap_or(1) != 0,
121            ignore_string_bounds: bytes.get(5).copied().unwrap_or(1) != 0,
122            ignore_member_names: bytes.get(6).copied().unwrap_or(0) != 0,
123            prevent_type_widening: bytes.get(7).copied().unwrap_or(0) != 0,
124            force_type_validation: bytes.get(8).copied().unwrap_or(0) != 0,
125        }
126    }
127}
128
129/// Data-Representation-ID (§7.6.3.2.1).
130#[repr(i16)]
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub enum DataRepresentationId {
133    /// XCDR version 1 (classic).
134    Xcdr1 = 0,
135    /// XML representation.
136    Xml = 1,
137    /// XCDR version 2 (xtypes, default).
138    Xcdr2 = 2,
139}
140
141impl DataRepresentationId {
142    /// Versuch einen i16 zu einer bekannten Representation zu mappen.
143    #[must_use]
144    pub const fn from_i16(v: i16) -> Option<Self> {
145        match v {
146            0 => Some(Self::Xcdr1),
147            1 => Some(Self::Xml),
148            2 => Some(Self::Xcdr2),
149            _ => None,
150        }
151    }
152}
153
154/// Ergebnis einer Data-Representation-Negotiation.
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub enum RepresentationNegotiation {
157    /// Gemeinsamer Wert gefunden.
158    Accepted(DataRepresentationId),
159    /// Keine Ueberschneidung.
160    NoOverlap,
161}
162
163/// Wie DCPS §2.2.3 beschreibt: Writer offeriert Representations, Reader
164/// akzeptiert eine Liste. Match = erste Writer-Choice, die in Reader-
165/// Liste vorhanden ist.
166#[must_use]
167pub fn negotiate_representation(
168    writer_offered: &[i16],
169    reader_accepted: &[i16],
170) -> RepresentationNegotiation {
171    for w in writer_offered {
172        if reader_accepted.contains(w) {
173            if let Some(kind) = DataRepresentationId::from_i16(*w) {
174                return RepresentationNegotiation::Accepted(kind);
175            }
176        }
177    }
178    RepresentationNegotiation::NoOverlap
179}
180
181/// Wire-Constraint-Matrix (§7.6.3.1 Tab.59): pro DataRepresentation und
182/// Extensibility-Kategorie definiert die Spec, ob das Pairing zulaessig
183/// ist. XCDR1 stuetzt FINAL und MUTABLE (mit PL_CDR1); XCDR2 stuetzt
184/// FINAL, APPENDABLE und MUTABLE.
185///
186/// Liefert `Ok(())` wenn die Kombination in der Spec-Matrix steht;
187/// sonst `Err` mit deutscher Beschreibung.
188///
189/// # Errors
190/// `&'static str` mit der Verletzung.
191pub fn check_data_repr_extensibility(
192    repr: DataRepresentationId,
193    ext: ExtensibilityForRepr,
194) -> Result<(), &'static str> {
195    use DataRepresentationId::*;
196    use ExtensibilityForRepr::*;
197    match (repr, ext) {
198        // XCDR1 erlaubt FINAL + MUTABLE (via PL_CDR1) — APPENDABLE-NICHT
199        // direkt definiert (Spec faellt auf FINAL-Behandlung zurueck;
200        // Validator hier sieht das als Verletzung).
201        (Xcdr1, Final) | (Xcdr1, Mutable) => Ok(()),
202        (Xcdr1, Appendable) => Err(
203            "Tab.59 §7.6.3.1: XCDR1 unterstuetzt APPENDABLE nicht direkt — Encoder muss FINAL waehlen",
204        ),
205        // XCDR2 unterstuetzt alle drei Kategorien (FINAL, APPENDABLE, MUTABLE).
206        (Xcdr2, _) => Ok(()),
207        // XML hat eigene Repraesentation, keine direkte Extensibility-
208        // Constraint im selben Sinn.
209        (Xml, _) => Ok(()),
210    }
211}
212
213/// Extensibility-Kategorie fuer die DataRepresentation-Constraint-Matrix.
214#[derive(Debug, Clone, Copy, PartialEq, Eq)]
215pub enum ExtensibilityForRepr {
216    /// `@final`.
217    Final,
218    /// `@appendable`.
219    Appendable,
220    /// `@mutable`.
221    Mutable,
222}
223
224#[cfg(test)]
225#[allow(clippy::unwrap_used)]
226mod tests {
227    use super::*;
228
229    #[test]
230    fn consistency_roundtrip_default() {
231        let c = TypeConsistencyEnforcement::default();
232        let bytes = c.to_bytes_le();
233        let decoded = TypeConsistencyEnforcement::from_bytes_le(&bytes);
234        assert_eq!(decoded, c);
235    }
236
237    #[test]
238    fn consistency_flags_all_set() {
239        let c = TypeConsistencyEnforcement {
240            kind: TypeConsistencyKind::ForceTypeValidation,
241            ignore_sequence_bounds: false,
242            ignore_string_bounds: false,
243            ignore_member_names: true,
244            prevent_type_widening: true,
245            force_type_validation: true,
246        };
247        let bytes = c.to_bytes_le();
248        let decoded = TypeConsistencyEnforcement::from_bytes_le(&bytes);
249        assert_eq!(decoded, c);
250    }
251
252    #[test]
253    fn negotiate_xcdr2_preferred() {
254        let result = negotiate_representation(&[2, 0], &[0, 2]);
255        assert_eq!(
256            result,
257            RepresentationNegotiation::Accepted(DataRepresentationId::Xcdr2)
258        );
259    }
260
261    #[test]
262    fn negotiate_fallback_to_xcdr1() {
263        let result = negotiate_representation(&[0], &[0, 2]);
264        assert_eq!(
265            result,
266            RepresentationNegotiation::Accepted(DataRepresentationId::Xcdr1)
267        );
268    }
269
270    #[test]
271    fn negotiate_no_overlap() {
272        let result = negotiate_representation(&[2], &[0]);
273        assert_eq!(result, RepresentationNegotiation::NoOverlap);
274    }
275
276    // ---- §7.6.3.1 Tab.59 DataRepresentation/Extensibility-Matrix ----
277
278    #[test]
279    fn xcdr1_final_combination_is_allowed() {
280        assert!(
281            check_data_repr_extensibility(DataRepresentationId::Xcdr1, ExtensibilityForRepr::Final)
282                .is_ok()
283        );
284    }
285
286    #[test]
287    fn xcdr1_mutable_combination_is_allowed() {
288        // XCDR1 + Mutable via PL_CDR1.
289        assert!(
290            check_data_repr_extensibility(
291                DataRepresentationId::Xcdr1,
292                ExtensibilityForRepr::Mutable
293            )
294            .is_ok()
295        );
296    }
297
298    #[test]
299    fn xcdr1_appendable_is_disallowed() {
300        let res = check_data_repr_extensibility(
301            DataRepresentationId::Xcdr1,
302            ExtensibilityForRepr::Appendable,
303        );
304        assert!(res.is_err());
305    }
306
307    #[test]
308    fn xcdr2_supports_all_three_extensibilities() {
309        for ext in [
310            ExtensibilityForRepr::Final,
311            ExtensibilityForRepr::Appendable,
312            ExtensibilityForRepr::Mutable,
313        ] {
314            assert!(
315                check_data_repr_extensibility(DataRepresentationId::Xcdr2, ext).is_ok(),
316                "XCDR2 + {ext:?} muss zulaessig sein"
317            );
318        }
319    }
320}