Skip to main content

primase_foundations/
bytes.rs

1//! Bounds-checked reading of fixed-size numbers and byte spans from a borrowed slice. Every read either returns the
2//! value or a [`BoundsError`] (or, once a name is attached with [`Field::field`], a [`FieldError`]) that says where the
3//! read was and how far it ran past the data; nothing here panics on short input.
4//!
5//! [`View`] reads at explicit offsets; [`Cursor`] reads in sequence and moves only after a successful read. Both can
6//! be given an `origin`, the offset of their first byte in the file they were cut from, so errors from a nested
7//! structure still report the offset in the file. Format, allocation and count policies stay with the caller, and so
8//! does the byte order: every multi-byte read states its [`Endian`].
9#![deny(clippy::disallowed_methods, clippy::disallowed_types)]
10
11use std::{fmt, ops::Range};
12
13/// Why a read was refused. The operands are kept as given, so the offset in a message is exact even where adding the
14/// origin would overflow `usize` (see [`BoundsError::absolute`]).
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum BoundsError {
17    /// `count` entries of `stride` bytes, or `offset` plus their length, do not fit in a `usize`.
18    LengthOverflow {
19        /// Where the span starts, within the view.
20        offset: usize,
21        /// The file offset of the view's first byte.
22        origin: usize,
23        /// The number of entries asked for.
24        count: usize,
25        /// The size of each entry in bytes.
26        stride: usize,
27    },
28    /// The span of `length` bytes at `offset` ends beyond the `total` bytes of the view.
29    OutOfBounds {
30        /// Where the span starts, within the view.
31        offset: usize,
32        /// The file offset of the view's first byte.
33        origin: usize,
34        /// The span's length in bytes.
35        length: usize,
36        /// The size of the view in bytes.
37        total: usize,
38    },
39}
40impl BoundsError {
41    fn with_origin(self, origin: usize) -> Self {
42        match self {
43            Self::LengthOverflow {
44                offset,
45                count,
46                stride,
47                ..
48            } => Self::LengthOverflow {
49                offset,
50                origin,
51                count,
52                stride,
53            },
54            Self::OutOfBounds {
55                offset,
56                length,
57                total,
58                ..
59            } => Self::OutOfBounds {
60                offset,
61                origin,
62                length,
63                total,
64            },
65        }
66    }
67    /// Exact absolute byte offset, including an origin beyond the local view.
68    pub fn absolute(&self) -> u128 {
69        let (offset, origin) = match *self {
70            Self::LengthOverflow { offset, origin, .. } | Self::OutOfBounds { offset, origin, .. } => {
71                (offset, origin)
72            }
73        };
74        // two `usize`s always sum within a `u128`; saturating only states that to the lint
75        (offset as u128).saturating_add(origin as u128)
76    }
77}
78impl fmt::Display for BoundsError {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        let offset = self.absolute();
81        match self {
82            Self::LengthOverflow { count, stride, .. } => write!(
83                f,
84                "byte span at {offset:#x}: {count} entries of {stride} bytes overflow"
85            ),
86            Self::OutOfBounds { length, total, .. } => write!(
87                f,
88                "byte span at {offset:#x}: {length} bytes outside a {total}-byte view"
89            ),
90        }
91    }
92}
93impl std::error::Error for BoundsError {}
94
95/// A read of a named field that failed: the field, and where its bytes would be ([`Field::field`]).
96#[derive(Clone, Copy, Debug, PartialEq, Eq)]
97pub struct FieldError {
98    /// The name given to [`Field::field`].
99    pub what: &'static str,
100    /// The refused read.
101    pub bounds: BoundsError,
102}
103impl fmt::Display for FieldError {
104    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105        let (what, at) = (self.what, self.bounds.absolute());
106        match self.bounds {
107            BoundsError::LengthOverflow { count, stride, .. } => {
108                write!(f, "{what} at {at:#x}: {count} entries of {stride} bytes overflow")
109            }
110            BoundsError::OutOfBounds { length, total, .. } => {
111                write!(f, "{what} at {at:#x}: {length} bytes run past the end of {total}")
112            }
113        }
114    }
115}
116impl std::error::Error for FieldError {}
117
118/// Names the field a failed read was of.
119pub trait Field<T> {
120    /// Turns a refused read into a [`FieldError`] naming `what`.
121    fn field(self, what: &'static str) -> Result<T, FieldError>;
122}
123impl<T> Field<T> for Result<T, BoundsError> {
124    fn field(self, what: &'static str) -> Result<T, FieldError> {
125        self.map_err(|bounds| FieldError { what, bounds })
126    }
127}
128
129/// The byte order of a multi-byte number: the format's, stated at each read.
130#[derive(Clone, Copy, Debug, PartialEq, Eq)]
131pub enum Endian {
132    /// Least significant byte first.
133    Little,
134    /// Most significant byte first.
135    Big,
136}
137
138/// The fixed-size numbers [`View`] and [`Cursor`] read, in either byte order.
139macro_rules! numbers {
140    ($($name:ident: $t:ty),* $(,)?) => {
141        impl View<'_> {
142            $(
143                #[doc = concat!("The `", stringify!($t), "` at `offset`, in byte order `e`.")]
144                #[inline]
145                pub fn $name(&self, offset: usize, e: Endian) -> Result<$t, BoundsError> {
146                    let b = self.array(offset)?;
147                    Ok(match e {
148                        Endian::Little => <$t>::from_le_bytes(b),
149                        Endian::Big => <$t>::from_be_bytes(b),
150                    })
151                }
152            )*
153        }
154        impl Cursor<'_> {
155            $(
156                #[doc = concat!("The next `", stringify!($t), "`, in byte order `e`.")]
157                #[inline]
158                pub fn $name(&mut self, e: Endian) -> Result<$t, BoundsError> {
159                    let b = self.array()?;
160                    Ok(match e {
161                        Endian::Little => <$t>::from_le_bytes(b),
162                        Endian::Big => <$t>::from_be_bytes(b),
163                    })
164                }
165            )*
166        }
167    };
168}
169numbers!(u16: u16, i16: i16, u32: u32, i32: i32, u64: u64, i64: i64, f32: f32, f64: f64);
170
171/// The byte range of `count` entries of `stride` bytes starting at `offset` in a view of `total` bytes: the
172/// multiplication, the addition and the bound are each checked, in that order. A zero stride makes every count fit,
173/// so it does not by itself make allocating or processing `count` records safe; that is the caller's policy.
174#[inline]
175pub fn checked_span(
176    offset: usize,
177    count: usize,
178    stride: usize,
179    total: usize,
180) -> Result<Range<usize>, BoundsError> {
181    let overflow = || BoundsError::LengthOverflow {
182        offset,
183        origin: 0,
184        count,
185        stride,
186    };
187    let length = count.checked_mul(stride).ok_or_else(overflow)?;
188    let end = offset.checked_add(length).ok_or_else(overflow)?;
189    if end > total {
190        return Err(BoundsError::OutOfBounds {
191            offset,
192            origin: 0,
193            length,
194            total,
195        });
196    }
197    Ok(offset..end)
198}
199
200/// A borrowed byte slice read at explicit offsets, with the file offset of its first byte for error reporting.
201#[derive(Clone, Copy, Debug)]
202pub struct View<'a> {
203    data: &'a [u8],
204    origin: usize,
205}
206impl<'a> View<'a> {
207    /// A view of `data`, whose first byte is offset 0 of the file.
208    #[inline]
209    pub fn new(data: &'a [u8]) -> Self {
210        Self::with_origin(data, 0)
211    }
212    /// `origin` is the source byte offset of `data[0]`; slicing offsets remain local.
213    #[inline]
214    pub fn with_origin(data: &'a [u8], origin: usize) -> Self {
215        Self { data, origin }
216    }
217    /// The range of `length` bytes at `offset`, if the view holds it.
218    #[inline]
219    fn locate(&self, offset: usize, length: usize) -> Result<Range<usize>, BoundsError> {
220        checked_span(offset, length, 1, self.data.len()).map_err(|e| e.with_origin(self.origin))
221    }
222    /// The bytes of a range [`View::locate`] returned.
223    #[inline]
224    fn bytes_in(&self, span: Range<usize>) -> Result<&'a [u8], BoundsError> {
225        let (offset, length) = (span.start, span.len());
226        self.data.get(span).ok_or(BoundsError::OutOfBounds {
227            offset,
228            origin: self.origin,
229            length,
230            total: self.data.len(),
231        })
232    }
233    /// The `length` bytes at `offset`.
234    #[inline]
235    pub fn slice(&self, offset: usize, length: usize) -> Result<&'a [u8], BoundsError> {
236        self.bytes_in(self.locate(offset, length)?)
237    }
238    /// The `N` bytes at `offset`.
239    #[inline]
240    pub fn array<const N: usize>(&self, offset: usize) -> Result<[u8; N], BoundsError> {
241        let mut bytes = [0; N];
242        bytes.copy_from_slice(self.slice(offset, N)?);
243        Ok(bytes)
244    }
245    /// The byte at `offset`.
246    #[inline]
247    pub fn u8(&self, offset: usize) -> Result<u8, BoundsError> {
248        self.array::<1>(offset).map(|[b]| b)
249    }
250    /// The byte at `offset`, signed.
251    #[inline]
252    pub fn i8(&self, offset: usize) -> Result<i8, BoundsError> {
253        self.array::<1>(offset).map(i8::from_le_bytes)
254    }
255}
256
257/// A sequential reader over a borrowed slice. It advances only after a successful read or seek, so a refused read
258/// leaves it where it was.
259#[derive(Clone, Debug)]
260pub struct Cursor<'a> {
261    view: View<'a>,
262    position: usize,
263}
264impl<'a> Cursor<'a> {
265    /// A cursor at the start of `data`, whose first byte is offset 0 of the file.
266    #[inline]
267    pub fn new(data: &'a [u8]) -> Self {
268        Self::with_origin(data, 0)
269    }
270    /// A cursor at the start of `data`, whose first byte is offset `origin` of the file.
271    #[inline]
272    pub fn with_origin(data: &'a [u8], origin: usize) -> Self {
273        Self {
274            view: View::with_origin(data, origin),
275            position: 0,
276        }
277    }
278    /// The offset of the next byte to read, within the cursor's data.
279    pub fn position(&self) -> usize {
280        self.position
281    }
282    /// All the bytes it reads, from its start (a parser that looks back or ahead by offset).
283    pub fn bytes(&self) -> &'a [u8] {
284        self.view.data
285    }
286    /// How many bytes are left after the position.
287    pub fn remaining(&self) -> usize {
288        // the position never passes the end, but a subtraction that cannot wrap is the cheaper proof
289        self.view.data.len().saturating_sub(self.position)
290    }
291    /// The next `length` bytes.
292    pub fn take(&mut self, length: usize) -> Result<&'a [u8], BoundsError> {
293        let span = self.view.locate(self.position, length)?;
294        let end = span.end;
295        let bytes = self.view.bytes_in(span)?;
296        self.position = end;
297        Ok(bytes)
298    }
299    /// The bytes from the position up to `end` (an offset in the view), the cursor left at `end`: refused when `end`
300    /// is before the position or past the view.
301    pub fn take_to(&mut self, end: usize) -> Result<&'a [u8], BoundsError> {
302        let within = View::with_origin(self.view.slice(0, end)?, self.view.origin);
303        let bytes = within.slice(self.position, end.saturating_sub(self.position))?;
304        self.position = end;
305        Ok(bytes)
306    }
307    /// Moves to `offset` (the end of the data is allowed); an offset past it is refused.
308    pub fn seek(&mut self, offset: usize) -> Result<(), BoundsError> {
309        self.view.slice(offset, 0)?;
310        self.position = offset;
311        Ok(())
312    }
313    /// The next `N` bytes.
314    pub fn array<const N: usize>(&mut self) -> Result<[u8; N], BoundsError> {
315        let span = self.view.locate(self.position, N)?;
316        let end = span.end;
317        let mut bytes = [0; N];
318        bytes.copy_from_slice(self.view.bytes_in(span)?);
319        self.position = end;
320        Ok(bytes)
321    }
322    /// The next byte.
323    pub fn u8(&mut self) -> Result<u8, BoundsError> {
324        self.array::<1>().map(|[b]| b)
325    }
326    /// The next byte, signed.
327    pub fn i8(&mut self) -> Result<i8, BoundsError> {
328        self.array::<1>().map(i8::from_le_bytes)
329    }
330    /// A `u32` count, in byte order `e`, of records of `each` bytes that the rest of the view holds; a count it
331    /// can't hold is refused, its span's error naming where the records would start, and the cursor stays before the
332    /// count. A zero `each` bounds nothing (`checked_span`).
333    pub fn count(&mut self, e: Endian, each: usize) -> Result<usize, BoundsError> {
334        let start = self.position;
335        let n = self.u32(e)?;
336        // past usize (a 16-bit target), the count is beyond any view: the largest is refused the same way
337        let n = usize::try_from(n).unwrap_or(usize::MAX);
338        match checked_span(self.position, n, each, self.view.data.len()) {
339            Ok(_) => Ok(n),
340            Err(err) => {
341                self.position = start;
342                Err(err.with_origin(self.view.origin))
343            }
344        }
345    }
346}
347
348#[cfg(test)]
349mod tests {
350    use super::*;
351    #[test]
352    fn spans_and_arithmetic_boundaries() {
353        assert_eq!(checked_span(0, 0, 1, 0), Ok(0..0));
354        assert_eq!(checked_span(4, 0, 7, 4), Ok(4..4));
355        assert!(matches!(
356            checked_span(5, 0, 1, 4),
357            Err(BoundsError::OutOfBounds { .. })
358        ));
359        assert_eq!(checked_span(1, usize::MAX, 0, 1), Ok(1..1));
360        assert!(matches!(
361            checked_span(0, usize::MAX, 2, usize::MAX),
362            Err(BoundsError::LengthOverflow { .. })
363        ));
364        assert!(matches!(
365            checked_span(usize::MAX, 1, 1, usize::MAX),
366            Err(BoundsError::LengthOverflow { .. })
367        ));
368        assert_eq!(View::new(&[1, 2, 3]).array::<2>(1), Ok([2, 3]));
369        assert!(View::new(&[]).array::<1>(0).is_err());
370    }
371    #[test]
372    fn origins_are_exact_even_past_address_space() {
373        let view = View::with_origin(&[], usize::MAX);
374        assert_eq!(view.slice(1, 0).unwrap_err().absolute(), usize::MAX as u128 + 1);
375        assert_eq!(
376            view.slice(1, 0),
377            Err(BoundsError::OutOfBounds {
378                offset: 1,
379                origin: usize::MAX,
380                length: 0,
381                total: 0,
382            })
383        );
384        assert_eq!(
385            view.slice(usize::MAX, 1),
386            Err(BoundsError::LengthOverflow {
387                offset: usize::MAX,
388                origin: usize::MAX,
389                count: 1,
390                stride: 1,
391            })
392        );
393    }
394    /// Numbers read in the stated byte order, positionally and in sequence; a failed read leaves the cursor where it
395    /// was; a field's error names it and its exact offset.
396    #[test]
397    fn numbers_in_either_order_and_named_fields() {
398        let bytes = [0x01, 0x02, 0x03, 0x04, 0x00, 0x00, 0x80, 0x3f];
399        let view = View::with_origin(&bytes, 0x100);
400        assert_eq!(view.u16(0, Endian::Little), Ok(0x0201));
401        assert_eq!(view.u16(0, Endian::Big), Ok(0x0102));
402        assert_eq!(view.u32(0, Endian::Little), Ok(0x0403_0201));
403        assert_eq!(view.i8(2), Ok(3));
404        assert_eq!(view.f32(4, Endian::Little), Ok(1.0));
405        assert_eq!(view.u64(0, Endian::Big), Ok(0x0102_0304_0000_803f));
406        assert!(view.u32(6, Endian::Little).is_err());
407        let mut c = Cursor::with_origin(&bytes, 0x100);
408        assert_eq!(c.u8(), Ok(1));
409        assert_eq!(c.u16(Endian::Big), Ok(0x0203));
410        assert_eq!(c.position(), 3);
411        let err = c.u64(Endian::Little).field("a header's size").unwrap_err();
412        assert_eq!(c.position(), 3);
413        assert_eq!(
414            err.to_string(),
415            "a header's size at 0x103: 8 bytes run past the end of 8"
416        );
417        assert_eq!(c.array::<5>(), Ok([0x04, 0x00, 0x00, 0x80, 0x3f]));
418        // up to an end: not before the position, not past the view
419        let mut c = Cursor::new(&bytes);
420        c.seek(2).unwrap();
421        assert_eq!(c.take_to(4), Ok(&[0x03, 0x04][..]));
422        assert_eq!(c.position(), 4);
423        assert!(c.take_to(3).is_err());
424        assert!(c.take_to(9).is_err());
425        assert_eq!(c.position(), 4);
426    }
427
428    /// A count the rest of the view holds is read; one it can't (or whose bytes overflow) is refused without
429    /// advancing, naming where its records would start.
430    #[test]
431    fn counts_are_held_to_the_rest_of_the_view() {
432        // a count of 2 records of 2 bytes, then 4 bytes
433        let bytes = [2, 0, 0, 0, 9, 9, 9, 9];
434        let mut c = Cursor::new(&bytes);
435        assert_eq!(c.count(Endian::Little, 2), Ok(2));
436        assert_eq!(c.position(), 4);
437        let mut c = Cursor::with_origin(&bytes, 0x10);
438        assert_eq!(
439            c.count(Endian::Little, 3),
440            Err(BoundsError::OutOfBounds {
441                offset: 4,
442                origin: 0x10,
443                length: 6,
444                total: 8,
445            })
446        );
447        assert_eq!(c.position(), 0);
448        let huge = [0xff, 0xff, 0xff, 0xff];
449        assert!(matches!(
450            Cursor::new(&huge).count(Endian::Little, usize::MAX),
451            Err(BoundsError::LengthOverflow { .. })
452        ));
453        assert!(Cursor::new(&huge[..2]).count(Endian::Little, 1).is_err());
454    }
455
456    #[test]
457    fn failed_cursor_operations_do_not_advance() {
458        let mut cursor = Cursor::with_origin(&[1, 2, 3], 10);
459        assert_eq!(cursor.take(2), Ok(&[1, 2][..]));
460        assert!(cursor.take(2).is_err());
461        assert!(cursor.take(usize::MAX).is_err());
462        assert!(cursor.seek(4).is_err());
463        assert_eq!(cursor.position(), 2);
464        assert_eq!(cursor.remaining(), 1);
465        assert_eq!(cursor.take(1), Ok(&[3][..]));
466        assert_eq!(cursor.take(0), Ok(&[][..]));
467        assert_eq!(cursor.seek(0), Ok(()));
468        assert_eq!(cursor.remaining(), 3);
469    }
470}