Skip to main content

datagram_socket/
buffer.rs

1// Copyright (C) 2026, Cloudflare, Inc.
2// All rights reserved.
3//
4// Redistribution and use in source and binary forms, with or without
5// modification, are permitted provided that the following conditions are
6// met:
7//
8//     * Redistributions of source code must retain the above copyright notice,
9//       this list of conditions and the following disclaimer.
10//
11//     * Redistributions in binary form must reproduce the above copyright
12//       notice, this list of conditions and the following disclaimer in the
13//       documentation and/or other materials provided with the distribution.
14//
15// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
16// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
17// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
18// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
19// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
20// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
21// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
22// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
23// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
24// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
27/// A simple buffer for working with datagrams.
28///
29/// This is a thin wrapper around a `Vec<u8>` but it maintains an offset / read
30/// position into the vector to the actual start of the datagram. This enables
31/// users to preallocate headroom in front of the actual datagram to prepend
32/// additional headers, or to advance the cursor to consume such prefixes.
33#[derive(Default, Clone)]
34pub struct DgramBuffer {
35    data: Vec<u8>,
36    start: usize,
37}
38
39impl DgramBuffer {
40    /// Creates an empty `DgramBuffer` with no allocated capacity.
41    pub fn new() -> Self {
42        Default::default()
43    }
44
45    /// Creates a `DgramBuffer` by copying `data`; the read cursor starts at
46    /// the beginning.
47    pub fn from_slice(data: &[u8]) -> Self {
48        DgramBuffer {
49            data: data.into(),
50            start: 0,
51        }
52    }
53
54    /// Creates an empty `DgramBuffer` pre-allocated for at least `capacity`
55    /// bytes.
56    pub fn with_capacity(capacity: usize) -> Self {
57        DgramBuffer {
58            data: Vec::with_capacity(capacity),
59            start: 0,
60        }
61    }
62
63    /// Creates a `DgramBuffer` pre-allocated for `capacity` bytes with
64    /// `headroom` zero bytes reserved at the front for later prefix insertion
65    /// via [`try_add_prefix`]. The read cursor is positioned after the
66    /// headroom. Panics if `headroom > capacity`.
67    ///
68    /// [`try_add_prefix`]: DgramBuffer::try_add_prefix
69    pub fn with_capacity_and_headroom(capacity: usize, headroom: usize) -> Self {
70        assert!(capacity >= headroom);
71        let mut v = Vec::with_capacity(capacity);
72        v.resize(headroom, 0);
73        DgramBuffer {
74            data: v,
75            start: headroom,
76        }
77    }
78
79    /// Wraps an existing `Vec<u8>`, treating the first `headroom` bytes as
80    /// reserved prefix space. The read cursor is positioned after the
81    /// headroom. Panics if `headroom > v.len()`.
82    pub fn from_vec_with_headroom(v: Vec<u8>, headroom: usize) -> Self {
83        assert!(headroom <= v.len());
84        DgramBuffer {
85            data: v,
86            start: headroom,
87        }
88    }
89
90    /// Truncates the readable portion to `count` bytes, discarding any data
91    /// beyond that point. If count is greater or equal to the buffer’s current
92    /// length, this has no effect.
93    pub fn truncate(&mut self, count: usize) {
94        self.data.truncate(self.start + count);
95    }
96
97    /// Advances the cursor by `count` bytes; panics if `count` exceeds
98    /// the number of readable bytes.
99    pub fn advance(&mut self, count: usize) {
100        assert!(self.start + count <= self.data.len());
101        self.start += count;
102    }
103
104    /// Prepends `prefix` into the headroom region, sliding the read cursor
105    /// backwards. Returns `Err(())` if the headroom is smaller than
106    /// `prefix.len()`.
107    #[allow(
108        clippy::result_unit_err,
109        reason = "There is only a single error case, adding a custom error type doesn't make sense"
110    )]
111    pub fn try_add_prefix(&mut self, prefix: &[u8]) -> Result<(), ()> {
112        if self.start < prefix.len() {
113            return Err(());
114        }
115
116        self.start -= prefix.len();
117        self.data[self.start..self.start + prefix.len()].copy_from_slice(prefix);
118        Ok(())
119    }
120
121    /// Ensures that at least `headroom` bytes of headroom are available,
122    /// potentially shifting elements and reallocating. If `headroom` is
123    /// less than the existing headroom, this method does nothing.
124    pub fn splice_headroom(&mut self, headroom: usize) {
125        if self.start >= headroom {
126            return;
127        }
128        self.data
129            .splice(0..self.start, std::iter::repeat_n(0u8, headroom));
130        self.start = headroom;
131    }
132
133    /// Resets the buffer to an empty state, dropping all data and headroom.
134    pub fn clear(&mut self) {
135        self.start = 0;
136        self.data.clear();
137    }
138
139    /// Returns the number of readable bytes (i.e. `data.len() - pos`).
140    pub fn len(&self) -> usize {
141        self.data.len() - self.start
142    }
143
144    /// Returns `true` if there are no readable bytes.
145    pub fn is_empty(&self) -> bool {
146        self.len() == 0
147    }
148
149    /// Returns the readable bytes as a slice (i.e. `data[pos..]`).
150    pub fn as_slice(&self) -> &[u8] {
151        &self.data[self.start..]
152    }
153
154    /// Returns the readable bytes as a mutable slice (i.e. `data[pos..]`).
155    pub fn as_mut_slice(&mut self) -> &mut [u8] {
156        &mut self.data[self.start..]
157    }
158
159    /// Returns the number of bytes the underlying `Vec` can accept before
160    /// reallocating (i.e. `capacity - len`).
161    pub fn spare_capacity(&self) -> usize {
162        self.data.capacity() - self.data.len()
163    }
164
165    /// Consumes the buffer and returns the inner `Vec<u8>` and the current
166    /// read position as `(data, pos)`.
167    pub fn into_parts(self) -> (Vec<u8>, usize) {
168        (self.data, self.start)
169    }
170}
171
172impl AsRef<[u8]> for DgramBuffer {
173    fn as_ref(&self) -> &[u8] {
174        self.as_slice()
175    }
176}
177
178impl AsMut<[u8]> for DgramBuffer {
179    fn as_mut(&mut self) -> &mut [u8] {
180        self.as_mut_slice()
181    }
182}
183
184impl std::fmt::Debug for DgramBuffer {
185    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
186        if self.is_empty() {
187            return f.write_str("[]");
188        }
189
190        write!(f, "[0x")?;
191        // Render payload bytes as two-char hex, underscore-separated after every
192        // 4.
193        for (i, byte) in self.as_slice().iter().enumerate() {
194            if i > 0 && i % 4 == 0 {
195                f.write_str("_")?;
196            }
197            write!(f, "{:02x}", byte)?;
198        }
199        write!(f, ", len={}]", self.len())?;
200
201        Ok(())
202    }
203}
204
205// SAFETY: All required methods delegate directly to `Vec<u8>`'s trusted
206// `BufMut` implementation.
207unsafe impl bytes::BufMut for DgramBuffer {
208    fn remaining_mut(&self) -> usize {
209        self.data.remaining_mut()
210    }
211
212    unsafe fn advance_mut(&mut self, cnt: usize) {
213        // SAFETY: We trust `Vec<u8>`'s `BufMut` implementation.
214        self.data.advance_mut(cnt);
215    }
216
217    fn chunk_mut(&mut self) -> &mut bytes::buf::UninitSlice {
218        self.data.chunk_mut()
219    }
220
221    // Forwarding these provided methods of `BufMut`, because the
222    // implementation on `Vec<u8>` has specialization for them.
223    fn put<T: bytes::Buf>(&mut self, src: T)
224    where
225        Self: Sized,
226    {
227        self.data.put(src);
228    }
229
230    fn put_slice(&mut self, src: &[u8]) {
231        self.data.put_slice(src);
232    }
233
234    fn put_bytes(&mut self, val: u8, cnt: usize) {
235        self.data.put_bytes(val, cnt);
236    }
237}
238
239impl From<Vec<u8>> for DgramBuffer {
240    fn from(v: Vec<u8>) -> Self {
241        DgramBuffer { data: v, start: 0 }
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use bytes::BufMut;
248
249    use super::*;
250
251    #[test]
252    fn new_is_empty() {
253        let b = DgramBuffer::new();
254        assert_eq!(b.as_slice(), &[]);
255        assert_eq!(b.len(), 0);
256        assert!(b.is_empty());
257    }
258
259    #[test]
260    fn from_slice_copies_data() {
261        let b = DgramBuffer::from_slice(&[1, 2, 3]);
262        assert_eq!(b.as_slice(), &[1, 2, 3]);
263        assert_eq!(b.len(), 3);
264        assert!(!b.is_empty());
265    }
266
267    #[test]
268    fn with_capacity_is_empty_with_allocation() {
269        let b = DgramBuffer::with_capacity(64);
270        assert_eq!(b.as_slice(), &[]);
271        assert_eq!(b.spare_capacity(), 64);
272        assert!(b.is_empty());
273    }
274
275    #[test]
276    fn with_capacity_and_headroom_positions_cursor() {
277        let b = DgramBuffer::with_capacity_and_headroom(16, 4);
278        assert_eq!(b.as_slice(), &[]);
279        assert_eq!(b.len(), 0);
280        assert_eq!(b.spare_capacity(), 12);
281    }
282
283    #[test]
284    fn from_vec_with_headroom_exposes_payload() {
285        let v = vec![0u8, 0, 0, 1, 2, 3];
286        let b = DgramBuffer::from_vec_with_headroom(v, 3);
287        assert_eq!(b.as_slice(), &[1, 2, 3]);
288        assert_eq!(b.len(), 3);
289    }
290
291    #[test]
292    fn truncate_shortens_readable_view() {
293        let mut b = DgramBuffer::from_slice(&[1, 2, 3, 4, 5]);
294        b.truncate(3);
295        assert_eq!(b.as_slice(), &[1, 2, 3]);
296        assert_eq!(b.len(), 3);
297    }
298
299    #[test]
300    fn advance_moves_cursor() {
301        let mut b = DgramBuffer::from_slice(&[1, 2, 3, 4]);
302        b.advance(2);
303        assert_eq!(b.as_slice(), &[3, 4]);
304        assert_eq!(b.len(), 2);
305    }
306
307    #[test]
308    #[should_panic]
309    fn advance_past_end_panics() {
310        let mut b = DgramBuffer::from_slice(&[1, 2]);
311        b.advance(3);
312    }
313
314    #[test]
315    fn try_add_prefix() {
316        let mut b = DgramBuffer::with_capacity_and_headroom(8, 4);
317        b.put_slice(&[0xaa, 0xbb]);
318        b.try_add_prefix(&[0x01, 0x02]).unwrap();
319        assert_eq!(b.as_slice(), &[0x01, 0x02, 0xaa, 0xbb]);
320        assert_eq!(b.len(), 4);
321        b.try_add_prefix(&[0x42, 0x23]).unwrap();
322        assert_eq!(b.as_slice(), &[0x42, 0x23, 0x01, 0x02, 0xaa, 0xbb]);
323        assert_eq!(b.len(), 6);
324        assert!(b.try_add_prefix(&[0x01]).is_err());
325    }
326
327    #[test]
328    fn try_add_prefix_fails_when_headroom_insufficient() {
329        let mut b = DgramBuffer::with_capacity_and_headroom(8, 1);
330        assert!(b.try_add_prefix(&[0x01, 0x02]).is_err());
331    }
332
333    #[test]
334    fn clear_resets_to_empty() {
335        let mut b = DgramBuffer::from_slice(&[1, 2, 3]);
336        b.clear();
337        assert_eq!(b.as_slice(), &[]);
338        assert_eq!(b.len(), 0);
339        assert!(b.is_empty());
340    }
341
342    #[test]
343    fn from_vec() {
344        let b = DgramBuffer::from(vec![7, 8, 9]);
345        assert_eq!(b.as_slice(), &[7, 8, 9]);
346    }
347
348    #[test]
349    fn into_parts_returns_data_and_pos() {
350        let mut b = DgramBuffer::with_capacity_and_headroom(8, 3);
351        b.put_slice(&[1, 2, 3]);
352        let (data, pos) = b.into_parts();
353        assert_eq!(pos, 3);
354        assert_eq!(&data, &[0, 0, 0, 1, 2, 3]);
355    }
356
357    #[test]
358    fn bufmut_put_slice_appends() {
359        let mut b = DgramBuffer::new();
360        b.put_slice(&[0x0a, 0x0b, 0x0c]);
361        assert_eq!(b.as_slice(), &[0x0a, 0x0b, 0x0c]);
362    }
363
364    #[test]
365    fn as_ref_matches_as_slice() {
366        let b = DgramBuffer::from_slice(&[1, 2, 3]);
367        assert_eq!(b.as_ref(), b.as_slice());
368    }
369
370    #[test]
371    fn as_mut_slice_allows_mutation() {
372        let mut b = DgramBuffer::from_slice(&[1, 2, 3]);
373        b.as_mut_slice()[1] = 0xff;
374        assert_eq!(b.as_slice(), &[1, 0xff, 3]);
375    }
376
377    #[test]
378    fn as_mut_via_trait_allows_mutation() {
379        let mut b = DgramBuffer::from_slice(&[1, 2, 3]);
380        b.as_mut()[1] = 0xff;
381        assert_eq!(b.as_slice(), &[1, 0xff, 3]);
382    }
383
384    // Debug format
385
386    #[test]
387    fn debug_empty() {
388        let b = DgramBuffer::new();
389        assert_eq!(format!("{:?}", b), "[]");
390    }
391
392    #[test]
393    fn debug_payload_hex_with_group_separator() {
394        let b = DgramBuffer::from_slice(&[
395            0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
396        ]);
397        assert_eq!(format!("{:?}", b), "[0x00010203_04050607_08, len=9]");
398    }
399
400    #[test]
401    fn debug_payload_exact_group_boundary() {
402        let b = DgramBuffer::from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]);
403        assert_eq!(format!("{:?}", b), "[0xaabbccdd, len=4]");
404    }
405
406    /// When existing headroom already satisfies the request, nothing changes.
407    #[test]
408    fn splice_headroom_noop() {
409        // 4 bytes of headroom, 3 bytes of payload.
410        let mut b = DgramBuffer::with_capacity_and_headroom(16, 4);
411        b.put_slice(&[1, 2, 3]);
412        let (data_before, pos_before) = b.into_parts();
413
414        let mut b =
415            DgramBuffer::from_vec_with_headroom(data_before.clone(), pos_before);
416        b.splice_headroom(3); // less than existing -- noop
417        let (data_after, pos_after) = b.clone().into_parts();
418        assert_eq!(pos_after, pos_before);
419        assert_eq!(data_after, data_before);
420
421        b.splice_headroom(4); // same as existing -- noop
422        assert_eq!(pos_after, pos_before);
423        assert_eq!(data_after, data_before);
424    }
425
426    /// When there is no existing headroom, splice_headroom inserts the
427    /// requested number of zero bytes at the front.
428    #[test]
429    fn splice_headroom_inserts_headroom_when_none_exists() {
430        // from_slice starts with pos=0 (no headroom).
431        let mut b = DgramBuffer::from_slice(&[1, 2, 3]);
432        b.splice_headroom(4);
433
434        // pos advances to 4; payload is unchanged.
435        assert_eq!(b.as_slice(), &[1, 2, 3]);
436        let (data, pos) = b.into_parts();
437        assert_eq!(pos, 4);
438        // The first four bytes are the new headroom zeros; payload follows.
439        assert_eq!(&data, &[0, 0, 0, 0, 1, 2, 3]);
440    }
441
442    /// When headroom is smaller than requested, the gap is filled with zeros
443    /// and the prefix region is correctly sized.
444    #[test]
445    fn splice_headroom_grows_insufficient_headroom() {
446        // 2 bytes of headroom, 3 bytes of payload.
447        let mut b = DgramBuffer::with_capacity_and_headroom(16, 2);
448        b.put_slice(&[10, 20, 30]);
449        // pos=2, request 6 bytes of headroom.
450        b.splice_headroom(6);
451
452        assert_eq!(b.as_slice(), &[10, 20, 30]);
453        let (data, pos) = b.into_parts();
454        assert_eq!(pos, 6);
455        assert_eq!(&data[..6], &[0u8; 6]);
456        assert_eq!(&data[6..], &[10, 20, 30]);
457    }
458}