Skip to main content

zerodds_dcps/
error.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! DCPS-Fehlertypen. An OMG DDS 1.4 §2.2.2.1 `ReturnCode_t` angelehnt,
4//! aber als Rust-Result<T, DdsError> — deutlich angenehmer als
5//! sprayed-error-codes.
6
7extern crate alloc;
8use alloc::string::String;
9
10/// Fehler aus DCPS-Operationen. Halbwegs analog zum Spec-
11/// `ReturnCode_t`-Enum; wir lassen allerdings `RETCODE_OK` weg
12/// (stattdessen `Result::Ok`) und mergen `BAD_PARAMETER` +
13/// `PRECONDITION_NOT_MET` wo die Unterscheidung nichts hilft.
14#[derive(Debug, Clone, PartialEq, Eq)]
15#[non_exhaustive]
16pub enum DdsError {
17    /// Ungueltiger Parameter (z.B. leerer Topic-Name).
18    BadParameter {
19        /// Welcher Parameter, kurz.
20        what: &'static str,
21    },
22    /// Operation paesst nicht zum Entity-Zustand (z.B. `write` auf
23    /// einen DataWriter, dessen Participant beendet wurde).
24    PreconditionNotMet {
25        /// Kurze Beschreibung.
26        reason: &'static str,
27    },
28    /// Unterliegende Wire/CDR-Operation ist gefehlschlagen.
29    WireError {
30        /// Nachricht, statisch oder dynamisch.
31        message: String,
32    },
33    /// QoS-Policy-Set nicht konsistent. Beispiel: Reliability=
34    /// Best-Effort + History=KeepAll ohne resource_limits — Spec
35    /// §2.2.3 nennt einige inkompatible Kombinationen.
36    InconsistentPolicy {
37        /// Welche Policy(s) kollidieren.
38        what: &'static str,
39    },
40    /// Transport-Operation fehlgeschlagen.
41    TransportError {
42        /// Statisches Label des Fehlers.
43        label: &'static str,
44    },
45    /// Resource-Limit erreicht (max_samples, max_instances, etc.).
46    OutOfResources {
47        /// Welches Limit.
48        what: &'static str,
49    },
50    /// Timeout bei blockierender Operation (`take_w_timeout`, etc.).
51    Timeout,
52    /// Operation nicht implementiert in v1.2.
53    Unsupported {
54        /// Kurz-Beschreibung.
55        feature: &'static str,
56    },
57    /// Operation durch Security-Policy untersagt — DDS-Security 1.2
58    /// §7.3.25, DCPS 1.4 §2.2.2.1.1 ReturnCode_t = NOT_ALLOWED_BY_SECURITY.
59    /// Der Permissions-/Access-Plugin hat das Lesen/Schreiben auf einem
60    /// geschuetzten Topic abgelehnt; der Caller darf nicht erfahren,
61    /// **welches** Permissions-Detail die Ursache war (Information-Leak).
62    NotAllowedBySecurity {
63        /// Kurze, sicherheits-unkritische Beschreibung.
64        what: &'static str,
65    },
66    /// `IllegalOperation` — die Operation passt strukturell nicht
67    /// (z.B. write() auf einen DataReader). DCPS 1.4 §2.2.2.1.1
68    /// ReturnCode_t = ILLEGAL_OPERATION.
69    IllegalOperation {
70        /// Was nicht geht.
71        what: &'static str,
72    },
73    /// `ImmutablePolicy` — set_qos auf eine post-enable immutable QoS-
74    /// Policy. DCPS 1.4 §2.2.2.1.1 ReturnCode_t = IMMUTABLE_POLICY.
75    ImmutablePolicy {
76        /// Welche Policy.
77        policy: &'static str,
78    },
79    /// `AlreadyDeleted` — Operation auf einer geloeschten Entity.
80    /// DCPS 1.4 §2.2.2.1.1 ReturnCode_t = ALREADY_DELETED.
81    AlreadyDeleted,
82    /// `NoData` — read/take ohne neue Samples. DCPS 1.4 §2.2.2.1.1
83    /// ReturnCode_t = NO_DATA. Wird in der Praxis oft als leerer
84    /// Sample-Vec zurueckgegeben statt als Error.
85    NoData,
86    /// `Error` — generischer, nicht-spezifischer Fehler. DCPS 1.4
87    /// §2.2.2.1.1 ReturnCode_t = ERROR. Letzte Wahl wenn keine
88    /// spezifischere Variante passt; vermeidet Information-Leak
89    /// gegenueber dem Caller.
90    Other {
91        /// Kurze Beschreibung.
92        reason: &'static str,
93    },
94    /// `NotEnabled` — Operation auf einer Entity, die noch nicht via
95    /// `enable()` aktiviert wurde. DCPS 1.4 §2.2.2.1.1 ReturnCode_t =
96    /// NOT_ENABLED. §2.1.2 — Entities sind nach `create_*` disabled
97    /// bis `enable()`; viele Ops MUESSEN dann NotEnabled liefern.
98    NotEnabled,
99}
100
101impl core::fmt::Display for DdsError {
102    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
103        match self {
104            Self::BadParameter { what } => write!(f, "bad parameter: {what}"),
105            Self::PreconditionNotMet { reason } => write!(f, "precondition not met: {reason}"),
106            Self::WireError { message } => write!(f, "wire/cdr error: {message}"),
107            Self::InconsistentPolicy { what } => write!(f, "inconsistent qos policy: {what}"),
108            Self::TransportError { label } => write!(f, "transport error: {label}"),
109            Self::OutOfResources { what } => write!(f, "out of resources: {what}"),
110            Self::Timeout => f.write_str("dds timeout"),
111            Self::Unsupported { feature } => write!(f, "not supported in v1.2: {feature}"),
112            Self::NotAllowedBySecurity { what } => {
113                write!(f, "not allowed by security policy: {what}")
114            }
115            Self::IllegalOperation { what } => write!(f, "illegal operation: {what}"),
116            Self::ImmutablePolicy { policy } => {
117                write!(f, "qos policy is immutable post-enable: {policy}")
118            }
119            Self::AlreadyDeleted => f.write_str("entity already deleted"),
120            Self::NoData => f.write_str("no data available"),
121            Self::Other { reason } => write!(f, "dds error: {reason}"),
122            Self::NotEnabled => f.write_str("entity not enabled"),
123        }
124    }
125}
126
127/// OMG DDS 1.4 `ReturnCode_t`-Werte. Disjoint mit `Result::Ok` —
128/// `RETCODE_OK = 0` ist nicht in dieser Liste, weil Ok via
129/// `Result::Ok` repraesentiert wird.
130pub mod return_code {
131    /// RETCODE_OK = 0 (nur fuer Wire-Mapping; Rust-API nutzt `Ok`).
132    pub const OK: i32 = 0;
133    /// RETCODE_ERROR.
134    pub const ERROR: i32 = 1;
135    /// RETCODE_UNSUPPORTED.
136    pub const UNSUPPORTED: i32 = 2;
137    /// RETCODE_BAD_PARAMETER.
138    pub const BAD_PARAMETER: i32 = 3;
139    /// RETCODE_PRECONDITION_NOT_MET.
140    pub const PRECONDITION_NOT_MET: i32 = 4;
141    /// RETCODE_OUT_OF_RESOURCES.
142    pub const OUT_OF_RESOURCES: i32 = 5;
143    /// RETCODE_NOT_ENABLED.
144    pub const NOT_ENABLED: i32 = 6;
145    /// RETCODE_IMMUTABLE_POLICY.
146    pub const IMMUTABLE_POLICY: i32 = 7;
147    /// RETCODE_INCONSISTENT_POLICY.
148    pub const INCONSISTENT_POLICY: i32 = 8;
149    /// RETCODE_ALREADY_DELETED.
150    pub const ALREADY_DELETED: i32 = 9;
151    /// RETCODE_TIMEOUT.
152    pub const TIMEOUT: i32 = 10;
153    /// RETCODE_NO_DATA.
154    pub const NO_DATA: i32 = 11;
155    /// RETCODE_ILLEGAL_OPERATION.
156    pub const ILLEGAL_OPERATION: i32 = 12;
157    /// RETCODE_NOT_ALLOWED_BY_SECURITY.
158    pub const NOT_ALLOWED_BY_SECURITY: i32 = 13;
159}
160
161impl DdsError {
162    /// Mappt den Fehler auf den OMG `ReturnCode_t`-Integer-Wert
163    /// (DCPS 1.4 §2.2.2.1.1). `WireError` und `TransportError` werden
164    /// auf RETCODE_ERROR (1) abgebildet — das ist Spec-konform: beide
165    /// sind unspezifizierte Implementations-Fehler.
166    #[must_use]
167    pub fn as_return_code(&self) -> i32 {
168        match self {
169            Self::BadParameter { .. } => return_code::BAD_PARAMETER,
170            Self::PreconditionNotMet { .. } => return_code::PRECONDITION_NOT_MET,
171            Self::WireError { .. } | Self::TransportError { .. } | Self::Other { .. } => {
172                return_code::ERROR
173            }
174            Self::InconsistentPolicy { .. } => return_code::INCONSISTENT_POLICY,
175            Self::OutOfResources { .. } => return_code::OUT_OF_RESOURCES,
176            Self::Timeout => return_code::TIMEOUT,
177            Self::Unsupported { .. } => return_code::UNSUPPORTED,
178            Self::NotAllowedBySecurity { .. } => return_code::NOT_ALLOWED_BY_SECURITY,
179            Self::IllegalOperation { .. } => return_code::ILLEGAL_OPERATION,
180            Self::ImmutablePolicy { .. } => return_code::IMMUTABLE_POLICY,
181            Self::AlreadyDeleted => return_code::ALREADY_DELETED,
182            Self::NoData => return_code::NO_DATA,
183            Self::NotEnabled => return_code::NOT_ENABLED,
184        }
185    }
186}
187
188#[cfg(feature = "std")]
189impl std::error::Error for DdsError {}
190
191/// Kurzform fuer DCPS-Ergebnisse.
192pub type Result<T> = core::result::Result<T, DdsError>;
193
194#[cfg(test)]
195#[allow(clippy::expect_used)]
196mod tests {
197    use super::*;
198
199    // ---- §2.2.1.1.x ReturnCode_t Mapping ----
200
201    #[test]
202    fn rc_error_maps_for_wire_and_transport_and_other() {
203        // Spec §2.2.1.1.2 RC ERROR — generischer Fehler.
204        assert_eq!(
205            DdsError::WireError {
206                message: "x".into()
207            }
208            .as_return_code(),
209            return_code::ERROR
210        );
211        assert_eq!(
212            DdsError::TransportError { label: "y" }.as_return_code(),
213            return_code::ERROR
214        );
215        assert_eq!(
216            DdsError::Other { reason: "z" }.as_return_code(),
217            return_code::ERROR
218        );
219    }
220
221    #[test]
222    fn rc_unsupported_maps() {
223        assert_eq!(
224            DdsError::Unsupported { feature: "f" }.as_return_code(),
225            return_code::UNSUPPORTED
226        );
227    }
228
229    #[test]
230    fn rc_bad_parameter_maps() {
231        assert_eq!(
232            DdsError::BadParameter { what: "p" }.as_return_code(),
233            return_code::BAD_PARAMETER
234        );
235    }
236
237    #[test]
238    fn rc_precondition_not_met_maps() {
239        assert_eq!(
240            DdsError::PreconditionNotMet { reason: "r" }.as_return_code(),
241            return_code::PRECONDITION_NOT_MET
242        );
243    }
244
245    #[test]
246    fn rc_out_of_resources_maps() {
247        assert_eq!(
248            DdsError::OutOfResources { what: "samples" }.as_return_code(),
249            return_code::OUT_OF_RESOURCES
250        );
251    }
252
253    #[test]
254    fn rc_not_enabled_maps() {
255        assert_eq!(
256            DdsError::NotEnabled.as_return_code(),
257            return_code::NOT_ENABLED
258        );
259    }
260
261    #[test]
262    fn rc_immutable_policy_maps() {
263        assert_eq!(
264            DdsError::ImmutablePolicy {
265                policy: "Reliability"
266            }
267            .as_return_code(),
268            return_code::IMMUTABLE_POLICY
269        );
270    }
271
272    #[test]
273    fn rc_inconsistent_policy_maps() {
274        assert_eq!(
275            DdsError::InconsistentPolicy { what: "x" }.as_return_code(),
276            return_code::INCONSISTENT_POLICY
277        );
278    }
279
280    #[test]
281    fn rc_already_deleted_maps() {
282        assert_eq!(
283            DdsError::AlreadyDeleted.as_return_code(),
284            return_code::ALREADY_DELETED
285        );
286    }
287
288    #[test]
289    fn rc_timeout_maps() {
290        assert_eq!(DdsError::Timeout.as_return_code(), return_code::TIMEOUT);
291    }
292
293    #[test]
294    fn rc_no_data_maps() {
295        assert_eq!(DdsError::NoData.as_return_code(), return_code::NO_DATA);
296    }
297
298    #[test]
299    fn rc_illegal_operation_maps() {
300        assert_eq!(
301            DdsError::IllegalOperation {
302                what: "write_on_reader"
303            }
304            .as_return_code(),
305            return_code::ILLEGAL_OPERATION
306        );
307    }
308
309    #[test]
310    fn rc_not_allowed_by_security_maps() {
311        assert_eq!(
312            DdsError::NotAllowedBySecurity { what: "topic" }.as_return_code(),
313            return_code::NOT_ALLOWED_BY_SECURITY
314        );
315    }
316
317    #[test]
318    fn rc_constants_have_spec_values() {
319        // OMG DDS 1.4 §2.2.2.1.1 numerische Spec-Werte. Ein Drift hier
320        // bricht jeden Wire-Mapping-Test in C++/Java-Bridges.
321        assert_eq!(return_code::OK, 0);
322        assert_eq!(return_code::ERROR, 1);
323        assert_eq!(return_code::UNSUPPORTED, 2);
324        assert_eq!(return_code::BAD_PARAMETER, 3);
325        assert_eq!(return_code::PRECONDITION_NOT_MET, 4);
326        assert_eq!(return_code::OUT_OF_RESOURCES, 5);
327        assert_eq!(return_code::NOT_ENABLED, 6);
328        assert_eq!(return_code::IMMUTABLE_POLICY, 7);
329        assert_eq!(return_code::INCONSISTENT_POLICY, 8);
330        assert_eq!(return_code::ALREADY_DELETED, 9);
331        assert_eq!(return_code::TIMEOUT, 10);
332        assert_eq!(return_code::NO_DATA, 11);
333        assert_eq!(return_code::ILLEGAL_OPERATION, 12);
334        assert_eq!(return_code::NOT_ALLOWED_BY_SECURITY, 13);
335    }
336
337    #[test]
338    fn rc_display_does_not_leak_security_details() {
339        // §2.2.2.1.1 NOT_ALLOWED_BY_SECURITY — Caller darf nicht den
340        // Permissions-Detail-Grund erfahren (Information-Leak).
341        let e = DdsError::NotAllowedBySecurity { what: "Topic A" };
342        let s = format!("{e}");
343        assert!(s.contains("not allowed by security"));
344        assert!(s.contains("Topic A"));
345        // Sollte NICHT konkrete Permissions-Internals erwaehnen.
346        assert!(!s.to_lowercase().contains("permission file"));
347        assert!(!s.to_lowercase().contains("ca cert"));
348    }
349}