Skip to main content

vmb_core/
error.rs

1//! Error type shared by the `vmb-rs` domain and every `VmbRuntime` adapter.
2//!
3//! Vimba X itself returns `VmbError_t` (a signed 32-bit integer) from every
4//! call. This module maps non-success return codes into a rich [`VmbError`]
5//! enum that carries both the numeric code and a static human-readable
6//! name.
7//!
8//! The Vimba X C API does not expose a runtime error-to-string function, so
9//! the mapping from code to name is performed manually via [`error_name`]
10//! below. The list of codes is derived from `VmbErrorType` in
11//! `vmb_sys::bindings`; a drift guard in the `vmb` facade verifies the two
12//! stay in sync.
13
14use std::path::PathBuf;
15
16use thiserror::Error;
17
18use crate::Result;
19
20/// Errors returned by the safe `vmb` wrapper.
21#[derive(Debug, Error)]
22pub enum VmbError {
23    /// A non-success return code from a VmbC call.
24    #[error("Vimba SDK error {code} ({}): {message}", error_name(*code))]
25    Sdk {
26        /// The raw `VmbError_t` return code.
27        code: i32,
28        /// A short description of which call failed.
29        message: String,
30    },
31
32    /// The Vimba runtime has not been started, or was already shut down.
33    #[error("Vimba X runtime has not been started")]
34    NotStarted,
35
36    /// Attempted to start the Vimba runtime while a previous `VmbSystem`
37    /// instance is still alive. The runtime is process-global and the
38    /// wrapper enforces a singleton invariant.
39    #[error("Vimba X runtime is already started (singleton violation)")]
40    AlreadyStarted,
41
42    /// I/O failure while reading a settings XML file (or a related path).
43    #[error("I/O error for {}: {source}", path.display())]
44    Io {
45        /// The offending path.
46        path: PathBuf,
47        /// The underlying [`std::io::Error`].
48        #[source]
49        source: std::io::Error,
50    },
51
52    /// A string supplied by the SDK or by the caller was not valid UTF-8 or
53    /// contained an interior nul byte.
54    #[error("invalid string (non-UTF-8 or interior nul) in {context}")]
55    InvalidString {
56        /// A static label identifying which string was rejected.
57        context: &'static str,
58    },
59
60    /// `Camera::start_capture` was called while a previous capture is still
61    /// running on the same camera.
62    #[error("capture is already running on this camera")]
63    CaptureAlreadyRunning,
64
65    /// A received frame was smaller than expected for its declared format.
66    #[error("frame too small: expected {expected} bytes, got {actual}")]
67    FrameTooSmall {
68        /// The declared/expected byte count.
69        expected: usize,
70        /// The actual byte count delivered by the SDK.
71        actual: usize,
72    },
73}
74
75/// Map a Vimba error code to its static name. Returns `"VmbErrorUnknown"`
76/// for codes the wrapper doesn't know about.
77///
78/// The list mirrors `vmb_sys::bindings::VmbErrorType`. The drift guard in
79/// the `vmb` facade's integration tests verifies the two stay in sync.
80pub const fn error_name(code: i32) -> &'static str {
81    match code {
82        0 => "VmbErrorSuccess",
83        -1 => "VmbErrorInternalFault",
84        -2 => "VmbErrorApiNotStarted",
85        -3 => "VmbErrorNotFound",
86        -4 => "VmbErrorBadHandle",
87        -5 => "VmbErrorDeviceNotOpen",
88        -6 => "VmbErrorInvalidAccess",
89        -7 => "VmbErrorBadParameter",
90        -8 => "VmbErrorStructSize",
91        -9 => "VmbErrorMoreData",
92        -10 => "VmbErrorWrongType",
93        -11 => "VmbErrorInvalidValue",
94        -12 => "VmbErrorTimeout",
95        -13 => "VmbErrorOther",
96        -14 => "VmbErrorResources",
97        -15 => "VmbErrorInvalidCall",
98        -16 => "VmbErrorNoTL",
99        -17 => "VmbErrorNotImplemented",
100        -18 => "VmbErrorNotSupported",
101        -19 => "VmbErrorIncomplete",
102        -20 => "VmbErrorIO",
103        -21 => "VmbErrorValidValueSetNotPresent",
104        -22 => "VmbErrorGenTLUnspecified",
105        -23 => "VmbErrorUnspecified",
106        -24 => "VmbErrorBusy",
107        -25 => "VmbErrorNoData",
108        -26 => "VmbErrorParsingChunkData",
109        -27 => "VmbErrorInUse",
110        -28 => "VmbErrorUnknown",
111        -29 => "VmbErrorXml",
112        -30 => "VmbErrorNotAvailable",
113        -31 => "VmbErrorNotInitialized",
114        -32 => "VmbErrorInvalidAddress",
115        -33 => "VmbErrorAlready",
116        -34 => "VmbErrorNoChunkData",
117        -35 => "VmbErrorUserCallbackException",
118        -36 => "VmbErrorFeaturesUnavailable",
119        -37 => "VmbErrorTLNotFound",
120        -39 => "VmbErrorAmbiguous",
121        -40 => "VmbErrorRetriesExceeded",
122        -41 => "VmbErrorInsufficientBufferCount",
123        1 => "VmbErrorCustom",
124        _ => "VmbErrorUnrecognized",
125    }
126}
127
128/// Convert a raw VmbC return code into a `Result`.
129///
130/// Returns `Ok(())` if `code == 0` (success), and `Err(VmbError::Sdk { .. })`
131/// otherwise. The `message` is a generic placeholder — callers may wrap the
132/// result with `.map_err(...)` to add call-site context if they wish.
133pub fn check(code: i32) -> Result<()> {
134    if code == 0 {
135        Ok(())
136    } else {
137        Err(VmbError::Sdk {
138            code,
139            message: format!("VmbC call failed ({})", error_name(code)),
140        })
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn error_name_maps_known_codes() {
150        assert_eq!(error_name(0), "VmbErrorSuccess");
151        assert_eq!(error_name(-2), "VmbErrorApiNotStarted");
152        assert_eq!(error_name(-41), "VmbErrorInsufficientBufferCount");
153        assert_eq!(error_name(1), "VmbErrorCustom");
154    }
155
156    #[test]
157    fn error_name_unknown_code_has_fallback() {
158        assert_eq!(error_name(12345), "VmbErrorUnrecognized");
159        assert_eq!(error_name(-999), "VmbErrorUnrecognized");
160    }
161
162    #[test]
163    fn error_name_covers_every_documented_code() {
164        // Exhaustive over the full -41..=1 range (the SDK's current
165        // numeric space). Deletion mutants on individual arms are
166        // caught because removing any arm would reroute that code to
167        // `VmbErrorUnrecognized`.
168        let expected: &[(i32, &str)] = &[
169            (0, "VmbErrorSuccess"),
170            (-1, "VmbErrorInternalFault"),
171            (-2, "VmbErrorApiNotStarted"),
172            (-3, "VmbErrorNotFound"),
173            (-4, "VmbErrorBadHandle"),
174            (-5, "VmbErrorDeviceNotOpen"),
175            (-6, "VmbErrorInvalidAccess"),
176            (-7, "VmbErrorBadParameter"),
177            (-8, "VmbErrorStructSize"),
178            (-9, "VmbErrorMoreData"),
179            (-10, "VmbErrorWrongType"),
180            (-11, "VmbErrorInvalidValue"),
181            (-12, "VmbErrorTimeout"),
182            (-13, "VmbErrorOther"),
183            (-14, "VmbErrorResources"),
184            (-15, "VmbErrorInvalidCall"),
185            (-16, "VmbErrorNoTL"),
186            (-17, "VmbErrorNotImplemented"),
187            (-18, "VmbErrorNotSupported"),
188            (-19, "VmbErrorIncomplete"),
189            (-20, "VmbErrorIO"),
190            (-21, "VmbErrorValidValueSetNotPresent"),
191            (-22, "VmbErrorGenTLUnspecified"),
192            (-23, "VmbErrorUnspecified"),
193            (-24, "VmbErrorBusy"),
194            (-25, "VmbErrorNoData"),
195            (-26, "VmbErrorParsingChunkData"),
196            (-27, "VmbErrorInUse"),
197            (-28, "VmbErrorUnknown"),
198            (-29, "VmbErrorXml"),
199            (-30, "VmbErrorNotAvailable"),
200            (-31, "VmbErrorNotInitialized"),
201            (-32, "VmbErrorInvalidAddress"),
202            (-33, "VmbErrorAlready"),
203            (-34, "VmbErrorNoChunkData"),
204            (-35, "VmbErrorUserCallbackException"),
205            (-36, "VmbErrorFeaturesUnavailable"),
206            (-37, "VmbErrorTLNotFound"),
207            (-39, "VmbErrorAmbiguous"),
208            (-40, "VmbErrorRetriesExceeded"),
209            (-41, "VmbErrorInsufficientBufferCount"),
210            (1, "VmbErrorCustom"),
211        ];
212        for (code, name) in expected {
213            assert_eq!(error_name(*code), *name, "wrong name for code {code}");
214        }
215        // A known gap in the SDK's enum (code `-38` is unused) must
216        // fall through to the catch-all.
217        assert_eq!(error_name(-38), "VmbErrorUnrecognized");
218    }
219
220    #[test]
221    fn display_includes_error_name() {
222        let err = VmbError::Sdk {
223            code: -4,
224            message: "bad handle".to_string(),
225        };
226        let s = format!("{err}");
227        assert!(s.contains("VmbErrorBadHandle"));
228        assert!(s.contains("bad handle"));
229    }
230
231    #[test]
232    fn check_success_is_ok() {
233        assert!(check(0).is_ok());
234    }
235
236    #[test]
237    fn check_error_is_sdk_with_code_and_name() {
238        match check(-4) {
239            Err(VmbError::Sdk { code, message }) => {
240                assert_eq!(code, -4);
241                assert!(message.contains("VmbErrorBadHandle"));
242            }
243            other => panic!("expected Err(Sdk), got {other:?}"),
244        }
245    }
246
247    #[test]
248    fn display_invalid_string_includes_context() {
249        let err = VmbError::InvalidString {
250            context: "camera_id",
251        };
252        assert!(format!("{err}").contains("camera_id"));
253    }
254
255    #[test]
256    fn display_frame_too_small_includes_counts() {
257        let err = VmbError::FrameTooSmall {
258            expected: 100,
259            actual: 80,
260        };
261        let s = format!("{err}");
262        assert!(s.contains("100"));
263        assert!(s.contains("80"));
264    }
265
266    #[test]
267    fn display_io_includes_path() {
268        let err = VmbError::Io {
269            path: PathBuf::from("/tmp/does-not-exist.xml"),
270            source: std::io::Error::other("boom"),
271        };
272        let s = format!("{err}");
273        assert!(s.contains("does-not-exist.xml"));
274    }
275
276    #[test]
277    fn already_started_and_not_started_display() {
278        assert!(format!("{}", VmbError::AlreadyStarted).contains("already started"));
279        assert!(format!("{}", VmbError::NotStarted).contains("not been started"));
280    }
281
282    #[test]
283    fn capture_already_running_display() {
284        assert!(format!("{}", VmbError::CaptureAlreadyRunning).contains("already running"));
285    }
286}