Skip to main content

zerodds_wire/
error.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! What can go wrong on the wire.
4
5use alloc::string::String;
6
7/// Why a read failed, or a write refused.
8///
9/// Every variant names what was expected and where. A reader that
10/// returned a zero for a truncated stream would hand the caller a value
11/// that was never sent; a writer that narrowed a value it cannot carry
12/// would send one nobody wrote. `OutOfRange` is the variant both use —
13/// `at` is the offset read from on the way in and the offset the value
14/// would have gone to on the way out.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum WireError {
17    /// The stream ended before the value did.
18    Truncated {
19        /// Where the read started.
20        at: usize,
21        /// How many bytes it needed.
22        needed: usize,
23        /// How many were left.
24        available: usize,
25    },
26    /// A length field claims more than the stream holds.
27    ///
28    /// Separate from `Truncated`: a length that cannot be true is a
29    /// malformed stream, not a short one, and allocating for it is how a
30    /// reader turns a bad message into an out-of-memory.
31    ImpossibleLength {
32        /// Where the length was read.
33        at: usize,
34        /// What it claimed.
35        claimed: usize,
36        /// How many bytes remain.
37        available: usize,
38    },
39    /// A string held bytes that are not UTF-8.
40    NotUtf8 {
41        /// Where the string started.
42        at: usize,
43    },
44    /// A value outside what its type admits.
45    OutOfRange {
46        /// Where it was read.
47        at: usize,
48        /// What was expected.
49        expected: String,
50    },
51    /// A value the receiving type cannot hold, under a position whose
52    /// `try_construct` is `DISCARD`.
53    ///
54    /// DDS-XTypes 1.3 §7.2.2.7 and §7.2.4.1: "the default behavior is to
55    /// discard the O2 object to avoid misinterpretation." §7.2.3 says
56    /// the sample "can be safely ignored (filtered out) by the
57    /// receiver".
58    ///
59    /// Kept apart from every other variant on purpose, and the
60    /// distinction is measured. Cyclone's path is the clearest witness:
61    /// `dds_stream_normalize` fails inside `ddsi_serdata_from_ser`,
62    /// which returns `NULL`, and delivery ends at
63    /// `remote_on_delivery_failure_fastpath` — the sample never becomes
64    /// a sample. **A discarded sample is an absence, not an error.** The
65    /// stream is well formed and the next sample may well fit; every
66    /// other variant here says the *stream* is wrong. The reflective
67    /// column spells it `ProbeError::Discarded`, the C column
68    /// `ZDW_DISCARDED` and the Python column `Discarded`.
69    Discarded {
70        /// Where the value that does not fit began.
71        at: usize,
72        /// What the receiving type can hold.
73        expected: String,
74        /// What arrived.
75        found: String,
76    },
77    /// The stream is well formed and framed the way another
78    /// implementation frames it, not the way this reader is configured.
79    ///
80    /// Separate from [`Self::OutOfRange`] on purpose. A reader given the
81    /// wrong dialect and a reader given a corrupt stream both stop, and
82    /// only one of them is fixed by changing a flag. Telling a user
83    /// "expected a four-byte parameter length" when the answer is
84    /// `--dialect fastdds` costs an afternoon; naming the form the bytes
85    /// do fit costs nothing.
86    WrongFraming {
87        /// Where the mismatch was found.
88        at: usize,
89        /// What the configured framing calls for.
90        expected: String,
91        /// What the stream has instead.
92        found: String,
93        /// The setting whose other value the stream fits, when one does
94        /// — `Some("--pl-cdr1-headers short-true-length")`. `None` when
95        /// no configuration would have produced these bytes, which
96        /// makes it a malformed stream after all.
97        fits: Option<&'static str>,
98    },
99}
100
101impl WireError {
102    /// A refusal raised from outside the runtime.
103    ///
104    /// The runtime owns the wire and the generated code owns the type,
105    /// and some refusals are the type's: a member the stream says is
106    /// absent where the generated field cannot hold an absence, a value
107    /// that names no enumerator. `runtime/c` already has
108    /// `zdw_reader_refuse` for exactly this; the Rust column had no way
109    /// to say it and read on instead, which puts every following member
110    /// at the wrong offset and reports success.
111    #[must_use]
112    pub fn refused(at: usize, expected: &str) -> Self {
113        Self::OutOfRange {
114            at,
115            expected: String::from(expected),
116        }
117    }
118
119    /// Whether this is the absence [`WireError::Discarded`] describes
120    /// rather than a statement that the stream is malformed.
121    ///
122    /// The one question a subscriber has to be able to ask: a discarded
123    /// sample is dropped and the next one read, a malformed stream is
124    /// reported. §7.2.4.1 against §7.2.3.
125    #[must_use]
126    pub const fn is_discarded(&self) -> bool {
127        matches!(self, Self::Discarded { .. })
128    }
129}
130
131impl core::fmt::Display for WireError {
132    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
133        match self {
134            Self::Truncated {
135                at,
136                needed,
137                available,
138            } => write!(f, "at {at}: {needed} bytes needed, {available} available"),
139            Self::ImpossibleLength {
140                at,
141                claimed,
142                available,
143            } => write!(
144                f,
145                "at {at}: a length of {claimed} with {available} bytes left"
146            ),
147            Self::NotUtf8 { at } => write!(f, "at {at}: the string is not UTF-8"),
148            Self::Discarded {
149                at,
150                expected,
151                found,
152            } => write!(
153                f,
154                "at {at}: the sample is discarded — this type holds {expected} and the stream \
155                 carried {found}"
156            ),
157            Self::OutOfRange { at, expected } => write!(f, "at {at}: expected {expected}"),
158            Self::WrongFraming {
159                at,
160                expected,
161                found,
162                fits,
163            } => match fits {
164                Some(setting) => write!(
165                    f,
166                    "at {at}: this reader is configured for {expected} and the stream has \
167                     {found}, which is what {setting} writes"
168                ),
169                None => write!(
170                    f,
171                    "at {at}: this reader is configured for {expected} and the stream has \
172                     {found}, which no setting writes"
173                ),
174            },
175        }
176    }
177}
178
179impl core::error::Error for WireError {}