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//! Each width has a signed reader too (`le_i32`, `try_be_i64`, …), for the fields a format
18//! declares signed; and [`try_bytes`] copies out a fixed-width byte window (a GUID, a
19//! signature, a digest) under the same bounds rule.
20//!
21//! ```
22//! use safe_read::{le_u32, be_u16, u8, try_le_u32, le_i32, try_bytes};
23//! assert_eq!(le_u32(&[0x78, 0x56, 0x34, 0x12], 0), 0x1234_5678);
24//! assert_eq!(be_u16(&[0xaa, 0x12, 0x34], 1), 0x1234);
25//! assert_eq!(u8(&[0xab], 0), 0xab);
26//! // Signed fields come back negative, not as their huge unsigned twin:
27//! assert_eq!(le_i32(&[0xff, 0xff, 0xff, 0xff], 0), -1);
28//! assert_eq!(try_bytes::<2>(&[0xaa, 0xbb, 0xcc], 1), Some([0xbb, 0xcc]));
29//! // Out of range: 0 for the plain readers, None for the `try_` twins:
30//! assert_eq!(le_u32(&[1, 2, 3], 0), 0);
31//! assert_eq!(try_le_u32(&[1, 2, 3], 0), None);
32//! ```
33//!
34//! `#![no_std]` — pure slice arithmetic, no allocation.
35
36/// Define a fixed-width integer reader pair. The `try_` twin returns `None` when the window
37/// at `off` is not fully in range (too short, offset past EOF, or `off + width` overflowing
38/// `usize`); the plain reader unwraps that to `0`. Neither ever panics.
39macro_rules! bounded_reader {
40    ($name:ident, $try_name:ident, $ty:ty, $width:literal, $from_bytes:ident) => {
41        #[doc = concat!("Read a `", stringify!($ty), "` at `off`; `None` if out of range. Never panics. Use when `0` must be distinguished from absent/truncated.")]
42        #[must_use]
43        pub fn $try_name(data: &[u8], off: usize) -> Option<$ty> {
44            let end = off.checked_add($width)?;
45            let slice = data.get(off..end)?;
46            let mut buf = [0u8; $width];
47            buf.copy_from_slice(slice);
48            Some(<$ty>::$from_bytes(buf))
49        }
50
51        #[doc = concat!("Read a `", stringify!($ty), "` at `off`; `0` if out of range. Never panics.")]
52        #[must_use]
53        pub fn $name(data: &[u8], off: usize) -> $ty {
54            $try_name(data, off).unwrap_or(0)
55        }
56    };
57}
58
59bounded_reader!(be_u16, try_be_u16, u16, 2, from_be_bytes);
60bounded_reader!(be_u32, try_be_u32, u32, 4, from_be_bytes);
61bounded_reader!(be_u64, try_be_u64, u64, 8, from_be_bytes);
62bounded_reader!(le_u16, try_le_u16, u16, 2, from_le_bytes);
63bounded_reader!(le_u32, try_le_u32, u32, 4, from_le_bytes);
64bounded_reader!(le_u64, try_le_u64, u64, 8, from_le_bytes);
65
66// Signed twins. Two's-complement reinterpretation is the whole difference: a field the
67// format declares signed (a FILETIME delta, a negative record offset, a signed count)
68// read through the unsigned reader comes back as its huge positive twin.
69bounded_reader!(be_i16, try_be_i16, i16, 2, from_be_bytes);
70bounded_reader!(be_i32, try_be_i32, i32, 4, from_be_bytes);
71bounded_reader!(be_i64, try_be_i64, i64, 8, from_be_bytes);
72bounded_reader!(le_i16, try_le_i16, i16, 2, from_le_bytes);
73bounded_reader!(le_i32, try_le_i32, i32, 4, from_le_bytes);
74bounded_reader!(le_i64, try_le_i64, i64, 8, from_le_bytes);
75
76/// Copy the `N`-byte window at `off` into an array; `None` if that window is not fully in
77/// range (too short, offset past EOF, or `off + N` overflowing `usize`). Never panics.
78///
79/// The array flavour of the readers above, for the fixed-width windows that are not
80/// integers — GUIDs, signatures, digests, fixed-size name fields:
81///
82/// ```
83/// use safe_read::try_bytes;
84/// let record = [0xde, 0xad, 0xbe, 0xef, 0x01, 0x02];
85/// assert_eq!(try_bytes::<4>(&record, 0), Some([0xde, 0xad, 0xbe, 0xef]));
86/// assert_eq!(try_bytes::<4>(&record, 3), None); // runs past the end
87/// ```
88#[must_use]
89pub fn try_bytes<const N: usize>(data: &[u8], off: usize) -> Option<[u8; N]> {
90    let end = off.checked_add(N)?;
91    let slice = data.get(off..end)?;
92    let mut buf = [0u8; N];
93    buf.copy_from_slice(slice);
94    Some(buf)
95}
96
97/// Read a single byte at `off`; `None` if `off` is past the end. Never panics.
98#[must_use]
99pub fn try_u8(data: &[u8], off: usize) -> Option<u8> {
100    data.get(off).copied()
101}
102
103/// Read a single byte at `off`; `0` if `off` is past the end. Never panics. (Endianness is
104/// irrelevant for one byte; provided so callers never index `data[off]` directly.)
105#[must_use]
106pub fn u8(data: &[u8], off: usize) -> u8 {
107    try_u8(data, off).unwrap_or(0)
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn big_endian_reads_in_range() {
116        assert_eq!(be_u16(&[0x12, 0x34], 0), 0x1234);
117        assert_eq!(be_u32(&[0, 0, 1, 0], 0), 256);
118        assert_eq!(
119            be_u64(&[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08], 0),
120            0x0102_0304_0506_0708
121        );
122    }
123
124    #[test]
125    fn little_endian_reads_in_range() {
126        assert_eq!(le_u16(&[0x34, 0x12], 0), 0x1234);
127        assert_eq!(le_u32(&[0, 1, 0, 0], 0), 256);
128        assert_eq!(
129            le_u64(&[0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01], 0),
130            0x0102_0304_0506_0708
131        );
132    }
133
134    #[test]
135    fn reads_honor_offset() {
136        assert_eq!(be_u16(&[0xaa, 0x12, 0x34], 1), 0x1234);
137        assert_eq!(le_u32(&[0xff, 0xff, 0, 1, 0, 0], 2), 256);
138    }
139
140    #[test]
141    fn single_byte_reads() {
142        assert_eq!(u8(&[0xab, 0xcd], 0), 0xab);
143        assert_eq!(u8(&[0xab, 0xcd], 1), 0xcd);
144        assert_eq!(u8(&[0xab], 5), 0); // past end → 0
145        assert_eq!(u8(&[], 0), 0);
146        assert_eq!(try_u8(&[0xab], 0), Some(0xab));
147        assert_eq!(try_u8(&[0xab], 1), None);
148    }
149
150    #[test]
151    fn try_variants_distinguish_zero_from_absent() {
152        assert_eq!(try_le_u32(&[0, 0, 0, 0], 0), Some(0)); // genuine in-range 0
153        assert_eq!(try_le_u32(&[0, 0, 0], 0), None); // too short
154        assert_eq!(try_be_u16(&[1, 2], 2), None); // offset past window
155        assert_eq!(
156            try_be_u64(&[1, 2, 3, 4, 5, 6, 7, 8], 0),
157            Some(0x0102_0304_0506_0708)
158        );
159        assert_eq!(try_le_u16(&[], 0), None);
160    }
161
162    #[test]
163    fn out_of_range_returns_zero_never_panics() {
164        assert_eq!(be_u32(&[1, 2, 3], 0), 0);
165        assert_eq!(be_u64(&[1, 2, 3, 4, 5, 6, 7], 0), 0);
166        assert_eq!(be_u32(&[1, 2, 3, 4], 2), 0);
167        assert_eq!(le_u16(&[1, 2], 2), 0);
168        assert_eq!(be_u16(&[], 0), 0);
169        assert_eq!(le_u32(&[1, 2, 3, 4], 100), 0);
170    }
171
172    #[test]
173    fn offset_overflow_returns_zero() {
174        assert_eq!(be_u32(&[1, 2, 3, 4], usize::MAX), 0);
175        assert_eq!(try_be_u32(&[1, 2, 3, 4], usize::MAX), None);
176    }
177
178    #[test]
179    fn signed_reads_in_range() {
180        assert_eq!(le_i16(&[0x34, 0x12], 0), 0x1234);
181        assert_eq!(be_i16(&[0x12, 0x34], 0), 0x1234);
182        assert_eq!(le_i32(&[0, 1, 0, 0], 0), 256);
183        assert_eq!(be_i32(&[0, 0, 1, 0], 0), 256);
184        assert_eq!(
185            le_i64(&[0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01], 0),
186            0x0102_0304_0506_0708
187        );
188        assert_eq!(
189            be_i64(&[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08], 0),
190            0x0102_0304_0506_0708
191        );
192    }
193
194    #[test]
195    fn signed_reads_honor_offset() {
196        assert_eq!(be_i16(&[0xaa, 0x12, 0x34], 1), 0x1234);
197        assert_eq!(le_i32(&[0xff, 0xff, 0, 1, 0, 0], 2), 256);
198    }
199
200    /// The whole point of the signed readers: a two's-complement bit pattern must come
201    /// back as a negative number, not as its huge unsigned twin.
202    #[test]
203    fn signed_reads_round_trip_negative_values() {
204        assert_eq!(le_i16(&[0xff, 0xff], 0), -1);
205        assert_eq!(be_i16(&[0xff, 0xff], 0), -1);
206        assert_eq!(le_i32(&[0xff, 0xff, 0xff, 0xff], 0), -1);
207        assert_eq!(be_i32(&[0xff, 0xff, 0xff, 0xff], 0), -1);
208        assert_eq!(le_i64(&[0xff; 8], 0), -1);
209        assert_eq!(be_i64(&[0xff; 8], 0), -1);
210
211        // i16::MIN / i32::MIN / i64::MIN — the sign bit alone.
212        assert_eq!(le_i16(&[0x00, 0x80], 0), i16::MIN);
213        assert_eq!(be_i16(&[0x80, 0x00], 0), i16::MIN);
214        assert_eq!(le_i32(&[0x00, 0x00, 0x00, 0x80], 0), i32::MIN);
215        assert_eq!(be_i32(&[0x80, 0x00, 0x00, 0x00], 0), i32::MIN);
216        assert_eq!(le_i64(&[0, 0, 0, 0, 0, 0, 0, 0x80], 0), i64::MIN);
217        assert_eq!(be_i64(&[0x80, 0, 0, 0, 0, 0, 0, 0], 0), i64::MIN);
218
219        // A FILETIME-style "no such time" sentinel and an ordinary negative delta.
220        assert_eq!(
221            le_i64(&[0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff], 0),
222            -2
223        );
224        assert_eq!(le_i32(&[0x9c, 0xff, 0xff, 0xff], 0), -100);
225
226        assert_eq!(try_le_i32(&[0xff, 0xff, 0xff, 0xff], 0), Some(-1));
227        assert_eq!(try_be_i64(&[0xff; 8], 0), Some(-1));
228    }
229
230    #[test]
231    fn signed_out_of_range_returns_zero_or_none() {
232        assert_eq!(le_i16(&[1], 0), 0);
233        assert_eq!(be_i16(&[], 0), 0);
234        assert_eq!(le_i32(&[1, 2, 3], 0), 0);
235        assert_eq!(be_i32(&[1, 2, 3, 4], 2), 0);
236        assert_eq!(le_i64(&[1, 2, 3, 4, 5, 6, 7], 0), 0);
237        assert_eq!(be_i64(&[1, 2, 3, 4, 5, 6, 7, 8], 100), 0);
238
239        assert_eq!(try_le_i16(&[1], 0), None);
240        assert_eq!(try_be_i16(&[], 0), None);
241        assert_eq!(try_le_i32(&[1, 2, 3], 0), None);
242        assert_eq!(try_be_i32(&[1, 2, 3, 4], 2), None);
243        assert_eq!(try_le_i64(&[1, 2, 3, 4, 5, 6, 7], 0), None);
244        assert_eq!(try_be_i64(&[1, 2, 3, 4, 5, 6, 7, 8], 100), None);
245
246        // A genuine in-range 0 is still distinguishable from absent.
247        assert_eq!(try_le_i32(&[0, 0, 0, 0], 0), Some(0));
248    }
249
250    #[test]
251    fn signed_offset_overflow_returns_zero() {
252        assert_eq!(le_i16(&[1, 2, 3, 4], usize::MAX), 0);
253        assert_eq!(be_i32(&[1, 2, 3, 4], usize::MAX), 0);
254        assert_eq!(le_i64(&[1, 2, 3, 4], usize::MAX), 0);
255        assert_eq!(try_le_i16(&[1, 2, 3, 4], usize::MAX), None);
256        assert_eq!(try_be_i32(&[1, 2, 3, 4], usize::MAX), None);
257        assert_eq!(try_le_i64(&[1, 2, 3, 4], usize::MAX), None);
258    }
259
260    #[test]
261    fn try_bytes_window_in_range() {
262        let data = [0xde, 0xad, 0xbe, 0xef, 0x01, 0x02];
263        assert_eq!(try_bytes::<4>(&data, 0), Some([0xde, 0xad, 0xbe, 0xef]));
264        assert_eq!(try_bytes::<2>(&data, 4), Some([0x01, 0x02]));
265        assert_eq!(try_bytes::<1>(&data, 5), Some([0x02]));
266        assert_eq!(try_bytes::<6>(&data, 0), Some(data));
267        // A zero-width window is in range anywhere up to the end.
268        assert_eq!(try_bytes::<0>(&data, 6), Some([]));
269        assert_eq!(try_bytes::<0>(&[], 0), Some([]));
270        // A 16-byte GUID window — the case the fleet re-derives everywhere.
271        let guid = [
272            0x77, 0x4e, 0xc1, 0x1a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
273            0x00, 0x00,
274        ];
275        assert_eq!(try_bytes::<16>(&guid, 0), Some(guid));
276    }
277
278    #[test]
279    fn try_bytes_out_of_range_returns_none() {
280        let data = [1u8, 2, 3, 4];
281        assert_eq!(try_bytes::<5>(&data, 0), None); // window longer than slice
282        assert_eq!(try_bytes::<4>(&data, 1), None); // window runs past the end
283        assert_eq!(try_bytes::<1>(&data, 4), None); // offset at the end
284        assert_eq!(try_bytes::<1>(&[], 0), None);
285        assert_eq!(try_bytes::<16>(&data, 0), None);
286    }
287
288    /// `off + N` must be a `checked_add`: an unchecked one wraps here and would then
289    /// index a window that looks in range.
290    #[test]
291    fn try_bytes_offset_overflow_returns_none() {
292        let data = [1u8, 2, 3, 4];
293        assert_eq!(try_bytes::<4>(&data, usize::MAX), None);
294        assert_eq!(try_bytes::<16>(&data, usize::MAX - 8), None);
295        assert_eq!(try_bytes::<2>(&data, usize::MAX - 1), None);
296    }
297}