ibu/
error.rs

1//! Error handling for the IBU library.
2//!
3//! This module defines all error types that can occur during IBU file operations,
4//! including I/O errors, format validation errors, and processing errors.
5
6use std::error::Error as StdError;
7use thiserror::Error;
8
9/// A specialized `Result` type for IBU operations.
10///
11/// This type is used throughout the IBU library for any operation that can fail.
12/// It's equivalent to `std::result::Result<T, IbuError>`.
13///
14/// # Examples
15///
16/// ```rust
17/// use ibu::{Header, Result};
18///
19/// fn create_header() -> Result<Header> {
20///     let header = Header::new(16, 12);
21///     header.validate()?;
22///     Ok(header)
23/// }
24/// ```
25pub type Result<T> = std::result::Result<T, IbuError>;
26
27/// Error types for IBU operations.
28///
29/// This enum covers all possible error conditions that can occur when reading,
30/// writing, or processing IBU files. Each variant provides specific context
31/// about what went wrong to help with debugging and error handling.
32///
33/// # Examples
34///
35/// ```rust
36/// use ibu::{IbuError, Reader};
37/// use std::io::Cursor;
38///
39/// // Handle specific error types
40/// let invalid_data = vec![0u8; 32];
41/// let cursor = Cursor::new(invalid_data);
42///
43/// match Reader::new(cursor) {
44///     Err(IbuError::InvalidMagicNumber { expected, actual }) => {
45///         println!("Wrong file type: expected {:#x}, got {:#x}", expected, actual);
46///     },
47///     Err(IbuError::Io(io_err)) => {
48///         println!("I/O error: {}", io_err);
49///     },
50///     Err(e) => {
51///         println!("Other error: {}", e);
52///     },
53///     Ok(_) => unreachable!(),
54/// }
55/// ```
56#[derive(Error, Debug)]
57pub enum IbuError {
58    /// I/O error from the underlying reader or writer.
59    ///
60    /// This wraps standard I/O errors that can occur when reading from or
61    /// writing to files, network streams, or other I/O sources.
62    #[error("I/O error")]
63    Io(#[from] std::io::Error),
64
65    /// Compression/decompression error from niffler.
66    ///
67    /// This occurs when there are problems with compressed file formats
68    /// like gzip or zstd when the `niffler` feature is enabled.
69    #[error("Niffler error")]
70    Niffler(#[from] niffler::Error),
71
72    /// Invalid magic number in file header.
73    ///
74    /// The file doesn't start with the expected IBU magic number (0x21554249).
75    /// This usually indicates the file is not an IBU file or is corrupted.
76    #[error("Invalid magic number, expected ({expected:#x}), found ({actual:#x})")]
77    InvalidMagicNumber { expected: u32, actual: u32 },
78
79    /// Incomplete record data at the specified file position.
80    ///
81    /// This occurs when the file ends in the middle of a record, indicating
82    /// the file was truncated or corrupted during writing.
83    #[error("Truncated record at position {pos}")]
84    TruncatedRecord { pos: usize },
85
86    /// Unsupported file format version.
87    ///
88    /// The file was created with a different version of the IBU format
89    /// that is not supported by this library version.
90    #[error("Invalid version found, expected ({expected}), found ({actual})")]
91    InvalidVersion { expected: u32, actual: u32 },
92
93    /// Barcode length is outside the valid range (1-32).
94    ///
95    /// Barcode lengths must be between 1 and 32 bases due to the 2-bit
96    /// encoding scheme used in the format.
97    #[error("Invalid barcode length: {0} (must be 1-32)")]
98    InvalidBarcodeLength(u32),
99
100    /// UMI length is outside the valid range (1-32).
101    ///
102    /// UMI lengths must be between 1 and 32 bases due to the 2-bit
103    /// encoding scheme used in the format.
104    #[error("Invalid UMI length: {0} (must be 1-32)")]
105    InvalidUmiLength(u32),
106
107    /// File data size is not a multiple of the record size.
108    ///
109    /// This indicates the file is corrupted or was not written properly,
110    /// as all IBU files should contain complete 24-byte records after the header.
111    #[error("Invalid map size - not a multiple of record size")]
112    InvalidMapSize,
113
114    /// Array index is out of bounds.
115    ///
116    /// This occurs when trying to access records beyond the end of the file
117    /// or with invalid slice bounds in memory-mapped operations.
118    #[error("Invalid index ({idx}) - Must be less than {max}")]
119    InvalidIndex { idx: usize, max: usize },
120
121    /// Error occurred during parallel processing.
122    ///
123    /// This wraps errors that occur in user-defined parallel processors,
124    /// allowing custom error types to be propagated through the parallel
125    /// processing system.
126    #[error("Processing error: {0}")]
127    Process(Box<dyn StdError + Send + Sync>),
128}
129
130/// Trait for converting errors into `IbuError::Process` variants.
131///
132/// This trait provides a convenient way to convert custom error types
133/// into IBU errors for use in parallel processing contexts.
134///
135/// # Examples
136///
137/// ```rust
138/// use ibu::{IntoIbuError, IbuError};
139/// use std::fmt;
140///
141/// #[derive(Debug)]
142/// struct CustomError(String);
143///
144/// impl fmt::Display for CustomError {
145///     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146///         write!(f, "Custom error: {}", self.0)
147///     }
148/// }
149///
150/// impl std::error::Error for CustomError {}
151///
152/// // Convert to IbuError
153/// let custom_err = CustomError("something went wrong".to_string());
154/// let ibu_err = custom_err.into_ibu_error();
155///
156/// match ibu_err {
157///     IbuError::Process(_) => println!("Converted successfully"),
158///     _ => unreachable!(),
159/// }
160/// ```
161pub trait IntoIbuError {
162    /// Converts the error into an `IbuError`.
163    fn into_ibu_error(self) -> IbuError;
164}
165
166/// Blanket implementation for all error types.
167///
168/// Any type that implements `std::error::Error + Send + Sync + 'static`
169/// can be automatically converted to `IbuError::Process`.
170impl<E> IntoIbuError for E
171where
172    E: std::error::Error + Send + Sync + 'static,
173{
174    fn into_ibu_error(self) -> IbuError {
175        IbuError::Process(self.into())
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182    use std::fmt;
183
184    #[derive(Debug)]
185    struct CustomError(String);
186
187    impl fmt::Display for CustomError {
188        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189            write!(f, "Custom error: {}", self.0)
190        }
191    }
192
193    impl std::error::Error for CustomError {}
194
195    #[test]
196    fn test_error_display_messages() {
197        // Test InvalidMagicNumber
198        let err = IbuError::InvalidMagicNumber {
199            expected: 0x21554249,
200            actual: 0x12345678,
201        };
202        let display = format!("{}", err);
203        assert!(display.contains("0x21554249"));
204        assert!(display.contains("0x12345678"));
205
206        // Test InvalidVersion
207        let err = IbuError::InvalidVersion {
208            expected: 2,
209            actual: 1,
210        };
211        let display = format!("{}", err);
212        assert!(display.contains("expected (2)"));
213        assert!(display.contains("found (1)"));
214
215        // Test TruncatedRecord
216        let err = IbuError::TruncatedRecord { pos: 1024 };
217        let display = format!("{}", err);
218        assert!(display.contains("1024"));
219
220        // Test InvalidBarcodeLength
221        let err = IbuError::InvalidBarcodeLength(33);
222        let display = format!("{}", err);
223        assert!(display.contains("33"));
224        assert!(display.contains("1-32"));
225
226        // Test InvalidUmiLength
227        let err = IbuError::InvalidUmiLength(0);
228        let display = format!("{}", err);
229        assert!(display.contains("0"));
230        assert!(display.contains("1-32"));
231
232        // Test InvalidMapSize
233        let err = IbuError::InvalidMapSize;
234        let display = format!("{}", err);
235        assert!(display.contains("not a multiple"));
236
237        // Test InvalidIndex
238        let err = IbuError::InvalidIndex { idx: 100, max: 50 };
239        let display = format!("{}", err);
240        assert!(display.contains("100"));
241        assert!(display.contains("50"));
242
243        // Test Process error
244        let custom_err = CustomError("test error".to_string());
245        let err = IbuError::Process(custom_err.into());
246        let display = format!("{}", err);
247        assert!(display.contains("Processing error"));
248    }
249
250    #[test]
251    fn test_error_debug() {
252        let err = IbuError::InvalidMagicNumber {
253            expected: 0x21554249,
254            actual: 0x12345678,
255        };
256        let debug = format!("{:?}", err);
257        assert!(debug.contains("InvalidMagicNumber"));
258    }
259
260    #[test]
261    fn test_io_error_conversion() {
262        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
263        let ibu_err: IbuError = io_err.into();
264
265        match ibu_err {
266            IbuError::Io(inner) => {
267                assert_eq!(inner.kind(), std::io::ErrorKind::NotFound);
268            }
269            _ => panic!("Expected Io variant"),
270        }
271    }
272
273    #[cfg(feature = "niffler")]
274    #[test]
275    fn test_niffler_error_conversion() {
276        // This is a bit tricky to test without creating actual niffler errors
277        // but we can at least verify the type signature compiles by checking
278        // that the From trait is implemented
279        use std::any::TypeId;
280        assert_eq!(
281            TypeId::of::<niffler::Error>(),
282            TypeId::of::<niffler::Error>()
283        );
284    }
285
286    #[test]
287    fn test_into_ibu_error_trait() {
288        let custom_err = CustomError("test".to_string());
289        let ibu_err = custom_err.into_ibu_error();
290
291        match ibu_err {
292            IbuError::Process(boxed) => {
293                let display = format!("{}", boxed);
294                assert!(display.contains("Custom error: test"));
295            }
296            _ => panic!("Expected Process variant"),
297        }
298    }
299
300    #[test]
301    fn test_result_type_alias() {
302        fn test_function() -> Result<i32> {
303            Ok(42)
304        }
305
306        fn failing_function() -> Result<i32> {
307            Err(IbuError::InvalidMapSize)
308        }
309
310        assert_eq!(test_function().unwrap(), 42);
311        assert!(failing_function().is_err());
312    }
313
314    #[test]
315    fn test_error_source_chain() {
316        let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "Access denied");
317        let ibu_err = IbuError::Io(io_err);
318
319        // Test that we can access the source error
320        let source = ibu_err.source();
321        assert!(source.is_some());
322
323        if let Some(source) = source {
324            let io_source = source.downcast_ref::<std::io::Error>();
325            assert!(io_source.is_some());
326            assert_eq!(
327                io_source.unwrap().kind(),
328                std::io::ErrorKind::PermissionDenied
329            );
330        }
331    }
332
333    #[test]
334    fn test_error_send_sync() {
335        // Ensure our error type is Send + Sync for threading
336        fn is_send<T: Send>() {}
337        fn is_sync<T: Sync>() {}
338
339        is_send::<IbuError>();
340        is_sync::<IbuError>();
341    }
342
343    #[test]
344    fn test_custom_error_in_process() {
345        #[derive(Debug)]
346        struct ThreadError {
347            thread_id: usize,
348            message: String,
349        }
350
351        impl fmt::Display for ThreadError {
352            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
353                write!(f, "Thread {} error: {}", self.thread_id, self.message)
354            }
355        }
356
357        impl std::error::Error for ThreadError {}
358
359        let thread_err = ThreadError {
360            thread_id: 3,
361            message: "Processing failed".to_string(),
362        };
363
364        let ibu_err = thread_err.into_ibu_error();
365        let display = format!("{}", ibu_err);
366
367        assert!(display.contains("Thread 3 error"));
368        assert!(display.contains("Processing failed"));
369    }
370}