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