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