Skip to main content

fit/
stream.rs

1//! Byte-level cursor over a FIT file slice.
2//!
3//! [`ByteStream`] is a non-owning, position-tracking reader. Each `read_*`
4//! advances the cursor; `peek_*` does not. All multi-byte readers come in
5//! both little-endian and big-endian forms because FIT specifies endianness
6//! per-message (via the architecture byte of each Definition Message).
7
8use crate::error::FitError;
9
10/// Endianness selector for multi-byte reads.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum Endian {
13    Little,
14    Big,
15}
16
17/// A read cursor over a borrowed byte slice.
18#[derive(Debug, Clone)]
19pub struct ByteStream<'a> {
20    bytes: &'a [u8],
21    pos: usize,
22}
23
24impl<'a> ByteStream<'a> {
25    /// Wrap a byte slice. Cursor starts at offset 0.
26    #[inline]
27    pub fn new(bytes: &'a [u8]) -> Self {
28        Self { bytes, pos: 0 }
29    }
30
31    /// Underlying slice (full length, ignoring cursor).
32    #[inline]
33    pub fn as_slice(&self) -> &'a [u8] {
34        self.bytes
35    }
36
37    /// Current cursor offset.
38    #[inline]
39    pub fn position(&self) -> usize {
40        self.pos
41    }
42
43    /// Move the cursor to an absolute offset. Returns an error if past end.
44    pub fn seek(&mut self, offset: usize) -> Result<(), FitError> {
45        if offset > self.bytes.len() {
46            return Err(FitError::UnexpectedEof { offset });
47        }
48        self.pos = offset;
49        Ok(())
50    }
51
52    /// Number of bytes remaining after the cursor.
53    #[inline]
54    pub fn remaining(&self) -> usize {
55        self.bytes.len().saturating_sub(self.pos)
56    }
57
58    /// True if cursor is at or past end of slice.
59    #[inline]
60    pub fn is_empty(&self) -> bool {
61        self.pos >= self.bytes.len()
62    }
63
64    /// Read the next byte without advancing.
65    pub fn peek_u8(&self) -> Result<u8, FitError> {
66        self.bytes
67            .get(self.pos)
68            .copied()
69            .ok_or(FitError::UnexpectedEof { offset: self.pos })
70    }
71
72    /// Read and consume the next byte.
73    pub fn read_u8(&mut self) -> Result<u8, FitError> {
74        let v = self.peek_u8()?;
75        self.pos += 1;
76        Ok(v)
77    }
78
79    /// Read and consume the next `n` bytes as a borrowed sub-slice.
80    pub fn read_bytes(&mut self, n: usize) -> Result<&'a [u8], FitError> {
81        if self.remaining() < n {
82            return Err(FitError::UnexpectedEof { offset: self.pos });
83        }
84        let slice = &self.bytes[self.pos..self.pos + n];
85        self.pos += n;
86        Ok(slice)
87    }
88
89    /// Read and consume an exact `N`-byte array.
90    pub fn read_array<const N: usize>(&mut self) -> Result<[u8; N], FitError> {
91        let slice = self.read_bytes(N)?;
92        // Length is guaranteed; conversion cannot fail.
93        let mut arr = [0u8; N];
94        arr.copy_from_slice(slice);
95        Ok(arr)
96    }
97
98    /// Read u16 in the requested endianness.
99    pub fn read_u16(&mut self, endian: Endian) -> Result<u16, FitError> {
100        let bytes = self.read_array::<2>()?;
101        Ok(match endian {
102            Endian::Little => u16::from_le_bytes(bytes),
103            Endian::Big => u16::from_be_bytes(bytes),
104        })
105    }
106
107    /// Read u32 in the requested endianness.
108    pub fn read_u32(&mut self, endian: Endian) -> Result<u32, FitError> {
109        let bytes = self.read_array::<4>()?;
110        Ok(match endian {
111            Endian::Little => u32::from_le_bytes(bytes),
112            Endian::Big => u32::from_be_bytes(bytes),
113        })
114    }
115
116    /// Convenience: read u16 little-endian.
117    #[inline]
118    pub fn read_u16_le(&mut self) -> Result<u16, FitError> {
119        self.read_u16(Endian::Little)
120    }
121
122    /// Convenience: read u32 little-endian.
123    #[inline]
124    pub fn read_u32_le(&mut self) -> Result<u32, FitError> {
125        self.read_u32(Endian::Little)
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn position_and_remaining() {
135        let mut s = ByteStream::new(&[1, 2, 3, 4, 5]);
136        assert_eq!(s.position(), 0);
137        assert_eq!(s.remaining(), 5);
138        let _ = s.read_u8().unwrap();
139        assert_eq!(s.position(), 1);
140        assert_eq!(s.remaining(), 4);
141    }
142
143    #[test]
144    fn peek_does_not_advance() {
145        let mut s = ByteStream::new(&[42]);
146        assert_eq!(s.peek_u8().unwrap(), 42);
147        assert_eq!(s.position(), 0);
148        assert_eq!(s.read_u8().unwrap(), 42);
149        assert_eq!(s.position(), 1);
150    }
151
152    #[test]
153    fn read_past_end_errors_with_correct_offset() {
154        let mut s = ByteStream::new(&[1, 2]);
155        let _ = s.read_u8().unwrap();
156        let _ = s.read_u8().unwrap();
157        assert_eq!(s.read_u8(), Err(FitError::UnexpectedEof { offset: 2 }),);
158    }
159
160    #[test]
161    fn endianness_is_correct() {
162        let mut s = ByteStream::new(&[0x12, 0x34, 0x56, 0x78]);
163        assert_eq!(s.read_u16(Endian::Little).unwrap(), 0x3412);
164        s.seek(0).unwrap();
165        assert_eq!(s.read_u16(Endian::Big).unwrap(), 0x1234);
166        s.seek(0).unwrap();
167        assert_eq!(s.read_u32(Endian::Little).unwrap(), 0x7856_3412);
168        s.seek(0).unwrap();
169        assert_eq!(s.read_u32(Endian::Big).unwrap(), 0x1234_5678);
170    }
171
172    #[test]
173    fn read_bytes_borrows_correct_range() {
174        let data = [10, 20, 30, 40, 50];
175        let mut s = ByteStream::new(&data);
176        assert_eq!(s.read_bytes(2).unwrap(), &[10, 20]);
177        assert_eq!(s.read_bytes(3).unwrap(), &[30, 40, 50]);
178        assert!(s.is_empty());
179    }
180
181    #[test]
182    fn seek_within_and_at_end_ok() {
183        let mut s = ByteStream::new(&[1, 2, 3]);
184        s.seek(3).unwrap(); // exact end is allowed
185        assert!(s.is_empty());
186        assert_eq!(s.seek(4), Err(FitError::UnexpectedEof { offset: 4 }));
187    }
188}