Skip to main content

safe_read/
lib.rs

1#![no_std]
2#![forbid(unsafe_code)]
3
4//! Panic-free bounded integer readers over an untrusted byte slice.
5//!
6//! Every read returns a benign default (0 / `None`) when the requested window is out of
7//! range — never a panic. This is the shared front door for every offset/length field
8//! parsed from an attacker-controllable forensic image, so each reader crate does not
9//! re-derive its own bounds-checked helpers.
10//!
11//! Two flavours per width:
12//! - **`le_u32(data, off) -> u32`** — returns `0` out of range (the common case; the parser
13//!   then rejects the structurally-invalid record through its own validation).
14//! - **`try_le_u32(data, off) -> Option<u32>`** — returns `None` out of range, for the callers
15//!   that must distinguish a genuine `0` field from an absent/truncated one.
16//!
17//! ```
18//! use safe_read::{le_u32, be_u16, u8, try_le_u32};
19//! assert_eq!(le_u32(&[0x78, 0x56, 0x34, 0x12], 0), 0x1234_5678);
20//! assert_eq!(be_u16(&[0xaa, 0x12, 0x34], 1), 0x1234);
21//! assert_eq!(u8(&[0xab], 0), 0xab);
22//! // Out of range: 0 for the plain readers, None for the `try_` twins:
23//! assert_eq!(le_u32(&[1, 2, 3], 0), 0);
24//! assert_eq!(try_le_u32(&[1, 2, 3], 0), None);
25//! ```
26//!
27//! `#![no_std]` — pure slice arithmetic, no allocation.
28
29/// Define a fixed-width integer reader pair. The `try_` twin returns `None` when the window
30/// at `off` is not fully in range (too short, offset past EOF, or `off + width` overflowing
31/// `usize`); the plain reader unwraps that to `0`. Neither ever panics.
32macro_rules! bounded_reader {
33    ($name:ident, $try_name:ident, $ty:ty, $width:literal, $from_bytes:ident) => {
34        #[doc = concat!("Read a `", stringify!($ty), "` at `off`; `None` if out of range. Never panics. Use when `0` must be distinguished from absent/truncated.")]
35        #[must_use]
36        pub fn $try_name(data: &[u8], off: usize) -> Option<$ty> {
37            let end = off.checked_add($width)?;
38            let slice = data.get(off..end)?;
39            let mut buf = [0u8; $width];
40            buf.copy_from_slice(slice);
41            Some(<$ty>::$from_bytes(buf))
42        }
43
44        #[doc = concat!("Read a `", stringify!($ty), "` at `off`; `0` if out of range. Never panics.")]
45        #[must_use]
46        pub fn $name(data: &[u8], off: usize) -> $ty {
47            $try_name(data, off).unwrap_or(0)
48        }
49    };
50}
51
52bounded_reader!(be_u16, try_be_u16, u16, 2, from_be_bytes);
53bounded_reader!(be_u32, try_be_u32, u32, 4, from_be_bytes);
54bounded_reader!(be_u64, try_be_u64, u64, 8, from_be_bytes);
55bounded_reader!(le_u16, try_le_u16, u16, 2, from_le_bytes);
56bounded_reader!(le_u32, try_le_u32, u32, 4, from_le_bytes);
57bounded_reader!(le_u64, try_le_u64, u64, 8, from_le_bytes);
58
59/// Read a single byte at `off`; `None` if `off` is past the end. Never panics.
60#[must_use]
61pub fn try_u8(data: &[u8], off: usize) -> Option<u8> {
62    data.get(off).copied()
63}
64
65/// Read a single byte at `off`; `0` if `off` is past the end. Never panics. (Endianness is
66/// irrelevant for one byte; provided so callers never index `data[off]` directly.)
67#[must_use]
68pub fn u8(data: &[u8], off: usize) -> u8 {
69    try_u8(data, off).unwrap_or(0)
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn big_endian_reads_in_range() {
78        assert_eq!(be_u16(&[0x12, 0x34], 0), 0x1234);
79        assert_eq!(be_u32(&[0, 0, 1, 0], 0), 256);
80        assert_eq!(
81            be_u64(&[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08], 0),
82            0x0102_0304_0506_0708
83        );
84    }
85
86    #[test]
87    fn little_endian_reads_in_range() {
88        assert_eq!(le_u16(&[0x34, 0x12], 0), 0x1234);
89        assert_eq!(le_u32(&[0, 1, 0, 0], 0), 256);
90        assert_eq!(
91            le_u64(&[0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01], 0),
92            0x0102_0304_0506_0708
93        );
94    }
95
96    #[test]
97    fn reads_honor_offset() {
98        assert_eq!(be_u16(&[0xaa, 0x12, 0x34], 1), 0x1234);
99        assert_eq!(le_u32(&[0xff, 0xff, 0, 1, 0, 0], 2), 256);
100    }
101
102    #[test]
103    fn single_byte_reads() {
104        assert_eq!(u8(&[0xab, 0xcd], 0), 0xab);
105        assert_eq!(u8(&[0xab, 0xcd], 1), 0xcd);
106        assert_eq!(u8(&[0xab], 5), 0); // past end → 0
107        assert_eq!(u8(&[], 0), 0);
108        assert_eq!(try_u8(&[0xab], 0), Some(0xab));
109        assert_eq!(try_u8(&[0xab], 1), None);
110    }
111
112    #[test]
113    fn try_variants_distinguish_zero_from_absent() {
114        assert_eq!(try_le_u32(&[0, 0, 0, 0], 0), Some(0)); // genuine in-range 0
115        assert_eq!(try_le_u32(&[0, 0, 0], 0), None); // too short
116        assert_eq!(try_be_u16(&[1, 2], 2), None); // offset past window
117        assert_eq!(
118            try_be_u64(&[1, 2, 3, 4, 5, 6, 7, 8], 0),
119            Some(0x0102_0304_0506_0708)
120        );
121        assert_eq!(try_le_u16(&[], 0), None);
122    }
123
124    #[test]
125    fn out_of_range_returns_zero_never_panics() {
126        assert_eq!(be_u32(&[1, 2, 3], 0), 0);
127        assert_eq!(be_u64(&[1, 2, 3, 4, 5, 6, 7], 0), 0);
128        assert_eq!(be_u32(&[1, 2, 3, 4], 2), 0);
129        assert_eq!(le_u16(&[1, 2], 2), 0);
130        assert_eq!(be_u16(&[], 0), 0);
131        assert_eq!(le_u32(&[1, 2, 3, 4], 100), 0);
132    }
133
134    #[test]
135    fn offset_overflow_returns_zero() {
136        assert_eq!(be_u32(&[1, 2, 3, 4], usize::MAX), 0);
137        assert_eq!(try_be_u32(&[1, 2, 3, 4], usize::MAX), None);
138    }
139}