Skip to main content

zerodds_dcps/
error.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! DCPS error types. Modeled on OMG DDS 1.4 §2.2.2.1 `ReturnCode_t`,
4//! but as a Rust `Result<T, DdsError>` — considerably nicer than
5//! sprayed error codes.
6
7extern crate alloc;
8use alloc::string::String;
9
10/// Errors from DCPS operations. Roughly analogous to the spec
11/// `ReturnCode_t` enum; we omit `RETCODE_OK` (using `Result::Ok`
12/// instead) and merge `BAD_PARAMETER` + `PRECONDITION_NOT_MET` where
13/// the distinction doesn't help.
14#[derive(Debug, Clone, PartialEq, Eq)]
15#[non_exhaustive]
16pub enum DdsError {
17    /// Invalid parameter (e.g. an empty topic name).
18    BadParameter {
19        /// Which parameter, briefly.
20        what: &'static str,
21    },
22    /// Operation does not fit the entity state (e.g. `write` on a
23    /// DataWriter whose participant has been shut down).
24    PreconditionNotMet {
25        /// Short description.
26        reason: &'static str,
27    },
28    /// Underlying wire/CDR operation failed.
29    WireError {
30        /// Message, static or dynamic.
31        message: String,
32    },
33    /// QoS policy set is not consistent. Example: Reliability=
34    /// Best-Effort + History=KeepAll without resource_limits — Spec
35    /// §2.2.3 lists some incompatible combinations.
36    InconsistentPolicy {
37        /// Which policy/policies collide.
38        what: &'static str,
39    },
40    /// Transport operation failed.
41    TransportError {
42        /// Static label of the error.
43        label: &'static str,
44    },
45    /// Resource limit reached (max_samples, max_instances, etc.).
46    OutOfResources {
47        /// Which limit.
48        what: &'static str,
49    },
50    /// Timeout on a blocking operation (`take_w_timeout`, etc.).
51    Timeout,
52    /// Operation not implemented in v1.2.
53    Unsupported {
54        /// Short description.
55        feature: &'static str,
56    },
57    /// Operation forbidden by the security policy — DDS-Security 1.2
58    /// §7.3.25, DCPS 1.4 §2.2.2.1.1 ReturnCode_t = NOT_ALLOWED_BY_SECURITY.
59    /// The permissions/access plugin denied the read/write on a
60    /// protected topic; the caller must not learn **which** permissions
61    /// detail was the cause (information leak).
62    NotAllowedBySecurity {
63        /// Short, security-noncritical description.
64        what: &'static str,
65    },
66    /// `IllegalOperation` — the operation does not fit structurally
67    /// (e.g. write() on a DataReader). DCPS 1.4 §2.2.2.1.1
68    /// ReturnCode_t = ILLEGAL_OPERATION.
69    IllegalOperation {
70        /// What does not work.
71        what: &'static str,
72    },
73    /// `ImmutablePolicy` — set_qos on a post-enable immutable QoS
74    /// policy. DCPS 1.4 §2.2.2.1.1 ReturnCode_t = IMMUTABLE_POLICY.
75    ImmutablePolicy {
76        /// Which policy.
77        policy: &'static str,
78    },
79    /// `AlreadyDeleted` — operation on a deleted entity.
80    /// DCPS 1.4 §2.2.2.1.1 ReturnCode_t = ALREADY_DELETED.
81    AlreadyDeleted,
82    /// `NoData` — read/take with no new samples. DCPS 1.4 §2.2.2.1.1
83    /// ReturnCode_t = NO_DATA. In practice often returned as an empty
84    /// sample Vec rather than as an error.
85    NoData,
86    /// `Error` — generic, non-specific error. DCPS 1.4 §2.2.2.1.1
87    /// ReturnCode_t = ERROR. Last resort when no more specific variant
88    /// fits; avoids an information leak toward the caller.
89    Other {
90        /// Short description.
91        reason: &'static str,
92    },
93    /// `NotEnabled` — operation on an entity that has not yet been
94    /// activated via `enable()`. DCPS 1.4 §2.2.2.1.1 ReturnCode_t =
95    /// NOT_ENABLED. §2.1.2 — entities are disabled after `create_*`
96    /// until `enable()`; many ops must then return NotEnabled.
97    NotEnabled,
98}
99
100impl core::fmt::Display for DdsError {
101    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
102        match self {
103            Self::BadParameter { what } => write!(f, "bad parameter: {what}"),
104            Self::PreconditionNotMet { reason } => write!(f, "precondition not met: {reason}"),
105            Self::WireError { message } => write!(f, "wire/cdr error: {message}"),
106            Self::InconsistentPolicy { what } => write!(f, "inconsistent qos policy: {what}"),
107            Self::TransportError { label } => write!(f, "transport error: {label}"),
108            Self::OutOfResources { what } => write!(f, "out of resources: {what}"),
109            Self::Timeout => f.write_str("dds timeout"),
110            Self::Unsupported { feature } => write!(f, "not supported in v1.2: {feature}"),
111            Self::NotAllowedBySecurity { what } => {
112                write!(f, "not allowed by security policy: {what}")
113            }
114            Self::IllegalOperation { what } => write!(f, "illegal operation: {what}"),
115            Self::ImmutablePolicy { policy } => {
116                write!(f, "qos policy is immutable post-enable: {policy}")
117            }
118            Self::AlreadyDeleted => f.write_str("entity already deleted"),
119            Self::NoData => f.write_str("no data available"),
120            Self::Other { reason } => write!(f, "dds error: {reason}"),
121            Self::NotEnabled => f.write_str("entity not enabled"),
122        }
123    }
124}
125
126/// OMG DDS 1.4 `ReturnCode_t` values. Disjoint with `Result::Ok` —
127/// `RETCODE_OK = 0` is not in this list because Ok is represented via
128/// `Result::Ok`.
129pub mod return_code {
130    /// RETCODE_OK = 0 (only for wire mapping; the Rust API uses `Ok`).
131    pub const OK: i32 = 0;
132    /// RETCODE_ERROR.
133    pub const ERROR: i32 = 1;
134    /// RETCODE_UNSUPPORTED.
135    pub const UNSUPPORTED: i32 = 2;
136    /// RETCODE_BAD_PARAMETER.
137    pub const BAD_PARAMETER: i32 = 3;
138    /// RETCODE_PRECONDITION_NOT_MET.
139    pub const PRECONDITION_NOT_MET: i32 = 4;
140    /// RETCODE_OUT_OF_RESOURCES.
141    pub const OUT_OF_RESOURCES: i32 = 5;
142    /// RETCODE_NOT_ENABLED.
143    pub const NOT_ENABLED: i32 = 6;
144    /// RETCODE_IMMUTABLE_POLICY.
145    pub const IMMUTABLE_POLICY: i32 = 7;
146    /// RETCODE_INCONSISTENT_POLICY.
147    pub const INCONSISTENT_POLICY: i32 = 8;
148    /// RETCODE_ALREADY_DELETED.
149    pub const ALREADY_DELETED: i32 = 9;
150    /// RETCODE_TIMEOUT.
151    pub const TIMEOUT: i32 = 10;
152    /// RETCODE_NO_DATA.
153    pub const NO_DATA: i32 = 11;
154    /// RETCODE_ILLEGAL_OPERATION.
155    pub const ILLEGAL_OPERATION: i32 = 12;
156    /// RETCODE_NOT_ALLOWED_BY_SECURITY.
157    pub const NOT_ALLOWED_BY_SECURITY: i32 = 13;
158}
159
160impl DdsError {
161    /// Maps the error to the OMG `ReturnCode_t` integer value
162    /// (DCPS 1.4 §2.2.2.1.1). `WireError` and `TransportError` are
163    /// mapped to RETCODE_ERROR (1) — this is spec-conformant: both are
164    /// unspecified implementation errors.
165    #[must_use]
166    pub fn as_return_code(&self) -> i32 {
167        match self {
168            Self::BadParameter { .. } => return_code::BAD_PARAMETER,
169            Self::PreconditionNotMet { .. } => return_code::PRECONDITION_NOT_MET,
170            Self::WireError { .. } | Self::TransportError { .. } | Self::Other { .. } => {
171                return_code::ERROR
172            }
173            Self::InconsistentPolicy { .. } => return_code::INCONSISTENT_POLICY,
174            Self::OutOfResources { .. } => return_code::OUT_OF_RESOURCES,
175            Self::Timeout => return_code::TIMEOUT,
176            Self::Unsupported { .. } => return_code::UNSUPPORTED,
177            Self::NotAllowedBySecurity { .. } => return_code::NOT_ALLOWED_BY_SECURITY,
178            Self::IllegalOperation { .. } => return_code::ILLEGAL_OPERATION,
179            Self::ImmutablePolicy { .. } => return_code::IMMUTABLE_POLICY,
180            Self::AlreadyDeleted => return_code::ALREADY_DELETED,
181            Self::NoData => return_code::NO_DATA,
182            Self::NotEnabled => return_code::NOT_ENABLED,
183        }
184    }
185}
186
187#[cfg(feature = "std")]
188impl std::error::Error for DdsError {}
189
190/// Shorthand for DCPS results.
191pub type Result<T> = core::result::Result<T, DdsError>;
192
193#[cfg(test)]
194#[allow(clippy::expect_used)]
195mod tests {
196    use super::*;
197
198    // ---- §2.2.1.1.x ReturnCode_t mapping ----
199
200    #[test]
201    fn rc_error_maps_for_wire_and_transport_and_other() {
202        // Spec §2.2.1.1.2 RC ERROR — generic error.
203        assert_eq!(
204            DdsError::WireError {
205                message: "x".into()
206            }
207            .as_return_code(),
208            return_code::ERROR
209        );
210        assert_eq!(
211            DdsError::TransportError { label: "y" }.as_return_code(),
212            return_code::ERROR
213        );
214        assert_eq!(
215            DdsError::Other { reason: "z" }.as_return_code(),
216            return_code::ERROR
217        );
218    }
219
220    #[test]
221    fn rc_unsupported_maps() {
222        assert_eq!(
223            DdsError::Unsupported { feature: "f" }.as_return_code(),
224            return_code::UNSUPPORTED
225        );
226    }
227
228    #[test]
229    fn rc_bad_parameter_maps() {
230        assert_eq!(
231            DdsError::BadParameter { what: "p" }.as_return_code(),
232            return_code::BAD_PARAMETER
233        );
234    }
235
236    #[test]
237    fn rc_precondition_not_met_maps() {
238        assert_eq!(
239            DdsError::PreconditionNotMet { reason: "r" }.as_return_code(),
240            return_code::PRECONDITION_NOT_MET
241        );
242    }
243
244    #[test]
245    fn rc_out_of_resources_maps() {
246        assert_eq!(
247            DdsError::OutOfResources { what: "samples" }.as_return_code(),
248            return_code::OUT_OF_RESOURCES
249        );
250    }
251
252    #[test]
253    fn rc_not_enabled_maps() {
254        assert_eq!(
255            DdsError::NotEnabled.as_return_code(),
256            return_code::NOT_ENABLED
257        );
258    }
259
260    #[test]
261    fn rc_immutable_policy_maps() {
262        assert_eq!(
263            DdsError::ImmutablePolicy {
264                policy: "Reliability"
265            }
266            .as_return_code(),
267            return_code::IMMUTABLE_POLICY
268        );
269    }
270
271    #[test]
272    fn rc_inconsistent_policy_maps() {
273        assert_eq!(
274            DdsError::InconsistentPolicy { what: "x" }.as_return_code(),
275            return_code::INCONSISTENT_POLICY
276        );
277    }
278
279    #[test]
280    fn rc_already_deleted_maps() {
281        assert_eq!(
282            DdsError::AlreadyDeleted.as_return_code(),
283            return_code::ALREADY_DELETED
284        );
285    }
286
287    #[test]
288    fn rc_timeout_maps() {
289        assert_eq!(DdsError::Timeout.as_return_code(), return_code::TIMEOUT);
290    }
291
292    #[test]
293    fn rc_no_data_maps() {
294        assert_eq!(DdsError::NoData.as_return_code(), return_code::NO_DATA);
295    }
296
297    #[test]
298    fn rc_illegal_operation_maps() {
299        assert_eq!(
300            DdsError::IllegalOperation {
301                what: "write_on_reader"
302            }
303            .as_return_code(),
304            return_code::ILLEGAL_OPERATION
305        );
306    }
307
308    #[test]
309    fn rc_not_allowed_by_security_maps() {
310        assert_eq!(
311            DdsError::NotAllowedBySecurity { what: "topic" }.as_return_code(),
312            return_code::NOT_ALLOWED_BY_SECURITY
313        );
314    }
315
316    #[test]
317    fn rc_constants_have_spec_values() {
318        // OMG DDS 1.4 §2.2.2.1.1 numeric spec values. A drift here
319        // breaks every wire-mapping test in the C++/Java bridges.
320        assert_eq!(return_code::OK, 0);
321        assert_eq!(return_code::ERROR, 1);
322        assert_eq!(return_code::UNSUPPORTED, 2);
323        assert_eq!(return_code::BAD_PARAMETER, 3);
324        assert_eq!(return_code::PRECONDITION_NOT_MET, 4);
325        assert_eq!(return_code::OUT_OF_RESOURCES, 5);
326        assert_eq!(return_code::NOT_ENABLED, 6);
327        assert_eq!(return_code::IMMUTABLE_POLICY, 7);
328        assert_eq!(return_code::INCONSISTENT_POLICY, 8);
329        assert_eq!(return_code::ALREADY_DELETED, 9);
330        assert_eq!(return_code::TIMEOUT, 10);
331        assert_eq!(return_code::NO_DATA, 11);
332        assert_eq!(return_code::ILLEGAL_OPERATION, 12);
333        assert_eq!(return_code::NOT_ALLOWED_BY_SECURITY, 13);
334    }
335
336    #[test]
337    fn rc_display_does_not_leak_security_details() {
338        // §2.2.2.1.1 NOT_ALLOWED_BY_SECURITY — the caller must not learn
339        // the permissions-detail reason (information leak).
340        let e = DdsError::NotAllowedBySecurity { what: "Topic A" };
341        let s = format!("{e}");
342        assert!(s.contains("not allowed by security"));
343        assert!(s.contains("Topic A"));
344        // Should NOT mention concrete permissions internals.
345        assert!(!s.to_lowercase().contains("permission file"));
346        assert!(!s.to_lowercase().contains("ca cert"));
347    }
348}