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
176/// Convenience type alias for `Result<T, ExtensionError>`.
177pub type ExtResult<T> = Result<T, ExtensionError>;
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn new_creates_with_message() {
185        let err = ExtensionError::new("test error");
186        assert_eq!(err.to_string(), "test error");
187        assert_eq!(err.as_str(), "test error");
188    }
189
190    #[test]
191    fn from_str() {
192        let err = ExtensionError::from("from str");
193        assert_eq!(err.message, "from str");
194    }
195
196    #[test]
197    fn from_string() {
198        let s = String::from("from String");
199        let err = ExtensionError::from(s);
200        assert_eq!(err.message, "from String");
201    }
202
203    #[test]
204    fn from_error_wraps_display() {
205        let parse_err = "abc".parse::<i32>().unwrap_err();
206        let err = ExtensionError::from_error(parse_err);
207        assert!(!err.message.is_empty());
208    }
209
210    #[test]
211    fn to_c_string_normal() {
212        let err = ExtensionError::new("hello world");
213        let cstr = err.to_c_string();
214        assert_eq!(cstr.to_str().unwrap(), "hello world");
215    }
216
217    #[test]
218    fn to_c_string_with_null_byte() {
219        // A message with an embedded null byte should be truncated at the null
220        let err = ExtensionError::new("before\0after");
221        let cstr = err.to_c_string();
222        assert_eq!(cstr.to_str().unwrap(), "before");
223    }
224
225    #[test]
226    fn to_c_string_empty() {
227        let err = ExtensionError::new("");
228        let cstr = err.to_c_string();
229        assert_eq!(cstr.to_str().unwrap(), "");
230    }
231
232    #[test]
233    fn display_impl() {
234        let err = ExtensionError::new("display test");
235        let s = format!("{err}");
236        assert_eq!(s, "display test");
237    }
238
239    #[test]
240    fn debug_impl() {
241        let err = ExtensionError::new("debug");
242        let s = format!("{err:?}");
243        assert!(s.contains("debug"));
244    }
245
246    #[test]
247    fn clone_eq() {
248        let err1 = ExtensionError::new("clone test");
249        let err2 = err1.clone();
250        assert_eq!(err1, err2);
251    }
252
253    #[test]
254    fn from_box_dyn_error() {
255        let boxed: Box<dyn std::error::Error> = "abc".parse::<i32>().unwrap_err().into();
256        let err = ExtensionError::from(boxed);
257        assert!(!err.message.is_empty());
258    }
259
260    #[test]
261    fn question_mark_operator_with_str() {
262        fn fails() -> Result<(), ExtensionError> {
263            Err("explicit error")?;
264            Ok(())
265        }
266        assert_eq!(fails().unwrap_err().as_str(), "explicit error");
267    }
268}