Skip to main content

hadris_io/
lib.rs

1//! # Hadris IO
2//!
3//! Portable I/O trait abstractions for the Hadris filesystem crates.
4//!
5//! This crate provides [`Read`], [`Write`], and [`Seek`] traits that work in
6//! both `std` and `no_std` environments. When the `std` feature is enabled,
7//! the traits re-export directly from `std::io`. In `no_std` mode, minimal
8//! custom trait definitions are provided with the same API surface.
9//!
10//! ## Feature Flags
11//!
12//! | Feature | Default | Description |
13//! |---------|---------|-------------|
14//! | `std`   | yes     | Standard library support (implies `alloc`) |
15//! | `sync`  | yes     | Synchronous I/O traits |
16//! | `async` | no      | Asynchronous I/O traits (uses async fn in trait) |
17//! | `alloc` | via `std` | Heap-backed error messages without `std` |
18//!
19//! `std` and the I/O mode are independent. The default feature set enables
20//! both `std` and `sync`; custom builds can select `sync`, `async`, or both.
21//!
22//! ## Quick Start
23//!
24//! ```rust
25//! use hadris_io::{Cursor, SeekFrom, Read, Seek};
26//!
27//! let data = [0x48, 0x44, 0x52, 0x53]; // "HDRS"
28//! let mut cursor = Cursor::new(&data);
29//!
30//! let mut buf = [0u8; 2];
31//! cursor.read_exact(&mut buf).unwrap();
32//! assert_eq!(&buf, b"HD");
33//!
34//! cursor.seek(SeekFrom::Start(0)).unwrap();
35//! cursor.read_exact(&mut buf).unwrap();
36//! assert_eq!(&buf, b"HD");
37//! ```
38//!
39//! ## Cursor
40//!
41//! The [`Cursor`] type wraps a byte slice and provides both [`Read`] and
42//! [`Seek`] implementations, useful for in-memory parsing:
43//!
44//! ```rust
45//! use hadris_io::Cursor;
46//!
47//! let data = b"Hello, Hadris!";
48//! let mut cursor = Cursor::new(data);
49//! assert_eq!(cursor.position(), 0);
50//! cursor.set_position(7);
51//! assert_eq!(cursor.position(), 7);
52//! ```
53//!
54//! ## Extension Traits
55//!
56//! The [`ReadExt`] trait adds structured reading via [`bytemuck`]:
57//!
58//! ```rust
59//! use hadris_io::{Cursor, ReadExt};
60//!
61//! let bytes = 0x1234u16.to_ne_bytes();
62//! let mut cursor = Cursor::new(&bytes);
63//! let value: u16 = cursor.read_struct().unwrap();
64//! assert_eq!(value, 0x1234);
65//! ```
66
67#![no_std]
68#![deny(missing_docs)]
69#![allow(async_fn_in_trait)]
70
71#[cfg(feature = "std")]
72extern crate std;
73
74// ---------------------------------------------------------------------------
75// Shared types (always available)
76// ---------------------------------------------------------------------------
77
78mod error;
79pub use error::{Error, ErrorKind, Result};
80
81/// Re-export std path types when std is available.
82#[cfg(feature = "std")]
83pub use std::path::{Path, PathBuf};
84
85/// Portable seek position, convertible to and from `std::io::SeekFrom`.
86pub use embedded_io::SeekFrom;
87
88/// Error implemented by portable underlying I/O sources.
89pub trait IoError: embedded_io::Error {}
90impl<T: embedded_io::Error + ?Sized> IoError for T {}
91
92/// Helper macro: short-circuit an `Err` by returning `Some(Err(..))`.
93///
94/// Useful in iterator implementations where the return type is
95/// `Option<Result<T>>`. Extracts the `Ok` value, or returns
96/// `Some(Err(..))` immediately on error.
97///
98/// # Example
99///
100/// ```rust
101/// use hadris_io::{try_io_result_option, Result, Error, ErrorKind};
102///
103/// fn next_item(ok: bool) -> Option<Result<u32>> {
104///     let result: Result<u32> = if ok {
105///         Ok(42)
106///     } else {
107///         Err(Error::new(ErrorKind::NotFound, "missing"))
108///     };
109///     let value = try_io_result_option!(result);
110///     Some(Ok(value * 2))
111/// }
112///
113/// assert!(matches!(next_item(true), Some(Ok(84))));
114/// assert!(matches!(next_item(false), Some(Err(_))));
115/// ```
116#[macro_export]
117macro_rules! try_io_result_option {
118    ($expr:expr) => {
119        match $expr {
120            Ok(val) => val,
121            Err(err) => return Some(Err(err.erase())),
122        }
123    };
124}
125
126// ---------------------------------------------------------------------------
127// Cursor (shared, works with both sync and async)
128// ---------------------------------------------------------------------------
129
130/// A no-std compatible Cursor for reading from byte slices.
131///
132/// Wraps a `&[u8]` and tracks a read position, implementing both
133/// [`sync::Read`] and [`sync::Seek`] (when the `sync` feature is enabled).
134///
135/// # Example
136///
137/// ```rust
138/// use hadris_io::{Cursor, Read, Seek, SeekFrom};
139///
140/// let data = [1u8, 2, 3, 4, 5];
141/// let mut cursor = Cursor::new(&data);
142///
143/// let mut buf = [0u8; 2];
144/// cursor.read_exact(&mut buf).unwrap();
145/// assert_eq!(buf, [1, 2]);
146///
147/// cursor.seek(SeekFrom::Start(0)).unwrap();
148/// cursor.read_exact(&mut buf).unwrap();
149/// assert_eq!(buf, [1, 2]);
150/// ```
151#[derive(Debug, Clone)]
152pub struct Cursor<'a> {
153    data: &'a [u8],
154    cursor: usize,
155}
156
157impl<'a> Cursor<'a> {
158    /// Creates a new cursor wrapping the given byte slice, starting at position 0.
159    ///
160    /// ```rust
161    /// use hadris_io::Cursor;
162    ///
163    /// let data = [1, 2, 3];
164    /// let cursor = Cursor::new(&data);
165    /// assert_eq!(cursor.position(), 0);
166    /// assert_eq!(cursor.get_ref().len(), 3);
167    /// ```
168    pub fn new(data: &'a [u8]) -> Self {
169        Self { data, cursor: 0 }
170    }
171
172    /// Returns the current byte offset within the underlying data.
173    ///
174    /// ```rust
175    /// use hadris_io::Cursor;
176    ///
177    /// let mut cursor = Cursor::new(&[0u8; 10]);
178    /// assert_eq!(cursor.position(), 0);
179    /// cursor.set_position(5);
180    /// assert_eq!(cursor.position(), 5);
181    /// ```
182    pub fn position(&self) -> usize {
183        self.cursor
184    }
185
186    /// Sets the cursor position to the given byte offset.
187    ///
188    /// ```rust
189    /// use hadris_io::Cursor;
190    ///
191    /// let mut cursor = Cursor::new(&[0u8; 10]);
192    /// cursor.set_position(7);
193    /// assert_eq!(cursor.position(), 7);
194    /// ```
195    pub fn set_position(&mut self, pos: usize) {
196        self.cursor = pos;
197    }
198
199    /// Returns a reference to the underlying byte slice.
200    ///
201    /// ```rust
202    /// use hadris_io::Cursor;
203    ///
204    /// let data = [1, 2, 3];
205    /// let cursor = Cursor::new(&data);
206    /// assert_eq!(cursor.get_ref(), &[1, 2, 3]);
207    /// ```
208    pub fn get_ref(&self) -> &'a [u8] {
209        self.data
210    }
211
212    #[cfg(any(feature = "sync", feature = "async", test))]
213    fn read_impl(&mut self, buf: &mut [u8]) -> core::result::Result<usize, ErrorKind> {
214        let remaining = self.data.len().saturating_sub(self.cursor);
215        let to_read = buf.len().min(remaining);
216        if to_read > 0 {
217            buf[..to_read].copy_from_slice(&self.data[self.cursor..self.cursor + to_read]);
218            self.cursor += to_read;
219        }
220        Ok(to_read)
221    }
222
223    #[cfg(any(feature = "sync", feature = "async", test))]
224    fn seek_impl(&mut self, pos: SeekFrom) -> core::result::Result<u64, ErrorKind> {
225        let new_pos = match pos {
226            SeekFrom::Start(offset) => offset as i64,
227            SeekFrom::End(offset) => self.data.len() as i64 + offset,
228            SeekFrom::Current(offset) => self.cursor as i64 + offset,
229        };
230
231        if new_pos < 0 {
232            return Err(ErrorKind::InvalidInput);
233        }
234
235        self.cursor = new_pos as usize;
236        Ok(self.cursor as u64)
237    }
238}
239
240// ---------------------------------------------------------------------------
241// Sync module
242// ---------------------------------------------------------------------------
243
244#[cfg(feature = "sync")]
245mod sync_api;
246
247/// Synchronous I/O traits.
248///
249/// Contains [`Read`], [`Write`], [`Seek`],
250/// plus extension traits [`ReadExt`], [`Parsable`],
251/// [`Writable`].
252#[cfg(feature = "sync")]
253pub mod sync {
254    pub use super::sync_api::*;
255}
256
257// Cursor: sync trait impls
258#[cfg(feature = "sync")]
259impl sync::Read for Cursor<'_> {
260    type Error = ErrorKind;
261
262    fn read(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error> {
263        self.read_impl(buf).map_err(Error::from_source)
264    }
265}
266
267#[cfg(feature = "sync")]
268impl sync::Seek for Cursor<'_> {
269    type Error = ErrorKind;
270
271    fn seek(&mut self, pos: SeekFrom) -> Result<u64> {
272        self.seek_impl(pos).map_err(Error::from_source)
273    }
274}
275
276// Default re-export for backwards compatibility
277#[cfg(feature = "sync")]
278pub use sync::*;
279
280// ---------------------------------------------------------------------------
281// Async module
282// ---------------------------------------------------------------------------
283
284#[cfg(feature = "async")]
285mod async_api;
286
287/// Asynchronous I/O traits (using async fn in trait).
288///
289/// Contains async versions of `Read`, `Write`, and `Seek`,
290/// plus async extension traits.
291#[cfg(feature = "async")]
292pub mod r#async {
293    pub use super::async_api::*;
294}
295
296// Cursor: async trait impls
297#[cfg(feature = "async")]
298impl r#async::Read for Cursor<'_> {
299    type Error = ErrorKind;
300
301    async fn read(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error> {
302        self.read_impl(buf).map_err(Error::from_source)
303    }
304}
305
306#[cfg(feature = "async")]
307impl r#async::Seek for Cursor<'_> {
308    type Error = ErrorKind;
309
310    async fn seek(&mut self, pos: SeekFrom) -> Result<u64> {
311        self.seek_impl(pos).map_err(Error::from_source)
312    }
313}
314
315#[cfg(all(test, feature = "sync"))]
316mod tests {
317    extern crate std;
318    use super::*;
319    use std::format;
320
321    // -----------------------------------------------------------------------
322    // Cursor tests
323    // -----------------------------------------------------------------------
324
325    #[test]
326    fn cursor_new_starts_at_zero() {
327        let data = [1, 2, 3, 4, 5];
328        let cursor = Cursor::new(&data);
329        assert_eq!(cursor.position(), 0);
330        assert_eq!(cursor.get_ref(), &data);
331    }
332
333    #[test]
334    fn cursor_set_position() {
335        let data = [0u8; 10];
336        let mut cursor = Cursor::new(&data);
337        cursor.set_position(5);
338        assert_eq!(cursor.position(), 5);
339        cursor.set_position(0);
340        assert_eq!(cursor.position(), 0);
341    }
342
343    #[test]
344    fn cursor_read_basic() {
345        let data = [10, 20, 30, 40, 50];
346        let mut cursor = Cursor::new(&data);
347        let mut buf = [0u8; 3];
348        let n = cursor.read_impl(&mut buf).unwrap();
349        assert_eq!(n, 3);
350        assert_eq!(buf, [10, 20, 30]);
351        assert_eq!(cursor.position(), 3);
352    }
353
354    #[test]
355    fn cursor_read_past_end() {
356        let data = [1, 2];
357        let mut cursor = Cursor::new(&data);
358        let mut buf = [0u8; 5];
359        let n = cursor.read_impl(&mut buf).unwrap();
360        assert_eq!(n, 2);
361        assert_eq!(&buf[..2], &[1, 2]);
362        assert_eq!(cursor.position(), 2);
363
364        // Reading again at end returns 0
365        let n = cursor.read_impl(&mut buf).unwrap();
366        assert_eq!(n, 0);
367    }
368
369    #[test]
370    fn cursor_read_empty_buffer() {
371        let data = [1, 2, 3];
372        let mut cursor = Cursor::new(&data);
373        let mut buf = [0u8; 0];
374        let n = cursor.read_impl(&mut buf).unwrap();
375        assert_eq!(n, 0);
376        assert_eq!(cursor.position(), 0);
377    }
378
379    #[test]
380    fn cursor_seek_start() {
381        let data = [0u8; 20];
382        let mut cursor = Cursor::new(&data);
383        let pos = cursor.seek_impl(SeekFrom::Start(10)).unwrap();
384        assert_eq!(pos, 10);
385        assert_eq!(cursor.position(), 10);
386    }
387
388    #[test]
389    fn cursor_seek_end() {
390        let data = [0u8; 20];
391        let mut cursor = Cursor::new(&data);
392        let pos = cursor.seek_impl(SeekFrom::End(-5)).unwrap();
393        assert_eq!(pos, 15);
394        assert_eq!(cursor.position(), 15);
395    }
396
397    #[test]
398    fn cursor_seek_current() {
399        let data = [0u8; 20];
400        let mut cursor = Cursor::new(&data);
401        cursor.set_position(10);
402        let pos = cursor.seek_impl(SeekFrom::Current(3)).unwrap();
403        assert_eq!(pos, 13);
404        let pos = cursor.seek_impl(SeekFrom::Current(-5)).unwrap();
405        assert_eq!(pos, 8);
406    }
407
408    #[test]
409    fn cursor_seek_negative_position_errors() {
410        let data = [0u8; 10];
411        let mut cursor = Cursor::new(&data);
412        let result = cursor.seek_impl(SeekFrom::End(-20));
413        assert!(result.is_err());
414        let err = result.unwrap_err();
415        assert_eq!(err.kind(), ErrorKind::InvalidInput);
416    }
417
418    #[test]
419    fn cursor_seek_to_start_of_stream() {
420        let data = [0u8; 10];
421        let mut cursor = Cursor::new(&data);
422        cursor.set_position(5);
423        let pos = cursor.seek_impl(SeekFrom::Start(0)).unwrap();
424        assert_eq!(pos, 0);
425    }
426
427    #[test]
428    fn cursor_clone() {
429        let data = [1, 2, 3, 4, 5];
430        let mut cursor = Cursor::new(&data);
431        cursor.set_position(3);
432        let clone = cursor.clone();
433        assert_eq!(clone.position(), 3);
434        assert_eq!(clone.get_ref(), cursor.get_ref());
435    }
436
437    #[test]
438    fn cursor_debug_format() {
439        let data = [1, 2, 3];
440        let cursor = Cursor::new(&data);
441        let debug = format!("{cursor:?}");
442        assert!(debug.contains("Cursor"));
443    }
444
445    // -----------------------------------------------------------------------
446    // Sync Read/Seek trait tests via Cursor
447    // -----------------------------------------------------------------------
448
449    #[test]
450    fn sync_read_trait() {
451        use sync::Read;
452        let data = [10, 20, 30, 40, 50];
453        let mut cursor = Cursor::new(&data);
454        let mut buf = [0u8; 3];
455        let n = cursor.read(&mut buf).unwrap();
456        assert_eq!(n, 3);
457        assert_eq!(buf, [10, 20, 30]);
458    }
459
460    #[test]
461    fn sync_read_exact_success() {
462        use sync::Read;
463        let data = [1, 2, 3, 4, 5];
464        let mut cursor = Cursor::new(&data);
465        let mut buf = [0u8; 5];
466        cursor.read_exact(&mut buf).unwrap();
467        assert_eq!(buf, [1, 2, 3, 4, 5]);
468    }
469
470    #[test]
471    fn sync_read_exact_eof() {
472        use sync::Read;
473        let data = [1, 2];
474        let mut cursor = Cursor::new(&data);
475        let mut buf = [0u8; 5];
476        let result = cursor.read_exact(&mut buf);
477        assert!(result.is_err());
478    }
479
480    #[test]
481    fn sync_seek_trait() {
482        use sync::Seek;
483        let data = [0u8; 20];
484        let mut cursor = Cursor::new(&data);
485        let pos = cursor.seek(SeekFrom::Start(10)).unwrap();
486        assert_eq!(pos, 10);
487        let pos = cursor.stream_position().unwrap();
488        assert_eq!(pos, 10);
489    }
490
491    #[test]
492    fn sync_seek_relative() {
493        use sync::Seek;
494        let data = [0u8; 20];
495        let mut cursor = Cursor::new(&data);
496        cursor.seek(SeekFrom::Start(5)).unwrap();
497        cursor.seek_relative(3).unwrap();
498        assert_eq!(cursor.stream_position().unwrap(), 8);
499        cursor.seek_relative(-2).unwrap();
500        assert_eq!(cursor.stream_position().unwrap(), 6);
501    }
502
503    // -----------------------------------------------------------------------
504    // ReadExt tests
505    // -----------------------------------------------------------------------
506
507    #[test]
508    fn read_ext_read_struct() {
509        use sync::ReadExt;
510        let data = [0x78, 0x56, 0x34, 0x12]; // LE u32 = 0x12345678
511        let mut cursor = Cursor::new(&data);
512        let val: u32 = cursor.read_struct().unwrap();
513        assert_eq!(val, u32::from_ne_bytes([0x78, 0x56, 0x34, 0x12]));
514    }
515
516    #[test]
517    fn read_ext_read_struct_eof() {
518        use sync::ReadExt;
519        let data = [0x78, 0x56]; // Only 2 bytes, not enough for u32
520        let mut cursor = Cursor::new(&data);
521        let result: Result<u32> = cursor.read_struct();
522        assert!(result.is_err());
523    }
524
525    // -----------------------------------------------------------------------
526    // try_io_result_option! macro tests
527    // -----------------------------------------------------------------------
528
529    #[test]
530    fn try_io_result_option_ok() {
531        fn test_fn() -> Option<Result<u32>> {
532            let val: Result<u32> = Ok(42);
533            let v = try_io_result_option!(val);
534            Some(Ok(v))
535        }
536        let result = test_fn();
537        assert!(matches!(result, Some(Ok(42))));
538    }
539
540    #[test]
541    fn try_io_result_option_err() {
542        fn test_fn() -> Option<Result<u32>> {
543            let val: Result<u32> = Err(Error::new(ErrorKind::NotFound, "not found"));
544            let _v = try_io_result_option!(val);
545            Some(Ok(0)) // Should not reach here
546        }
547        let result = test_fn();
548        assert!(matches!(result, Some(Err(_))));
549    }
550}