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 {}