Skip to main content

tinyboot_core/
ringbuf.rs

1/// Fixed-size ring buffer.
2///
3/// Designed for use with `PageWriter` where `N = 2 * PAGE_SIZE`.
4/// When consumed in PAGE_SIZE-aligned chunks, `peek` always returns
5/// a contiguous slice (no wrap on the read side).
6#[repr(align(4))]
7pub struct RingBuf<const N: usize> {
8    buf: core::mem::MaybeUninit<[u8; N]>,
9    head: usize,
10    tail: usize,
11}
12
13impl<const N: usize> Default for RingBuf<N> {
14    fn default() -> Self {
15        Self {
16            buf: core::mem::MaybeUninit::uninit(),
17            head: 0,
18            tail: 0,
19        }
20    }
21}
22
23impl<const N: usize> RingBuf<N> {
24    /// Bytes available to read.
25    #[inline(always)]
26    pub fn len(&self) -> usize {
27        self.head.wrapping_sub(self.tail) % N
28    }
29
30    /// Returns true if the buffer is empty.
31    #[inline(always)]
32    pub fn is_empty(&self) -> bool {
33        self.head == self.tail
34    }
35
36    /// Push bytes into the buffer, handling wrap-around.
37    /// Returns the number of bytes actually written (short if full).
38    pub fn push(&mut self, data: &[u8]) -> usize {
39        let free = N - 1 - self.len();
40        let n = data.len().min(free);
41
42        let ptr = self.buf.as_mut_ptr() as *mut u8;
43        let mut head = self.head;
44        for i in 0..n {
45            // SAFETY: i < n <= data.len(), head < N
46            unsafe { *ptr.add(head) = *data.get_unchecked(i) };
47            head += 1;
48            if head == N {
49                head = 0;
50            }
51        }
52        self.head = head;
53        n
54    }
55
56    /// Borrow up to `n` contiguous readable bytes without consuming.
57    ///
58    /// Returns fewer than `n` bytes only if fewer are available.
59    /// When `N = 2 * PAGE_SIZE` and you consume in PAGE_SIZE chunks,
60    /// this always returns a contiguous slice (read side never wraps mid-page).
61    pub fn peek(&self, n: usize) -> &[u8] {
62        let available = self.len().min(n);
63        // SAFETY: tail + available <= N, guaranteed by len() and consume() modulo N
64        unsafe {
65            core::slice::from_raw_parts(self.buf.as_ptr().cast::<u8>().add(self.tail), available)
66        }
67    }
68
69    /// Advance the read pointer by `n` bytes.
70    pub fn consume(&mut self, n: usize) {
71        self.tail = (self.tail + n) % N;
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn empty() {
81        let buf = RingBuf::<8>::default();
82        assert_eq!(buf.len(), 0);
83        assert_eq!(buf.peek(8), &[]);
84    }
85
86    #[test]
87    fn push_and_peek() {
88        let mut buf = RingBuf::<8>::default();
89        assert_eq!(buf.push(&[1, 2, 3]), 3);
90        assert_eq!(buf.len(), 3);
91        assert_eq!(buf.peek(3), &[1, 2, 3]);
92    }
93
94    #[test]
95    fn consume_advances() {
96        let mut buf = RingBuf::<8>::default();
97        buf.push(&[1, 2, 3, 4]);
98        buf.consume(2);
99        assert_eq!(buf.len(), 2);
100        assert_eq!(buf.peek(2), &[3, 4]);
101    }
102
103    #[test]
104    fn push_wraps() {
105        let mut buf = RingBuf::<8>::default();
106        buf.push(&[1, 2, 3, 4, 5]);
107        buf.consume(4); // tail=4, head=5
108        assert_eq!(buf.push(&[6, 7, 8, 9]), 4); // wraps: 6,7,8 at end, 9 at start
109        assert_eq!(buf.len(), 5);
110        assert_eq!(buf.peek(1), &[5]);
111    }
112
113    #[test]
114    fn full_returns_short() {
115        let mut buf = RingBuf::<4>::default();
116        assert_eq!(buf.push(&[1, 2, 3]), 3); // capacity is N-1 = 3
117        assert_eq!(buf.push(&[4]), 0); // full
118        assert_eq!(buf.len(), 3);
119    }
120
121    #[test]
122    fn page_aligned_peek_is_contiguous() {
123        // Simulates PageWriter pattern: N=8, PAGE_SIZE=4
124        let mut buf = RingBuf::<8>::default();
125
126        // Fill first page
127        buf.push(&[1, 2, 3, 4]);
128        assert_eq!(buf.peek(4), &[1, 2, 3, 4]);
129        buf.consume(4);
130
131        // Fill second page
132        buf.push(&[5, 6, 7, 8]);
133        assert_eq!(buf.peek(4), &[5, 6, 7, 8]);
134        buf.consume(4);
135
136        // Back to first page slot
137        buf.push(&[9, 10, 11, 12]);
138        assert_eq!(buf.peek(4), &[9, 10, 11, 12]);
139    }
140}