Skip to main content

zerodds_foundation/
buffer.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Fixed-capacity stack buffer for hot-path allocations.
4//!
5//! `PoolBuffer<CAP>` is an on-stack array-based buffer with
6//! fixed capacity `CAP`. Append operations are O(1) without
7//! touching the heap; overflow is signalled as `PoolBufferError::Overflow`
8//! instead of panicking. `no_std`-capable.
9//!
10//! ## Spec relation
11//!
12//! This module is not an OMG spec mapping — it is an internal
13//! performance primitive for the hot path
14//! `Writer::write`/`Reader::take`, which is meant to work without
15//! a per-sample realloc.
16
17#![allow(clippy::module_name_repetitions)]
18
19// ============================================================================
20// PoolBuffer<CAP> — fixed-capacity byte buffer
21// ============================================================================
22
23/// Fixed-capacity byte buffer that can be filled like `Vec<u8>`
24/// — but does not do a heap realloc. Overflow is reported via
25/// [`Result`], not via `panic!`.
26///
27/// Layout: `[u8; CAP]` + `len: u16` (max 65 535 bytes per buffer).
28/// This is enough for DDS hot-path samples up to 1.5 kB; larger
29/// samples continue to go through the `alloc` path.
30#[derive(Debug)]
31pub struct PoolBuffer<const CAP: usize> {
32    bytes: [u8; CAP],
33    len: u16,
34}
35
36impl<const CAP: usize> PoolBuffer<CAP> {
37    /// Creates an empty buffer.
38    ///
39    /// `CAP` must be `<= u16::MAX as usize` — for larger
40    /// values every mutation is rejected with [`PoolBufferError::CapacityTooLarge`].
41    #[must_use]
42    pub const fn new() -> Self {
43        Self {
44            bytes: [0u8; CAP],
45            len: 0,
46        }
47    }
48
49    /// Current length (bytes written).
50    #[must_use]
51    pub const fn len(&self) -> usize {
52        self.len as usize
53    }
54
55    /// `true` when no bytes have been written.
56    #[must_use]
57    pub const fn is_empty(&self) -> bool {
58        self.len == 0
59    }
60
61    /// Static capacity.
62    #[must_use]
63    pub const fn capacity(&self) -> usize {
64        CAP
65    }
66
67    /// Read-only slice over the written bytes.
68    #[must_use]
69    pub fn as_slice(&self) -> &[u8] {
70        // `len` is always <= CAP by construction — all mutation APIs
71        // cap it.
72        self.bytes.get(..self.len()).unwrap_or(&[])
73    }
74
75    /// Mutable slice over the uninitialized tail (capacity − len).
76    #[must_use]
77    pub fn spare_capacity_mut(&mut self) -> &mut [u8] {
78        let start = self.len();
79        self.bytes.get_mut(start..).unwrap_or(&mut [])
80    }
81
82    /// Resets the contents to `len = 0`. O(1), no memzero.
83    pub fn clear(&mut self) {
84        self.len = 0;
85    }
86
87    /// Appends `data` to the end. Errors if the buffer would be full.
88    ///
89    /// # Errors
90    /// [`PoolBufferError::Overflow`] when `self.len() + data.len() > CAP`.
91    /// [`PoolBufferError::CapacityTooLarge`] when `CAP > u16::MAX`.
92    pub fn extend_from_slice(&mut self, data: &[u8]) -> Result<(), PoolBufferError> {
93        if CAP > u16::MAX as usize {
94            return Err(PoolBufferError::CapacityTooLarge);
95        }
96        let needed = self
97            .len()
98            .checked_add(data.len())
99            .ok_or(PoolBufferError::Overflow)?;
100        if needed > CAP {
101            return Err(PoolBufferError::Overflow);
102        }
103        let start = self.len();
104        let dst = self
105            .bytes
106            .get_mut(start..needed)
107            .ok_or(PoolBufferError::Overflow)?;
108        dst.copy_from_slice(data);
109        // needed <= CAP <= u16::MAX, no truncation risk.
110        self.len = needed as u16;
111        Ok(())
112    }
113
114    /// Writes a single byte. Errors when full.
115    ///
116    /// # Errors
117    /// [`PoolBufferError::Overflow`] when the buffer is full.
118    pub fn push(&mut self, byte: u8) -> Result<(), PoolBufferError> {
119        self.extend_from_slice(&[byte])
120    }
121
122    /// Sets the length explicitly. Used after a `spare_capacity_mut`
123    /// write access by a codec backend.
124    ///
125    /// # Errors
126    /// [`PoolBufferError::Overflow`] when `new_len > CAP`.
127    pub fn set_len(&mut self, new_len: usize) -> Result<(), PoolBufferError> {
128        if new_len > CAP || CAP > u16::MAX as usize {
129            return Err(PoolBufferError::Overflow);
130        }
131        // The cast is safe after the range validation.
132        self.len = new_len as u16;
133        Ok(())
134    }
135}
136
137impl<const CAP: usize> Default for PoolBuffer<CAP> {
138    fn default() -> Self {
139        Self::new()
140    }
141}
142
143impl<const CAP: usize> AsRef<[u8]> for PoolBuffer<CAP> {
144    fn as_ref(&self) -> &[u8] {
145        self.as_slice()
146    }
147}
148
149// ============================================================================
150// Error
151// ============================================================================
152
153/// Error family for `PoolBuffer`.
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub enum PoolBufferError {
156    /// A write operation would exceed the static capacity.
157    Overflow,
158    /// `CAP > u16::MAX`. This variant exists because `len` is
159    /// modeled as a `u16`.
160    CapacityTooLarge,
161}
162
163impl core::fmt::Display for PoolBufferError {
164    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
165        match self {
166            Self::Overflow => f.write_str("pool buffer overflow"),
167            Self::CapacityTooLarge => f.write_str("CAP exceeds u16::MAX"),
168        }
169    }
170}
171
172#[cfg(feature = "std")]
173impl std::error::Error for PoolBufferError {}
174
175// ============================================================================
176// ============================================================================
177// Tests
178// ============================================================================
179
180#[cfg(test)]
181#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
182mod tests {
183    use super::*;
184
185    #[test]
186    fn empty_buffer_has_len_zero() {
187        let b: PoolBuffer<128> = PoolBuffer::new();
188        assert_eq!(b.len(), 0);
189        assert!(b.is_empty());
190        assert_eq!(b.capacity(), 128);
191        assert_eq!(b.as_slice(), &[]);
192    }
193
194    #[test]
195    fn extend_appends_and_tracks_len() {
196        let mut b: PoolBuffer<16> = PoolBuffer::new();
197        b.extend_from_slice(b"hello").unwrap();
198        assert_eq!(b.len(), 5);
199        assert_eq!(b.as_slice(), b"hello");
200        b.extend_from_slice(b" world").unwrap();
201        assert_eq!(b.as_slice(), b"hello world");
202    }
203
204    #[test]
205    fn extend_overflow_returns_error_no_partial_write() {
206        let mut b: PoolBuffer<8> = PoolBuffer::new();
207        b.extend_from_slice(b"1234").unwrap();
208        let err = b.extend_from_slice(b"56789").unwrap_err();
209        assert_eq!(err, PoolBufferError::Overflow);
210        assert_eq!(b.as_slice(), b"1234");
211    }
212
213    #[test]
214    fn push_byte_works() {
215        let mut b: PoolBuffer<4> = PoolBuffer::new();
216        b.push(0xCA).unwrap();
217        b.push(0xFE).unwrap();
218        assert_eq!(b.as_slice(), &[0xCA, 0xFE]);
219    }
220
221    #[test]
222    fn push_overflow_errors() {
223        let mut b: PoolBuffer<2> = PoolBuffer::new();
224        b.push(1).unwrap();
225        b.push(2).unwrap();
226        assert_eq!(b.push(3).unwrap_err(), PoolBufferError::Overflow);
227    }
228
229    #[test]
230    fn clear_resets_len_only() {
231        let mut b: PoolBuffer<16> = PoolBuffer::new();
232        b.extend_from_slice(b"abc").unwrap();
233        b.clear();
234        assert_eq!(b.len(), 0);
235        assert_eq!(b.as_slice(), &[]);
236        b.extend_from_slice(b"xyz").unwrap();
237        assert_eq!(b.as_slice(), b"xyz");
238    }
239
240    #[test]
241    fn spare_capacity_then_set_len() {
242        let mut b: PoolBuffer<32> = PoolBuffer::new();
243        let spare = b.spare_capacity_mut();
244        assert_eq!(spare.len(), 32);
245        spare[0..3].copy_from_slice(&[1, 2, 3]);
246        b.set_len(3).unwrap();
247        assert_eq!(b.as_slice(), &[1, 2, 3]);
248    }
249
250    #[test]
251    fn set_len_overflow_errors() {
252        let mut b: PoolBuffer<8> = PoolBuffer::new();
253        assert_eq!(b.set_len(9).unwrap_err(), PoolBufferError::Overflow);
254    }
255
256    #[test]
257    fn cap_above_u16_max_extend_errors() {
258        let mut b: PoolBuffer<{ u16::MAX as usize + 1 }> = PoolBuffer::new();
259        let err = b.extend_from_slice(&[0u8]).unwrap_err();
260        assert_eq!(err, PoolBufferError::CapacityTooLarge);
261    }
262
263    #[test]
264    fn error_display_strings() {
265        let s = std::format!("{}", PoolBufferError::Overflow);
266        assert!(s.contains("overflow"));
267        let s = std::format!("{}", PoolBufferError::CapacityTooLarge);
268        assert!(s.contains("u16"));
269    }
270}