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}