argus-chunk 0.1.0

Argus watches over chunk IO — little-endian wire primitives, bounds-checked spans, and raw/zstd codecs
Documentation
//! Little-endian wire primitives and alignment helpers.
//!
//! Fixed-layout encoders/decoders use these cursors instead of repeating
//! slice arithmetic. Overflow on write is a layout bug (panic); read length
//! is validated once via [`LeReader::require`].

use std::ops::Range;

/// Round `n` up to the next multiple of 8.
///
/// Already aligned values are returned unchanged.
///
/// # Panics
///
/// In debug builds, values greater than `u64::MAX - 7` overflow and panic.
/// Wire offsets should be bounds-checked before alignment.
#[inline]
#[must_use]
pub const fn align8(n: u64) -> u64 {
    (n + 7) & !7
}

/// Return the number of padding bytes needed for 8-byte alignment.
///
/// The result is always in `0..=7`, and is zero when `len` is already aligned.
#[inline]
#[must_use]
pub const fn padding_to_align8(len: usize) -> usize {
    (8 - (len % 8)) % 8
}

/// Why a half-open byte span `[offset, offset + len)` is invalid.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SpanError {
    /// `offset + len` overflowed `u64` / `usize`.
    AddOverflow,
    /// Span end exceeds the container length.
    OutOfBounds {
        /// Computed exclusive end of the span.
        end: u64,
    },
}

/// Validate `offset + len <= container_len` for a half-open byte span.
///
/// A zero-length span at `container_len` is valid.
///
/// # Errors
///
/// Returns [`SpanError`] when the span overflows or exceeds `container_len`.
pub fn checked_u64_byte_span(offset: u64, len: u64, container_len: u64) -> Result<(), SpanError> {
    let end = offset.checked_add(len).ok_or(SpanError::AddOverflow)?;
    if end > container_len {
        return Err(SpanError::OutOfBounds { end });
    }
    Ok(())
}

/// Construct a half-open subslice range after validating its bounds.
///
/// This function performs the arithmetic without indexing a slice, allowing
/// callers to translate [`SpanError`] into a format-specific error first.
///
/// # Errors
///
/// Returns [`SpanError`] when the span overflows or exceeds `container_len`.
pub fn checked_subslice(
    offset: usize,
    len: usize,
    container_len: usize,
) -> Result<Range<usize>, SpanError> {
    let end = offset.checked_add(len).ok_or(SpanError::AddOverflow)?;
    if end > container_len {
        return Err(SpanError::OutOfBounds { end: end as u64 });
    }
    Ok(offset..end)
}

/// Cursor that writes little-endian primitives into a mutable byte slice.
///
/// Panics on overflow; callers size buffers to the exact fixed structure
/// length, so overflow indicates a layout bug rather than runtime input error.
#[derive(Debug)]
pub struct LeWriter<'a> {
    buf: &'a mut [u8],
    pos: usize,
}

impl<'a> LeWriter<'a> {
    /// Create a writer over `buf`, starting at offset 0.
    #[inline]
    pub fn new(buf: &'a mut [u8]) -> Self {
        Self { buf, pos: 0 }
    }

    /// Current write offset.
    #[inline]
    #[must_use]
    pub fn position(&self) -> usize {
        self.pos
    }

    /// Write raw bytes verbatim and advance the cursor.
    ///
    /// # Panics
    ///
    /// Panics if `bytes` does not fit in the remaining output buffer.
    #[inline]
    pub fn put_bytes(&mut self, bytes: &[u8]) {
        self.buf[self.pos..self.pos + bytes.len()].copy_from_slice(bytes);
        self.pos += bytes.len();
    }

    /// Write a `u16` in little-endian order.
    ///
    /// # Panics
    ///
    /// Panics if fewer than 2 bytes remain.
    #[inline]
    pub fn put_u16(&mut self, v: u16) {
        self.put_bytes(&v.to_le_bytes());
    }

    /// Write a `u32` in little-endian order.
    ///
    /// # Panics
    ///
    /// Panics if fewer than 4 bytes remain.
    #[inline]
    pub fn put_u32(&mut self, v: u32) {
        self.put_bytes(&v.to_le_bytes());
    }

    /// Write a `u64` in little-endian order.
    ///
    /// # Panics
    ///
    /// Panics if fewer than 8 bytes remain.
    #[inline]
    pub fn put_u64(&mut self, v: u64) {
        self.put_bytes(&v.to_le_bytes());
    }

    /// Write `count` zero bytes, typically for reserved fields or padding.
    ///
    /// # Panics
    ///
    /// Panics if `count` exceeds the remaining output length.
    #[inline]
    pub fn put_zeros(&mut self, count: usize) {
        self.buf[self.pos..self.pos + count].fill(0);
        self.pos += count;
    }
}

/// Buffer too small for a named fixed structure.
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error("{structure}: need {need} bytes, got {got}")]
pub struct BufferTooShort {
    /// Structure name for diagnostics (e.g. `"superblock"`).
    pub structure: &'static str,
    /// Required byte length.
    pub need: usize,
    /// Actual buffer length.
    pub got: usize,
}

/// Cursor that reads little-endian primitives from a byte slice.
///
/// Length is validated once by the caller (via [`LeReader::require`]); the
/// typed readers then advance without returning per-field errors. This keeps
/// fixed-layout parsers concise while making the initial size check explicit.
#[derive(Debug)]
pub struct LeReader<'a> {
    buf: &'a [u8],
    pos: usize,
}

impl<'a> LeReader<'a> {
    /// Create a reader over `buf`, starting at offset 0.
    ///
    /// Prefer [`Self::require`] when parsing untrusted input.
    #[inline]
    #[must_use]
    pub fn new(buf: &'a [u8]) -> Self {
        Self { buf, pos: 0 }
    }

    /// Ensure `buf` holds at least `need` bytes for `structure`.
    ///
    /// # Errors
    ///
    /// Returns [`BufferTooShort`] when `buf.len() < need`.
    #[inline]
    pub fn require(
        buf: &'a [u8],
        structure: &'static str,
        need: usize,
    ) -> Result<Self, BufferTooShort> {
        if buf.len() < need {
            return Err(BufferTooShort {
                structure,
                need,
                got: buf.len(),
            });
        }
        Ok(Self::new(buf))
    }

    /// Current read offset.
    #[inline]
    #[must_use]
    pub fn position(&self) -> usize {
        self.pos
    }

    /// Return the remaining unread bytes without advancing the cursor.
    #[inline]
    #[must_use]
    pub fn remaining(&self) -> &'a [u8] {
        &self.buf[self.pos..]
    }

    /// Read a fixed 4-byte array (for example, a magic tag).
    ///
    /// # Panics
    ///
    /// Panics if fewer than 4 bytes remain.
    #[inline]
    pub fn take_4(&mut self) -> [u8; 4] {
        let mut out = [0u8; 4];
        out.copy_from_slice(&self.buf[self.pos..self.pos + 4]);
        self.pos += 4;
        out
    }

    /// Read a fixed 16-byte array (for example, a UUID).
    ///
    /// # Panics
    ///
    /// Panics if fewer than 16 bytes remain.
    #[inline]
    pub fn take_16(&mut self) -> [u8; 16] {
        let mut out = [0u8; 16];
        out.copy_from_slice(&self.buf[self.pos..self.pos + 16]);
        self.pos += 16;
        out
    }

    /// Read a little-endian `u16`.
    ///
    /// # Panics
    ///
    /// Panics if fewer than 2 bytes remain.
    #[inline]
    pub fn take_u16(&mut self) -> u16 {
        let mut b = [0u8; 2];
        b.copy_from_slice(&self.buf[self.pos..self.pos + 2]);
        self.pos += 2;
        u16::from_le_bytes(b)
    }

    /// Read a little-endian `u32`.
    ///
    /// # Panics
    ///
    /// Panics if fewer than 4 bytes remain.
    #[inline]
    pub fn take_u32(&mut self) -> u32 {
        let mut b = [0u8; 4];
        b.copy_from_slice(&self.buf[self.pos..self.pos + 4]);
        self.pos += 4;
        u32::from_le_bytes(b)
    }

    /// Read a little-endian `u64`.
    ///
    /// # Panics
    ///
    /// Panics if fewer than 8 bytes remain.
    #[inline]
    pub fn take_u64(&mut self) -> u64 {
        let mut b = [0u8; 8];
        b.copy_from_slice(&self.buf[self.pos..self.pos + 8]);
        self.pos += 8;
        u64::from_le_bytes(b)
    }

    /// Advance over `count` bytes, typically reserved fields.
    ///
    /// The bounds failure is observed by the next method that slices the
    /// buffer, or immediately by [`Self::remaining`].
    ///
    /// # Panics
    ///
    /// Addition overflows only for unrealistically large `count` values.
    #[inline]
    pub fn skip(&mut self, count: usize) {
        self.pos += count;
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn align8_rounds_up() {
        assert_eq!(align8(0), 0);
        assert_eq!(align8(1), 8);
        assert_eq!(align8(8), 8);
        assert_eq!(align8(9), 16);
        assert_eq!(align8(63), 64);
        assert_eq!(padding_to_align8(0), 0);
        assert_eq!(padding_to_align8(1), 7);
        assert_eq!(padding_to_align8(8), 0);
    }

    #[test]
    fn writer_reader_round_trip() {
        let mut buf = [0u8; 26];
        let mut w = LeWriter::new(&mut buf);
        w.put_bytes(b"TESS");
        w.put_u16(0xBEEF);
        w.put_u32(0xDEAD_BEEF);
        w.put_u64(0x0102_0304_0506_0708);
        w.put_zeros(8);
        assert_eq!(w.position(), 26);

        let mut r = LeReader::new(&buf);
        assert_eq!(&r.take_4(), b"TESS");
        assert_eq!(r.take_u16(), 0xBEEF);
        assert_eq!(r.take_u32(), 0xDEAD_BEEF);
        assert_eq!(r.take_u64(), 0x0102_0304_0506_0708);
        r.skip(8);
        assert_eq!(r.position(), 26);
    }

    #[test]
    fn require_rejects_short_buffer() {
        let buf = [0u8; 4];
        let result = LeReader::require(&buf, "thing", 8);
        assert_eq!(
            result.unwrap_err(),
            BufferTooShort {
                structure: "thing",
                need: 8,
                got: 4,
            }
        );
    }

    #[test]
    fn span_checks() {
        assert!(checked_u64_byte_span(10, 5, 15).is_ok());
        assert!(matches!(
            checked_u64_byte_span(10, 6, 15),
            Err(SpanError::OutOfBounds { end: 16 })
        ));
        assert!(matches!(
            checked_u64_byte_span(u64::MAX, 1, u64::MAX),
            Err(SpanError::AddOverflow)
        ));
        assert_eq!(checked_subslice(2, 3, 10).unwrap(), 2..5);
        assert!(checked_subslice(8, 3, 10).is_err());
    }
}