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