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    /// Failed to load the Vimba X shared library at runtime, or resolve
75    /// one of its symbols. Typically surfaced from `VmbFfiRuntime::new`
76    /// when the SDK is not installed on the host.
77    #[error("failed to load Vimba X runtime: {message}")]
78    LoadFailed {
79        /// Human-readable description of the loader failure, including
80        /// the path(s) tried and the underlying OS error.
81        message: String,
82    },
83}
84
85/// Map a Vimba error code to its static name. Returns `"VmbErrorUnknown"`
86/// for codes the wrapper doesn't know about.
87///
88/// The list mirrors `vmb_sys::bindings::VmbErrorType`. The drift guard in
89/// the `vmb` facade's integration tests verifies the two stay in sync.
90pub const fn error_name(code: i32) -> &'static str {
91    match code {
92        0 => "VmbErrorSuccess",
93        -1 => "VmbErrorInternalFault",
94        -2 => "VmbErrorApiNotStarted",
95        -3 => "VmbErrorNotFound",
96        -4 => "VmbErrorBadHandle",
97        -5 => "VmbErrorDeviceNotOpen",
98        -6 => "VmbErrorInvalidAccess",
99        -7 => "VmbErrorBadParameter",
100        -8 => "VmbErrorStructSize",
101        -9 => "VmbErrorMoreData",
102        -10 => "VmbErrorWrongType",
103        -11 => "VmbErrorInvalidValue",
104        -12 => "VmbErrorTimeout",
105        -13 => "VmbErrorOther",
106        -14 => "VmbErrorResources",
107        -15 => "VmbErrorInvalidCall",
108        -16 => "VmbErrorNoTL",
109        -17 => "VmbErrorNotImplemented",
110        -18 => "VmbErrorNotSupported",
111        -19 => "VmbErrorIncomplete",
112        -20 => "VmbErrorIO",
113        -21 => "VmbErrorValidValueSetNotPresent",
114        -22 => "VmbErrorGenTLUnspecified",
115        -23 => "VmbErrorUnspecified",
116        -24 => "VmbErrorBusy",
117        -25 => "VmbErrorNoData",
118        -26 => "VmbErrorParsingChunkData",
119        -27 => "VmbErrorInUse",
120        -28 => "VmbErrorUnknown",
121        -29 => "VmbErrorXml",
122        -30 => "VmbErrorNotAvailable",
123        -31 => "VmbErrorNotInitialized",
124        -32 => "VmbErrorInvalidAddress",
125        -33 => "VmbErrorAlready",
126        -34 => "VmbErrorNoChunkData",
127        -35 => "VmbErrorUserCallbackException",
128        -36 => "VmbErrorFeaturesUnavailable",
129        -37 => "VmbErrorTLNotFound",
130        -39 => "VmbErrorAmbiguous",
131        -40 => "VmbErrorRetriesExceeded",
132        -41 => "VmbErrorInsufficientBufferCount",
133        1 => "VmbErrorCustom",
134        _ => "VmbErrorUnrecognized",
135    }
136}
137
138/// Convert a raw VmbC return code into a `Result`.
139///
140/// Returns `Ok(())` if `code == 0` (success), and `Err(VmbError::Sdk { .. })`
141/// otherwise. The `message` is a generic placeholder — callers may wrap the
142/// result with `.map_err(...)` to add call-site context if they wish.
143pub fn check(code: i32) -> Result<()> {
144    if code == 0 {
145        Ok(())
146    } else {
147        Err(VmbError::Sdk {
148            code,
149            message: format!("VmbC call failed ({})", error_name(code)),
150        })
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn error_name_maps_known_codes() {
160        assert_eq!(error_name(0), "VmbErrorSuccess");
161        assert_eq!(error_name(-2), "VmbErrorApiNotStarted");
162        assert_eq!(error_name(-41), "VmbErrorInsufficientBufferCount");
163        assert_eq!(error_name(1), "VmbErrorCustom");
164    }
165
166    #[test]
167    fn error_name_unknown_code_has_fallback() {
168        assert_eq!(error_name(12345), "VmbErrorUnrecognized");
169        assert_eq!(error_name(-999), "VmbErrorUnrecognized");
170    }
171
172    #[test]
173    fn error_name_covers_every_documented_code() {
174        // Exhaustive over the full -41..=1 range (the SDK's current
175        // numeric space). Deletion mutants on individual arms are
176        // caught because removing any arm would reroute that code to
177        // `VmbErrorUnrecognized`.
178        let expected: &[(i32, &str)] = &[
179            (0, "VmbErrorSuccess"),
180            (-1, "VmbErrorInternalFault"),
181            (-2, "VmbErrorApiNotStarted"),
182            (-3, "VmbErrorNotFound"),
183            (-4, "VmbErrorBadHandle"),
184            (-5, "VmbErrorDeviceNotOpen"),
185            (-6, "VmbErrorInvalidAccess"),
186            (-7, "VmbErrorBadParameter"),
187            (-8, "VmbErrorStructSize"),
188            (-9, "VmbErrorMoreData"),
189            (-10, "VmbErrorWrongType"),
190            (-11, "VmbErrorInvalidValue"),
191            (-12, "VmbErrorTimeout"),
192            (-13, "VmbErrorOther"),
193            (-14, "VmbErrorResources"),
194            (-15, "VmbErrorInvalidCall"),
195            (-16, "VmbErrorNoTL"),
196            (-17, "VmbErrorNotImplemented"),
197            (-18, "VmbErrorNotSupported"),
198            (-19, "VmbErrorIncomplete"),
199            (-20, "VmbErrorIO"),
200            (-21, "VmbErrorValidValueSetNotPresent"),
201            (-22, "VmbErrorGenTLUnspecified"),
202            (-23, "VmbErrorUnspecified"),
203            (-24, "VmbErrorBusy"),
204            (-25, "VmbErrorNoData"),
205            (-26, "VmbErrorParsingChunkData"),
206            (-27, "VmbErrorInUse"),
207            (-28, "VmbErrorUnknown"),
208            (-29, "VmbErrorXml"),
209            (-30, "VmbErrorNotAvailable"),
210            (-31, "VmbErrorNotInitialized"),
211            (-32, "VmbErrorInvalidAddress"),
212            (-33, "VmbErrorAlready"),
213            (-34, "VmbErrorNoChunkData"),
214            (-35, "VmbErrorUserCallbackException"),
215            (-36, "VmbErrorFeaturesUnavailable"),
216            (-37, "VmbErrorTLNotFound"),
217            (-39, "VmbErrorAmbiguous"),
218            (-40, "VmbErrorRetriesExceeded"),
219            (-41, "VmbErrorInsufficientBufferCount"),
220            (1, "VmbErrorCustom"),
221        ];
222        for (code, name) in expected {
223            assert_eq!(error_name(*code), *name, "wrong name for code {code}");
224        }
225        // A known gap in the SDK's enum (code `-38` is unused) must
226        // fall through to the catch-all.
227        assert_eq!(error_name(-38), "VmbErrorUnrecognized");
228    }
229
230    #[test]
231    fn display_includes_error_name() {
232        let err = VmbError::Sdk {
233            code: -4,
234            message: "bad handle".to_string(),
235        };
236        let s = format!("{err}");
237        assert!(s.contains("VmbErrorBadHandle"));
238        assert!(s.contains("bad handle"));
239    }
240
241    #[test]
242    fn check_success_is_ok() {
243        assert!(check(0).is_ok());
244    }
245
246    #[test]
247    fn check_error_is_sdk_with_code_and_name() {
248        match check(-4) {
249            Err(VmbError::Sdk { code, message }) => {
250                assert_eq!(code, -4);
251                assert!(message.contains("VmbErrorBadHandle"));
252            }
253            other => panic!("expected Err(Sdk), got {other:?}"),
254        }
255    }
256
257    #[test]
258    fn display_invalid_string_includes_context() {
259        let err = VmbError::InvalidString {
260            context: "camera_id",
261        };
262        assert!(format!("{err}").contains("camera_id"));
263    }
264
265    #[test]
266    fn display_frame_too_small_includes_counts() {
267        let err = VmbError::FrameTooSmall {
268            expected: 100,
269            actual: 80,
270        };
271        let s = format!("{err}");
272        assert!(s.contains("100"));
273        assert!(s.contains("80"));
274    }
275
276    #[test]
277    fn display_io_includes_path() {
278        let err = VmbError::Io {
279            path: PathBuf::from("/tmp/does-not-exist.xml"),
280            source: std::io::Error::other("boom"),
281        };
282        let s = format!("{err}");
283        assert!(s.contains("does-not-exist.xml"));
284    }
285
286    #[test]
287    fn already_started_and_not_started_display() {
288        assert!(format!("{}", VmbError::AlreadyStarted).contains("already started"));
289        assert!(format!("{}", VmbError::NotStarted).contains("not been started"));
290    }
291
292    #[test]
293    fn capture_already_running_display() {
294        assert!(format!("{}", VmbError::CaptureAlreadyRunning).contains("already running"));
295    }
296
297    #[test]
298    fn load_failed_display_includes_message() {
299        let err = VmbError::LoadFailed {
300            message: "libVmbC.so: cannot open shared object file".to_string(),
301        };
302        let s = format!("{err}");
303        assert!(s.contains("libVmbC.so"), "loader message missing from {s}");
304        assert!(s.contains("load"), "error descriptor missing from {s}");
305    }
306}