Skip to main content

quack_rs/
error.rs

1// SPDX-License-Identifier: MIT
2// Copyright 2026 Tom F. <https://github.com/tomtom215/>
3// My way of giving something small back to the open source community
4// and encouraging more Rust development!
5
6//! Error types for `DuckDB` extension FFI error propagation.
7//!
8//! [`ExtensionError`] is the primary error type. It implements [`std::error::Error`],
9//! can be constructed from `&str` or `String`, and converts to a `CString` for
10//! passing to `access.set_error`.
11//!
12//! # Example
13//!
14//! ```rust
15//! use quack_rs::error::ExtensionError;
16//!
17//! let err = ExtensionError::from("Failed to register function");
18//! assert_eq!(err.to_string(), "Failed to register function");
19//! ```
20
21use std::ffi::CString;
22use std::fmt;
23
24/// An error that can occur during `DuckDB` extension initialization or registration.
25///
26/// This type is designed for use with the `?` operator inside the extension
27/// entry point. It can be reported back to `DuckDB` via `access.set_error`.
28///
29/// # Construction
30///
31/// ```rust
32/// use quack_rs::error::ExtensionError;
33///
34/// // From a string literal
35/// let e = ExtensionError::from("something went wrong");
36///
37/// // From a String
38/// let msg = format!("failed: {}", 42);
39/// let e = ExtensionError::from(msg);
40///
41/// // Wrapping another error
42/// let parse_err: Result<i32, _> = "not a number".parse();
43/// let e = parse_err.map_err(ExtensionError::from_error);
44/// ```
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct ExtensionError {
47    message: String,
48}
49
50impl ExtensionError {
51    /// Creates a new `ExtensionError` with the given message.
52    ///
53    /// # Example
54    ///
55    /// ```rust
56    /// use quack_rs::error::ExtensionError;
57    ///
58    /// let err = ExtensionError::new("registration failed");
59    /// assert_eq!(err.to_string(), "registration failed");
60    /// ```
61    #[inline]
62    pub fn new(message: impl Into<String>) -> Self {
63        Self {
64            message: message.into(),
65        }
66    }
67
68    /// Wraps any `std::error::Error` into an `ExtensionError`.
69    ///
70    /// # Example
71    ///
72    /// ```rust
73    /// use quack_rs::error::ExtensionError;
74    ///
75    /// let result: Result<i32, _> = "abc".parse::<i32>();
76    /// let err = result.map_err(ExtensionError::from_error);
77    /// assert!(err.is_err());
78    /// ```
79    #[inline]
80    pub fn from_error<E: std::error::Error>(e: E) -> Self {
81        Self {
82            message: e.to_string(),
83        }
84    }
85
86    /// Converts this error into a `CString` suitable for passing to `set_error`.
87    ///
88    /// If the message contains a null byte (which is valid in a Rust `String` but
89    /// not in a C string), the message is truncated at the first null byte.
90    ///
91    /// # Example
92    ///
93    /// ```rust
94    /// use quack_rs::error::ExtensionError;
95    ///
96    /// let err = ExtensionError::new("oops");
97    /// let cstr = err.to_c_string();
98    /// assert_eq!(cstr.to_str().unwrap(), "oops");
99    /// ```
100    #[must_use]
101    pub fn to_c_string(&self) -> CString {
102        CString::new(self.message.as_bytes()).unwrap_or_else(|_| {
103            // Truncate at the first null byte to produce a valid C string.
104            // No panic: if CString::new fails again (logically impossible since
105            // we truncate at the first null byte), fall back to a generic message.
106            let pos = self
107                .message
108                .bytes()
109                .position(|b| b == 0)
110                .unwrap_or(self.message.len());
111            CString::new(&self.message.as_bytes()[..pos]).unwrap_or_else(|_| {
112                // Defensive fallback — should never be reached.
113                CString::new("extension error (message contained null bytes)")
114                    .unwrap_or_else(|_| CString::default())
115            })
116        })
117    }
118
119    /// Returns the error message as a string slice.
120    ///
121    /// # Example
122    ///
123    /// ```rust
124    /// use quack_rs::error::ExtensionError;
125    ///
126    /// let err = ExtensionError::new("bad input");
127    /// assert_eq!(err.as_str(), "bad input");
128    /// ```
129    #[must_use]
130    #[inline]
131    pub fn as_str(&self) -> &str {
132        &self.message
133    }
134}
135
136impl fmt::Display for ExtensionError {
137    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138        f.write_str(&self.message)
139    }
140}
141
142impl std::error::Error for ExtensionError {}
143
144impl From<&str> for ExtensionError {
145    #[inline]
146    fn from(s: &str) -> Self {
147        Self::new(s)
148    }
149}
150
151impl From<String> for ExtensionError {
152    #[inline]
153    fn from(s: String) -> Self {
154        Self { message: s }
155    }
156}
157
158impl From<Box<dyn std::error::Error>> for ExtensionError {
159    #[inline]
160    fn from(e: Box<dyn std::error::Error>) -> Self {
161        Self {
162            message: e.to_string(),
163        }
164    }
165}
166
167impl From<Box<dyn std::error::Error + Send + Sync>> for ExtensionError {
168    #[inline]
169    fn from(e: Box<dyn std::error::Error + Send + Sync>) -> Self {
170        Self {
171            message: e.to_string(),
172        }
173    }
174}
175
176impl From<std::io::Error> for ExtensionError {
177    #[inline]
178    fn from(e: std::io::Error) -> Self {
179        Self {
180            message: e.to_string(),
181        }
182    }
183}
184
185impl From<std::ffi::NulError> for ExtensionError {
186    #[inline]
187    fn from(e: std::ffi::NulError) -> Self {
188        Self {
189            message: e.to_string(),
190        }
191    }
192}
193
194impl From<std::fmt::Error> for ExtensionError {
195    #[inline]
196    fn from(e: std::fmt::Error) -> Self {
197        Self {
198            message: e.to_string(),
199        }
200    }
201}
202
203/// Convenience type alias for `Result<T, ExtensionError>`.
204pub type ExtResult<T> = Result<T, ExtensionError>;
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn new_creates_with_message() {
212        let err = ExtensionError::new("test error");
213        assert_eq!(err.to_string(), "test error");
214        assert_eq!(err.as_str(), "test error");
215    }
216
217    #[test]
218    fn from_str() {
219        let err = ExtensionError::from("from str");
220        assert_eq!(err.message, "from str");
221    }
222
223    #[test]
224    fn from_string() {
225        let s = String::from("from String");
226        let err = ExtensionError::from(s);
227        assert_eq!(err.message, "from String");
228    }
229
230    #[test]
231    fn from_error_wraps_display() {
232        let parse_err = "abc".parse::<i32>().unwrap_err();
233        let err = ExtensionError::from_error(parse_err);
234        assert!(!err.message.is_empty());
235    }
236
237    #[test]
238    fn to_c_string_normal() {
239        let err = ExtensionError::new("hello world");
240        let cstr = err.to_c_string();
241        assert_eq!(cstr.to_str().unwrap(), "hello world");
242    }
243
244    #[test]
245    fn to_c_string_with_null_byte() {
246        // A message with an embedded null byte should be truncated at the null
247        let err = ExtensionError::new("before\0after");
248        let cstr = err.to_c_string();
249        assert_eq!(cstr.to_str().unwrap(), "before");
250    }
251
252    #[test]
253    fn to_c_string_empty() {
254        let err = ExtensionError::new("");
255        let cstr = err.to_c_string();
256        assert_eq!(cstr.to_str().unwrap(), "");
257    }
258
259    #[test]
260    fn display_impl() {
261        let err = ExtensionError::new("display test");
262        let s = format!("{err}");
263        assert_eq!(s, "display test");
264    }
265
266    #[test]
267    fn debug_impl() {
268        let err = ExtensionError::new("debug");
269        let s = format!("{err:?}");
270        assert!(s.contains("debug"));
271    }
272
273    #[test]
274    fn clone_eq() {
275        let err1 = ExtensionError::new("clone test");
276        let err2 = err1.clone();
277        assert_eq!(err1, err2);
278    }
279
280    #[test]
281    fn from_box_dyn_error() {
282        let boxed: Box<dyn std::error::Error> = "abc".parse::<i32>().unwrap_err().into();
283        let err = ExtensionError::from(boxed);
284        assert!(!err.message.is_empty());
285    }
286
287    #[test]
288    fn question_mark_operator_with_str() {
289        fn fails() -> Result<(), ExtensionError> {
290            Err("explicit error")?;
291            Ok(())
292        }
293        assert_eq!(fails().unwrap_err().as_str(), "explicit error");
294    }
295
296    #[test]
297    fn from_io_error() {
298        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
299        let err = ExtensionError::from(io_err);
300        assert_eq!(err.as_str(), "file not found");
301    }
302
303    #[test]
304    fn question_mark_with_io_error() {
305        fn fails() -> Result<(), ExtensionError> {
306            Err(std::io::Error::other("runtime init failed"))?;
307            Ok(())
308        }
309        assert_eq!(fails().unwrap_err().as_str(), "runtime init failed");
310    }
311
312    #[test]
313    fn from_nul_error() {
314        let nul_err = std::ffi::CString::new("hello\0world").unwrap_err();
315        let err = ExtensionError::from(nul_err);
316        assert!(!err.as_str().is_empty());
317    }
318
319    #[test]
320    fn from_fmt_error() {
321        let fmt_err = std::fmt::Error;
322        let err = ExtensionError::from(fmt_err);
323        assert!(!err.as_str().is_empty());
324    }
325
326    #[test]
327    fn to_c_string_leading_null_byte() {
328        // A message starting with null should truncate to empty string
329        let err = ExtensionError::new("\0trailing");
330        let cstr = err.to_c_string();
331        assert_eq!(cstr.to_str().unwrap(), "");
332    }
333
334    #[test]
335    fn to_c_string_multiple_null_bytes() {
336        let err = ExtensionError::new("first\0second\0third");
337        let cstr = err.to_c_string();
338        assert_eq!(cstr.to_str().unwrap(), "first");
339    }
340
341    #[test]
342    fn from_box_dyn_error_send_sync() {
343        let boxed: Box<dyn std::error::Error + Send + Sync> =
344            "abc".parse::<i32>().unwrap_err().into();
345        let err = ExtensionError::from(boxed);
346        assert!(!err.message.is_empty());
347    }
348
349    #[test]
350    fn ext_result_alias() {
351        // Verify ExtResult<T> alias works as expected
352        let ok_val: ExtResult<i32> = Ok(42);
353        assert!(ok_val.is_ok());
354
355        let err_val: ExtResult<i32> = Err(ExtensionError::new("fail"));
356        assert!(err_val.is_err());
357    }
358}