Skip to main content

argus/
le.rs

1//! Little-endian wire primitives and alignment helpers.
2//!
3//! Fixed-layout encoders/decoders use these cursors instead of repeating
4//! slice arithmetic. Overflow on write is a layout bug (panic); read length
5//! is validated once via [`LeReader::require`].
6
7use std::ops::Range;
8
9/// Round `n` up to the next multiple of 8.
10///
11/// Already aligned values are returned unchanged.
12///
13/// # Panics
14///
15/// In debug builds, values greater than `u64::MAX - 7` overflow and panic.
16/// Wire offsets should be bounds-checked before alignment.
17#[inline]
18#[must_use]
19pub const fn align8(n: u64) -> u64 {
20    (n + 7) & !7
21}
22
23/// Return the number of padding bytes needed for 8-byte alignment.
24///
25/// The result is always in `0..=7`, and is zero when `len` is already aligned.
26#[inline]
27#[must_use]
28pub const fn padding_to_align8(len: usize) -> usize {
29    (8 - (len % 8)) % 8
30}
31
32/// Why a half-open byte span `[offset, offset + len)` is invalid.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum SpanError {
35    /// `offset + len` overflowed `u64` / `usize`.
36    AddOverflow,
37    /// Span end exceeds the container length.
38    OutOfBounds {
39        /// Computed exclusive end of the span.
40        end: u64,
41    },
42}
43
44/// Validate `offset + len <= container_len` for a half-open byte span.
45///
46/// A zero-length span at `container_len` is valid.
47///
48/// # Errors
49///
50/// Returns [`SpanError`] when the span overflows or exceeds `container_len`.
51pub fn checked_u64_byte_span(offset: u64, len: u64, container_len: u64) -> Result<(), SpanError> {
52    let end = offset.checked_add(len).ok_or(SpanError::AddOverflow)?;
53    if end > container_len {
54        return Err(SpanError::OutOfBounds { end });
55    }
56    Ok(())
57}
58
59/// Construct a half-open subslice range after validating its bounds.
60///
61/// This function performs the arithmetic without indexing a slice, allowing
62/// callers to translate [`SpanError`] into a format-specific error first.
63///
64/// # Errors
65///
66/// Returns [`SpanError`] when the span overflows or exceeds `container_len`.
67pub fn checked_subslice(
68    offset: usize,
69    len: usize,
70    container_len: usize,
71) -> Result<Range<usize>, SpanError> {
72    let end = offset.checked_add(len).ok_or(SpanError::AddOverflow)?;
73    if end > container_len {
74        return Err(SpanError::OutOfBounds { end: end as u64 });
75    }
76    Ok(offset..end)
77}
78
79/// Cursor that writes little-endian primitives into a mutable byte slice.
80///
81/// Panics on overflow; callers size buffers to the exact fixed structure
82/// length, so overflow indicates a layout bug rather than runtime input error.
83#[derive(Debug)]
84pub struct LeWriter<'a> {
85    buf: &'a mut [u8],
86    pos: usize,
87}
88
89impl<'a> LeWriter<'a> {
90    /// Create a writer over `buf`, starting at offset 0.
91    #[inline]
92    pub fn new(buf: &'a mut [u8]) -> Self {
93        Self { buf, pos: 0 }
94    }
95
96    /// Current write offset.
97    #[inline]
98    #[must_use]
99    pub fn position(&self) -> usize {
100        self.pos
101    }
102
103    /// Write raw bytes verbatim and advance the cursor.
104    ///
105    /// # Panics
106    ///
107    /// Panics if `bytes` does not fit in the remaining output buffer.
108    #[inline]
109    pub fn put_bytes(&mut self, bytes: &[u8]) {
110        self.buf[self.pos..self.pos + bytes.len()].copy_from_slice(bytes);
111        self.pos += bytes.len();
112    }
113
114    /// Write a `u16` in little-endian order.
115    ///
116    /// # Panics
117    ///
118    /// Panics if fewer than 2 bytes remain.
119    #[inline]
120    pub fn put_u16(&mut self, v: u16) {
121        self.put_bytes(&v.to_le_bytes());
122    }
123
124    /// Write a `u32` in little-endian order.
125    ///
126    /// # Panics
127    ///
128    /// Panics if fewer than 4 bytes remain.
129    #[inline]
130    pub fn put_u32(&mut self, v: u32) {
131        self.put_bytes(&v.to_le_bytes());
132    }
133
134    /// Write a `u64` in little-endian order.
135    ///
136    /// # Panics
137    ///
138    /// Panics if fewer than 8 bytes remain.
139    #[inline]
140    pub fn put_u64(&mut self, v: u64) {
141        self.put_bytes(&v.to_le_bytes());
142    }
143
144    /// Write `count` zero bytes, typically for reserved fields or padding.
145    ///
146    /// # Panics
147    ///
148    /// Panics if `count` exceeds the remaining output length.
149    #[inline]
150    pub fn put_zeros(&mut self, count: usize) {
151        self.buf[self.pos..self.pos + count].fill(0);
152        self.pos += count;
153    }
154}
155
156/// Buffer too small for a named fixed structure.
157#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
158#[error("{structure}: need {need} bytes, got {got}")]
159pub struct BufferTooShort {
160    /// Structure name for diagnostics (e.g. `"superblock"`).
161    pub structure: &'static str,
162    /// Required byte length.
163    pub need: usize,
164    /// Actual buffer length.
165    pub got: usize,
166}
167
168/// Cursor that reads little-endian primitives from a byte slice.
169///
170/// Length is validated once by the caller (via [`LeReader::require`]); the
171/// typed readers then advance without returning per-field errors. This keeps
172/// fixed-layout parsers concise while making the initial size check explicit.
173#[derive(Debug)]
174pub struct LeReader<'a> {
175    buf: &'a [u8],
176    pos: usize,
177}
178
179impl<'a> LeReader<'a> {
180    /// Create a reader over `buf`, starting at offset 0.
181    ///
182    /// Prefer [`Self::require`] when parsing untrusted input.
183    #[inline]
184    #[must_use]
185    pub fn new(buf: &'a [u8]) -> Self {
186        Self { buf, pos: 0 }
187    }
188
189    /// Ensure `buf` holds at least `need` bytes for `structure`.
190    ///
191    /// # Errors
192    ///
193    /// Returns [`BufferTooShort`] when `buf.len() < need`.
194    #[inline]
195    pub fn require(
196        buf: &'a [u8],
197        structure: &'static str,
198        need: usize,
199    ) -> Result<Self, BufferTooShort> {
200        if buf.len() < need {
201            return Err(BufferTooShort {
202                structure,
203                need,
204                got: buf.len(),
205            });
206        }
207        Ok(Self::new(buf))
208    }
209
210    /// Current read offset.
211    #[inline]
212    #[must_use]
213    pub fn position(&self) -> usize {
214        self.pos
215    }
216
217    /// Return the remaining unread bytes without advancing the cursor.
218    #[inline]
219    #[must_use]
220    pub fn remaining(&self) -> &'a [u8] {
221        &self.buf[self.pos..]
222    }
223
224    /// Read a fixed 4-byte array (for example, a magic tag).
225    ///
226    /// # Panics
227    ///
228    /// Panics if fewer than 4 bytes remain.
229    #[inline]
230    pub fn take_4(&mut self) -> [u8; 4] {
231        let mut out = [0u8; 4];
232        out.copy_from_slice(&self.buf[self.pos..self.pos + 4]);
233        self.pos += 4;
234        out
235    }
236
237    /// Read a fixed 16-byte array (for example, a UUID).
238    ///
239    /// # Panics
240    ///
241    /// Panics if fewer than 16 bytes remain.
242    #[inline]
243    pub fn take_16(&mut self) -> [u8; 16] {
244        let mut out = [0u8; 16];
245        out.copy_from_slice(&self.buf[self.pos..self.pos + 16]);
246        self.pos += 16;
247        out
248    }
249
250    /// Read a little-endian `u16`.
251    ///
252    /// # Panics
253    ///
254    /// Panics if fewer than 2 bytes remain.
255    #[inline]
256    pub fn take_u16(&mut self) -> u16 {
257        let mut b = [0u8; 2];
258        b.copy_from_slice(&self.buf[self.pos..self.pos + 2]);
259        self.pos += 2;
260        u16::from_le_bytes(b)
261    }
262
263    /// Read a little-endian `u32`.
264    ///
265    /// # Panics
266    ///
267    /// Panics if fewer than 4 bytes remain.
268    #[inline]
269    pub fn take_u32(&mut self) -> u32 {
270        let mut b = [0u8; 4];
271        b.copy_from_slice(&self.buf[self.pos..self.pos + 4]);
272        self.pos += 4;
273        u32::from_le_bytes(b)
274    }
275
276    /// Read a little-endian `u64`.
277    ///
278    /// # Panics
279    ///
280    /// Panics if fewer than 8 bytes remain.
281    #[inline]
282    pub fn take_u64(&mut self) -> u64 {
283        let mut b = [0u8; 8];
284        b.copy_from_slice(&self.buf[self.pos..self.pos + 8]);
285        self.pos += 8;
286        u64::from_le_bytes(b)
287    }
288
289    /// Advance over `count` bytes, typically reserved fields.
290    ///
291    /// The bounds failure is observed by the next method that slices the
292    /// buffer, or immediately by [`Self::remaining`].
293    ///
294    /// # Panics
295    ///
296    /// Addition overflows only for unrealistically large `count` values.
297    #[inline]
298    pub fn skip(&mut self, count: usize) {
299        self.pos += count;
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306
307    #[test]
308    fn align8_rounds_up() {
309        assert_eq!(align8(0), 0);
310        assert_eq!(align8(1), 8);
311        assert_eq!(align8(8), 8);
312        assert_eq!(align8(9), 16);
313        assert_eq!(align8(63), 64);
314        assert_eq!(padding_to_align8(0), 0);
315        assert_eq!(padding_to_align8(1), 7);
316        assert_eq!(padding_to_align8(8), 0);
317    }
318
319    #[test]
320    fn writer_reader_round_trip() {
321        let mut buf = [0u8; 26];
322        let mut w = LeWriter::new(&mut buf);
323        w.put_bytes(b"TESS");
324        w.put_u16(0xBEEF);
325        w.put_u32(0xDEAD_BEEF);
326        w.put_u64(0x0102_0304_0506_0708);
327        w.put_zeros(8);
328        assert_eq!(w.position(), 26);
329
330        let mut r = LeReader::new(&buf);
331        assert_eq!(&r.take_4(), b"TESS");
332        assert_eq!(r.take_u16(), 0xBEEF);
333        assert_eq!(r.take_u32(), 0xDEAD_BEEF);
334        assert_eq!(r.take_u64(), 0x0102_0304_0506_0708);
335        r.skip(8);
336        assert_eq!(r.position(), 26);
337    }
338
339    #[test]
340    fn require_rejects_short_buffer() {
341        let buf = [0u8; 4];
342        let result = LeReader::require(&buf, "thing", 8);
343        assert_eq!(
344            result.unwrap_err(),
345            BufferTooShort {
346                structure: "thing",
347                need: 8,
348                got: 4,
349            }
350        );
351    }
352
353    #[test]
354    fn span_checks() {
355        assert!(checked_u64_byte_span(10, 5, 15).is_ok());
356        assert!(matches!(
357            checked_u64_byte_span(10, 6, 15),
358            Err(SpanError::OutOfBounds { end: 16 })
359        ));
360        assert!(matches!(
361            checked_u64_byte_span(u64::MAX, 1, u64::MAX),
362            Err(SpanError::AddOverflow)
363        ));
364        assert_eq!(checked_subslice(2, 3, 10).unwrap(), 2..5);
365        assert!(checked_subslice(8, 3, 10).is_err());
366    }
367}