Skip to main content

leodos_protocols/utils/
ringbuf.rs

1/// Fixed-capacity byte ring buffer for variable-length packets.
2///
3/// Stores packets contiguously with a 2-byte length prefix.
4/// When a packet doesn't fit between the write cursor and the
5/// buffer end, the gap is skipped and the packet wraps to
6/// position 0. Worst case wastes MTU bytes once — same as one
7/// `Deque` slot — but typical utilization is much better since
8/// small packets pack tightly.
9///
10/// When the ring is full, `push` returns `false` and the
11/// packet is dropped (tail-drop policy).
12pub struct RingBuffer<const N: usize> {
13    buf: [u8; N],
14    /// Write cursor (next push position).
15    head: usize,
16    /// Read cursor (next pop position).
17    tail: usize,
18    /// Number of packets currently stored.
19    count: usize,
20}
21
22/// Length prefix size in bytes.
23const LEN_SIZE: usize = 2;
24
25impl<const N: usize> RingBuffer<N> {
26    /// Create a new empty ring buffer.
27    pub const fn new() -> Self {
28        Self {
29            buf: [0u8; N],
30            head: 0,
31            tail: 0,
32            count: 0,
33        }
34    }
35
36    /// Push a packet into the ring. Returns `false` if full.
37    pub fn push(&mut self, data: &[u8]) -> bool {
38        let needed = LEN_SIZE + data.len();
39        if needed > N {
40            return false;
41        }
42
43        // Try to fit between head and end of buffer.
44        let space_to_end = N - self.head;
45        let head = if space_to_end < needed {
46            // Not enough contiguous space — skip the gap.
47            // Check if wrapping would overwrite unread data.
48            if self.count > 0 && self.head >= self.tail {
49                // Gap between head..N is wasted. We need
50                // room at 0..tail for the new packet.
51                if needed > self.tail {
52                    return false;
53                }
54            }
55            // Mark gap with a zero-length sentinel so pop
56            // knows to skip it.
57            self.buf[self.head] = 0;
58            self.buf[self.head + 1] = 0;
59            0
60        } else {
61            self.head
62        };
63
64        // Check for overlap with unread data.
65        if self.count > 0 && head < self.tail && head + needed > self.tail {
66            return false;
67        }
68
69        let len = data.len() as u16;
70        self.buf[head] = (len >> 8) as u8;
71        self.buf[head + 1] = len as u8;
72        self.buf[head + LEN_SIZE..head + needed].copy_from_slice(data);
73        self.head = head + needed;
74        if self.head == N {
75            self.head = 0;
76        }
77        self.count += 1;
78        true
79    }
80
81    /// Peek at the front packet without removing it.
82    pub fn front(&self) -> Option<&[u8]> {
83        if self.count == 0 {
84            return None;
85        }
86        let tail = self.skip_sentinel(self.tail);
87        let len = self.read_len(tail);
88        Some(&self.buf[tail + LEN_SIZE..tail + LEN_SIZE + len])
89    }
90
91    /// Remove the front packet.
92    pub fn pop(&mut self) {
93        if self.count == 0 {
94            return;
95        }
96        let tail = self.skip_sentinel(self.tail);
97        let len = self.read_len(tail);
98        self.tail = tail + LEN_SIZE + len;
99        if self.tail == N {
100            self.tail = 0;
101        }
102        self.count -= 1;
103        if self.count == 0 {
104            self.head = 0;
105            self.tail = 0;
106        }
107    }
108
109    /// Number of packets currently stored.
110    pub fn len(&self) -> usize {
111        self.count
112    }
113
114    /// Returns `true` if the ring contains no packets.
115    pub fn is_empty(&self) -> bool {
116        self.count == 0
117    }
118
119    /// Read the 2-byte big-endian length at position `pos`.
120    fn read_len(&self, pos: usize) -> usize {
121        ((self.buf[pos] as usize) << 8) | self.buf[pos + 1] as usize
122    }
123
124    /// Skip past a zero-length sentinel at `pos`, wrapping
125    /// to 0 if found.
126    fn skip_sentinel(&self, pos: usize) -> usize {
127        if pos + LEN_SIZE <= N && self.read_len(pos) == 0 && pos != self.head {
128            0
129        } else {
130            pos
131        }
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn push_pop_single() {
141        let mut ring = RingBuffer::<64>::new();
142        assert!(ring.push(&[1, 2, 3]));
143        assert_eq!(ring.len(), 1);
144        assert_eq!(ring.front(), Some([1, 2, 3].as_slice()));
145        ring.pop();
146        assert!(ring.is_empty());
147    }
148
149    #[test]
150    fn push_pop_multiple() {
151        let mut ring = RingBuffer::<64>::new();
152        assert!(ring.push(&[10, 20]));
153        assert!(ring.push(&[30, 40, 50]));
154        assert_eq!(ring.len(), 2);
155        assert_eq!(ring.front(), Some([10, 20].as_slice()));
156        ring.pop();
157        assert_eq!(ring.front(), Some([30, 40, 50].as_slice()));
158        ring.pop();
159        assert!(ring.is_empty());
160    }
161
162    #[test]
163    fn wraps_around() {
164        // 16-byte ring. Each packet uses LEN_SIZE + data.
165        let mut ring = RingBuffer::<16>::new();
166        // 2+4 = 6 bytes
167        assert!(ring.push(&[1, 2, 3, 4]));
168        // 2+4 = 6 bytes (total 12)
169        assert!(ring.push(&[5, 6, 7, 8]));
170        // Pop first to free space at the start.
171        ring.pop();
172        // 2+4 = 6 bytes — doesn't fit in remaining 4
173        // bytes, should wrap to start.
174        assert!(ring.push(&[9, 10, 11, 12]));
175        assert_eq!(ring.front(), Some([5, 6, 7, 8].as_slice()));
176        ring.pop();
177        assert_eq!(ring.front(), Some([9, 10, 11, 12].as_slice()));
178        ring.pop();
179        assert!(ring.is_empty());
180    }
181
182    #[test]
183    fn full_returns_false() {
184        let mut ring = RingBuffer::<8>::new();
185        // 2+3 = 5 bytes
186        assert!(ring.push(&[1, 2, 3]));
187        // 2+3 = 5 bytes — only 3 bytes left, won't fit.
188        assert!(!ring.push(&[4, 5, 6]));
189        assert_eq!(ring.len(), 1);
190    }
191
192    #[test]
193    fn too_large_packet() {
194        let mut ring = RingBuffer::<8>::new();
195        // 2+7 = 9 > 8
196        assert!(!ring.push(&[0; 7]));
197    }
198
199    #[test]
200    fn many_small_packets() {
201        let mut ring = RingBuffer::<32>::new();
202        // Each packet: 2 + 1 = 3 bytes. Can fit 10.
203        for i in 0..10u8 {
204            assert!(ring.push(&[i]));
205        }
206        assert_eq!(ring.len(), 10);
207        for i in 0..10u8 {
208            assert_eq!(ring.front(), Some([i].as_slice()));
209            ring.pop();
210        }
211        assert!(ring.is_empty());
212    }
213
214    #[test]
215    fn reuse_after_drain() {
216        let mut ring = RingBuffer::<16>::new();
217        for _ in 0..5 {
218            assert!(ring.push(&[1, 2, 3]));
219            ring.pop();
220        }
221        assert!(ring.is_empty());
222        assert!(ring.push(&[4, 5, 6, 7, 8]));
223        assert_eq!(ring.front(), Some([4, 5, 6, 7, 8].as_slice()));
224    }
225}