Skip to main content

zerodds_types/dynamic/
try_construct.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! XTypes 1.3 §7.5.4.1.2 — TryConstruct-Apply (C4.7).
4//!
5//! Wenn beim Decoden oder Setter-Aufruf ein Wert nicht in den Ziel-
6//! Member passt (z.B. String laenger als der bound, Sequence ueber
7//! max-Length, Enum-Wert ausserhalb des Wertebereichs), entscheidet
8//! der `try_construct`-Strategy, was passiert:
9//!
10//! - `Discard` — Wert verwerfen, Member bleibt unset.
11//! - `UseDefault` — Wert ignorieren, `member.default_value` setzen.
12//! - `Trim` — auf den Bound truncieren (Strings + Sequences); fuer
13//!   andere Bound-Violations Fallback auf Discard.
14//!
15//! Diese Logik wird **nur dann** ausgewertet wenn ein Bound-Violation
16//! tatsaechlich vorliegt — un-bounded Setter (Member-Type ohne
17//! `bound`-Limit) bleiben unveraendert.
18
19use alloc::string::ToString;
20use alloc::vec::Vec;
21
22use super::data::DynamicValue;
23use super::descriptor::{TryConstructKind, TypeKind};
24use super::type_::DynamicTypeMember;
25
26/// Ergebnis einer TryConstruct-Auswertung.
27#[derive(Debug, Clone, PartialEq)]
28pub enum TryConstructOutcome {
29    /// Wert ist innerhalb der Bounds — `set` darf ihn unveraendert
30    /// schreiben.
31    Accept(DynamicValue),
32    /// Wert wird verworfen — Member bleibt unset.
33    Discard,
34    /// Wert wird durch den default_value ersetzt.
35    UseDefault(DynamicValue),
36    /// Wert wird auf den Bound trunciert.
37    Trim(DynamicValue),
38}
39
40/// Wendet die `try_construct`-Strategie auf einen Setter-Wert an.
41/// Wenn keine Bound-Violation vorliegt, liefert die Funktion
42/// `Accept(value)` unveraendert zurueck.
43#[must_use]
44pub fn apply_try_construct(member: &DynamicTypeMember, value: DynamicValue) -> TryConstructOutcome {
45    let descriptor = member.descriptor();
46    let bound_max = bound_max_length(member);
47    let target_kind = member.dynamic_type().kind();
48
49    let violation = detect_violation(&value, target_kind, bound_max);
50    if violation.is_none() {
51        return TryConstructOutcome::Accept(value);
52    }
53
54    match descriptor.try_construct {
55        TryConstructKind::Discard => TryConstructOutcome::Discard,
56        TryConstructKind::UseDefault => {
57            match parse_default(descriptor.default_value.as_deref(), target_kind) {
58                Some(default) => TryConstructOutcome::UseDefault(default),
59                None => TryConstructOutcome::Discard,
60            }
61        }
62        TryConstructKind::Trim => match trim_value(value, target_kind, bound_max) {
63            Some(trimmed) => TryConstructOutcome::Trim(trimmed),
64            None => TryConstructOutcome::Discard,
65        },
66    }
67}
68
69/// Liefert das `max_length`-Bound aus dem Member-Type, falls relevant.
70/// `0` als Wert (Spec §7.5.1.2.4: 0 = unbounded) wird als `None`
71/// behandelt — un-bounded Setter umgehen die Apply-Logik.
72fn bound_max_length(member: &DynamicTypeMember) -> Option<usize> {
73    let descriptor = member.dynamic_type().descriptor();
74    match descriptor.kind {
75        TypeKind::String8 | TypeKind::String16 | TypeKind::Sequence | TypeKind::Map => descriptor
76            .bound
77            .first()
78            .copied()
79            .filter(|&b| b != 0)
80            .map(|b| b as usize),
81        TypeKind::Array => {
82            // Array hat fixed dimensions — Bound ist Produkt aller dims.
83            if descriptor.bound.is_empty() {
84                None
85            } else {
86                Some(descriptor.bound.iter().product::<u32>() as usize)
87            }
88        }
89        _ => None,
90    }
91}
92
93#[derive(Debug, PartialEq)]
94enum Violation {
95    StringTooLong,
96    SequenceTooLong,
97    ArrayLengthMismatch,
98}
99
100fn detect_violation(
101    value: &DynamicValue,
102    target_kind: TypeKind,
103    bound_max: Option<usize>,
104) -> Option<Violation> {
105    let max = bound_max?;
106    match (value, target_kind) {
107        (DynamicValue::String(s), TypeKind::String8) if s.len() > max => {
108            Some(Violation::StringTooLong)
109        }
110        (DynamicValue::WString(s), TypeKind::String16) if s.len() > max => {
111            Some(Violation::StringTooLong)
112        }
113        (DynamicValue::Sequence(s), TypeKind::Sequence) if s.len() > max => {
114            Some(Violation::SequenceTooLong)
115        }
116        (DynamicValue::Sequence(s), TypeKind::Array) if s.len() != max => {
117            Some(Violation::ArrayLengthMismatch)
118        }
119        _ => None,
120    }
121}
122
123fn trim_value(
124    value: DynamicValue,
125    target_kind: TypeKind,
126    bound_max: Option<usize>,
127) -> Option<DynamicValue> {
128    let max = bound_max?;
129    match (value, target_kind) {
130        (DynamicValue::String(mut s), TypeKind::String8) => {
131            // String-Trim auf Byte-Grenze, aber niemals mitten in einem
132            // UTF-8-Codepoint. Wir koerzieren auf die naechstkleinere
133            // Char-Boundary.
134            if s.len() > max {
135                let mut cut = max;
136                while !s.is_char_boundary(cut) && cut > 0 {
137                    cut -= 1;
138                }
139                s.truncate(cut);
140            }
141            Some(DynamicValue::String(s))
142        }
143        (DynamicValue::WString(mut s), TypeKind::String16) => {
144            if s.len() > max {
145                s.truncate(max);
146            }
147            Some(DynamicValue::WString(s))
148        }
149        (DynamicValue::Sequence(mut s), TypeKind::Sequence) => {
150            if s.len() > max {
151                s.truncate(max);
152            }
153            Some(DynamicValue::Sequence(s))
154        }
155        // Array-Length-Mismatch: kein sinnvoller Trim, weil ein Array
156        // exakte Dimension hat. Fallback auf Discard.
157        _ => None,
158    }
159}
160
161fn parse_default(default_str: Option<&str>, kind: TypeKind) -> Option<DynamicValue> {
162    let s = default_str?;
163    match kind {
164        TypeKind::Boolean => match s {
165            "TRUE" | "true" | "1" => Some(DynamicValue::Bool(true)),
166            "FALSE" | "false" | "0" => Some(DynamicValue::Bool(false)),
167            _ => None,
168        },
169        TypeKind::Byte | TypeKind::UInt8 => s.parse::<u8>().ok().map(DynamicValue::UInt8),
170        TypeKind::Int8 => s.parse::<i8>().ok().map(DynamicValue::Int8),
171        TypeKind::Int16 => s.parse::<i16>().ok().map(DynamicValue::Int16),
172        TypeKind::UInt16 => s.parse::<u16>().ok().map(DynamicValue::UInt16),
173        TypeKind::Int32 | TypeKind::Enumeration => s.parse::<i32>().ok().map(DynamicValue::Int32),
174        TypeKind::UInt32 => s.parse::<u32>().ok().map(DynamicValue::UInt32),
175        TypeKind::Int64 => s.parse::<i64>().ok().map(DynamicValue::Int64),
176        TypeKind::UInt64 => s.parse::<u64>().ok().map(DynamicValue::UInt64),
177        TypeKind::Float32 => s.parse::<f32>().ok().map(DynamicValue::Float32),
178        TypeKind::Float64 => s.parse::<f64>().ok().map(DynamicValue::Float64),
179        TypeKind::String8 => Some(DynamicValue::String(s.to_string())),
180        TypeKind::String16 => Some(DynamicValue::WString(s.encode_utf16().collect::<Vec<_>>())),
181        _ => None,
182    }
183}
184
185#[cfg(test)]
186#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
187mod tests {
188    use super::*;
189    use crate::dynamic::builder::{DynamicTypeBuilder, DynamicTypeBuilderFactory};
190    use crate::dynamic::descriptor::{MemberDescriptor, TypeDescriptor};
191    use alloc::boxed::Box;
192
193    fn make_struct_with_bounded_string(
194        max_len: u32,
195        try_construct: TryConstructKind,
196        default_value: Option<&str>,
197    ) -> crate::dynamic::DynamicType {
198        let mut builder = DynamicTypeBuilder::new(TypeDescriptor::structure("TestStruct"));
199        let mut string_desc = TypeDescriptor::primitive(TypeKind::String8, "string");
200        string_desc.bound = alloc::vec![max_len];
201        let mut member = MemberDescriptor::new("name", 1, string_desc);
202        member.try_construct = try_construct;
203        member.default_value = default_value.map(|s| s.to_string());
204        builder.add_member(member).unwrap();
205        builder.build().unwrap()
206    }
207
208    fn make_struct_with_bounded_seq(
209        max_len: u32,
210        try_construct: TryConstructKind,
211    ) -> crate::dynamic::DynamicType {
212        let mut builder = DynamicTypeBuilder::new(TypeDescriptor::structure("TestSeq"));
213        let mut seq_desc = TypeDescriptor::primitive(TypeKind::Sequence, "sequence");
214        seq_desc.bound = alloc::vec![max_len];
215        seq_desc.element_type = Some(Box::new(TypeDescriptor::primitive(TypeKind::Int32, "long")));
216        let mut member = MemberDescriptor::new("ids", 1, seq_desc);
217        member.try_construct = try_construct;
218        builder.add_member(member).unwrap();
219        builder.build().unwrap()
220    }
221
222    #[test]
223    fn discard_drops_too_long_string() {
224        let ty = make_struct_with_bounded_string(5, TryConstructKind::Discard, None);
225        let member = ty.member_by_id(1).unwrap();
226        let outcome = apply_try_construct(member, DynamicValue::String("toolong".into()));
227        assert_eq!(outcome, TryConstructOutcome::Discard);
228    }
229
230    #[test]
231    fn use_default_replaces_too_long_string() {
232        let ty = make_struct_with_bounded_string(5, TryConstructKind::UseDefault, Some("hello"));
233        let member = ty.member_by_id(1).unwrap();
234        let outcome = apply_try_construct(member, DynamicValue::String("toolong".into()));
235        match outcome {
236            TryConstructOutcome::UseDefault(DynamicValue::String(s)) => assert_eq!(s, "hello"),
237            other => panic!("expected UseDefault(\"hello\"), got {other:?}"),
238        }
239    }
240
241    #[test]
242    fn use_default_falls_back_to_discard_when_no_default() {
243        let ty = make_struct_with_bounded_string(5, TryConstructKind::UseDefault, None);
244        let member = ty.member_by_id(1).unwrap();
245        let outcome = apply_try_construct(member, DynamicValue::String("toolong".into()));
246        assert_eq!(outcome, TryConstructOutcome::Discard);
247    }
248
249    #[test]
250    fn trim_truncates_string_to_bound() {
251        let ty = make_struct_with_bounded_string(5, TryConstructKind::Trim, None);
252        let member = ty.member_by_id(1).unwrap();
253        let outcome = apply_try_construct(member, DynamicValue::String("hello world".into()));
254        match outcome {
255            TryConstructOutcome::Trim(DynamicValue::String(s)) => assert_eq!(s, "hello"),
256            other => panic!("expected Trim(\"hello\"), got {other:?}"),
257        }
258    }
259
260    #[test]
261    fn trim_respects_utf8_codepoint_boundaries() {
262        // "héllo" mit é = 2 byte. Bound 3 würde mitten in é trim → muss
263        // auf 2 byte zurueckfallen ("h" + Anfang von é → boundary 1 → "h").
264        let ty = make_struct_with_bounded_string(3, TryConstructKind::Trim, None);
265        let member = ty.member_by_id(1).unwrap();
266        let outcome = apply_try_construct(member, DynamicValue::String("héllo".into()));
267        match outcome {
268            TryConstructOutcome::Trim(DynamicValue::String(s)) => {
269                assert!(s.is_char_boundary(s.len()));
270                assert!(s.len() <= 3);
271                // Mit "h" (1 byte) + é (2 byte) = 3 byte char-boundary.
272                assert_eq!(s, "hé");
273            }
274            other => panic!("expected Trim, got {other:?}"),
275        }
276    }
277
278    #[test]
279    fn accept_when_value_within_bound() {
280        let ty = make_struct_with_bounded_string(10, TryConstructKind::Discard, None);
281        let member = ty.member_by_id(1).unwrap();
282        let outcome = apply_try_construct(member, DynamicValue::String("ok".into()));
283        match outcome {
284            TryConstructOutcome::Accept(DynamicValue::String(s)) => assert_eq!(s, "ok"),
285            other => panic!("expected Accept, got {other:?}"),
286        }
287    }
288
289    #[test]
290    fn unbounded_string_always_accepts() {
291        let ty = make_struct_with_bounded_string(0, TryConstructKind::Discard, None);
292        let member = ty.member_by_id(1).unwrap();
293        // bound = 0 = unbounded → no violation, no apply.
294        let outcome =
295            apply_try_construct(member, DynamicValue::String("a-very-long-string".into()));
296        match outcome {
297            TryConstructOutcome::Accept(_) => {}
298            other => panic!("expected Accept (unbounded), got {other:?}"),
299        }
300    }
301
302    #[test]
303    fn discard_drops_too_long_sequence() {
304        let ty = make_struct_with_bounded_seq(3, TryConstructKind::Discard);
305        let member = ty.member_by_id(1).unwrap();
306        let elements: Vec<_> = (0..5)
307            .map(|i| {
308                let prim = DynamicTypeBuilderFactory::get_primitive_type(TypeKind::Int32).unwrap();
309                let mut d = crate::dynamic::DynamicData::new(prim.clone());
310                d.set_int32_value(0, i).ok();
311                d
312            })
313            .collect();
314        let outcome = apply_try_construct(member, DynamicValue::Sequence(elements));
315        assert_eq!(outcome, TryConstructOutcome::Discard);
316    }
317
318    #[test]
319    fn trim_truncates_sequence_to_bound() {
320        let ty = make_struct_with_bounded_seq(3, TryConstructKind::Trim);
321        let member = ty.member_by_id(1).unwrap();
322        let elements: Vec<_> = (0..5)
323            .map(|i| {
324                let prim = DynamicTypeBuilderFactory::get_primitive_type(TypeKind::Int32).unwrap();
325                let mut d = crate::dynamic::DynamicData::new(prim.clone());
326                d.set_int32_value(0, i).ok();
327                d
328            })
329            .collect();
330        let outcome = apply_try_construct(member, DynamicValue::Sequence(elements));
331        match outcome {
332            TryConstructOutcome::Trim(DynamicValue::Sequence(s)) => assert_eq!(s.len(), 3),
333            other => panic!("expected Trim(seq[3]), got {other:?}"),
334        }
335    }
336
337    #[test]
338    fn parse_default_int32_works() {
339        let v = parse_default(Some("42"), TypeKind::Int32);
340        assert_eq!(v, Some(DynamicValue::Int32(42)));
341    }
342
343    #[test]
344    fn parse_default_bool_accepts_canonical_forms() {
345        assert_eq!(
346            parse_default(Some("TRUE"), TypeKind::Boolean),
347            Some(DynamicValue::Bool(true))
348        );
349        assert_eq!(
350            parse_default(Some("false"), TypeKind::Boolean),
351            Some(DynamicValue::Bool(false))
352        );
353    }
354
355    #[test]
356    fn parse_default_invalid_returns_none() {
357        assert_eq!(parse_default(Some("not-a-number"), TypeKind::Int32), None);
358    }
359}