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}