Skip to main content

dial9_core/
payload.rs

1//! Segment payload buffer.
2//!
3//! [`Payload`] is the byte container threaded through the processor pipeline.
4//! It owns one or more [`Bytes`] chunks so that processors that produce
5//! a payload by appending (e.g. symbolization, which emits the original
6//! trace plus a symbol table) can do so without copying the original input.
7//! For consumers that need a contiguous view, [`Payload::into_bytes`] /
8//! [`Payload::into_vec`] produce one — fast-pathing zero-copy when the
9//! payload already has a single chunk.
10
11use bytes::{Bytes, BytesMut};
12
13/// A segment payload — zero or more contiguous [`Bytes`] chunks.
14///
15/// Cloning a `Payload` is cheap: each chunk is `Bytes`, which clones via an
16/// `Arc` bump.
17#[derive(Default, Clone)]
18pub struct Payload {
19    chunks: Vec<Bytes>,
20    len: usize,
21}
22
23impl Payload {
24    /// Create an empty payload.
25    pub fn new() -> Self {
26        Self::default()
27    }
28
29    /// Wrap a single `Bytes` as a payload.
30    pub fn from_bytes(b: Bytes) -> Self {
31        let mut p = Self::new();
32        p.push(b);
33        p
34    }
35
36    /// Wrap a `Vec<u8>` as a payload (zero-copy via `Bytes::from`).
37    pub fn from_vec(v: Vec<u8>) -> Self {
38        Self::from_bytes(Bytes::from(v))
39    }
40
41    /// Total byte length across all chunks.
42    pub fn len(&self) -> usize {
43        self.len
44    }
45
46    /// True when the payload has no bytes.
47    pub fn is_empty(&self) -> bool {
48        self.len == 0
49    }
50
51    /// Number of chunks. Useful for tests that want to verify zero-copy.
52    pub fn chunk_count(&self) -> usize {
53        self.chunks.len()
54    }
55
56    /// Borrow the chunk slice. Stable order; may be empty.
57    pub fn chunks(&self) -> &[Bytes] {
58        &self.chunks
59    }
60
61    /// Iterate over chunks in order.
62    pub fn iter(&self) -> std::slice::Iter<'_, Bytes> {
63        self.chunks.iter()
64    }
65
66    /// Append a chunk. Empty chunks are dropped to keep `chunk_count` honest.
67    pub fn push(&mut self, b: Bytes) {
68        if b.is_empty() {
69            return;
70        }
71        self.len += b.len();
72        self.chunks.push(b);
73    }
74
75    /// Check whether the payload begins with `prefix`. Walks across chunk
76    /// boundaries so callers do not have to know the internal layout.
77    pub fn starts_with(&self, prefix: &[u8]) -> bool {
78        if prefix.len() > self.len {
79            return false;
80        }
81        let mut remaining = prefix;
82        for chunk in &self.chunks {
83            if remaining.is_empty() {
84                return true;
85            }
86            let take = remaining.len().min(chunk.len());
87            if chunk[..take] != remaining[..take] {
88                return false;
89            }
90            remaining = &remaining[take..];
91        }
92        remaining.is_empty()
93    }
94
95    /// Concatenate chunks into a single contiguous [`Bytes`].
96    ///
97    /// Fast path: if there is exactly one chunk, it is returned as-is
98    /// (zero-copy). Otherwise a `BytesMut` of `self.len()` is allocated and
99    /// each chunk is copied in order.
100    pub fn into_bytes(mut self) -> Bytes {
101        match self.chunks.len() {
102            0 => Bytes::new(),
103            1 => self.chunks.pop().unwrap(),
104            _ => {
105                let mut out = BytesMut::with_capacity(self.len);
106                for chunk in self.chunks {
107                    out.extend_from_slice(&chunk);
108                }
109                out.freeze()
110            }
111        }
112    }
113
114    /// Concatenate chunks into a single contiguous `Vec<u8>`.
115    pub fn into_vec(self) -> Vec<u8> {
116        let mut out = Vec::with_capacity(self.len);
117        for chunk in &self.chunks {
118            out.extend_from_slice(chunk);
119        }
120        out
121    }
122}
123
124impl From<Bytes> for Payload {
125    fn from(b: Bytes) -> Self {
126        Self::from_bytes(b)
127    }
128}
129
130impl From<Vec<u8>> for Payload {
131    fn from(v: Vec<u8>) -> Self {
132        Self::from_vec(v)
133    }
134}
135
136impl From<&'static [u8]> for Payload {
137    fn from(s: &'static [u8]) -> Self {
138        Self::from_bytes(Bytes::from_static(s))
139    }
140}
141
142impl<const N: usize> From<&'static [u8; N]> for Payload {
143    fn from(s: &'static [u8; N]) -> Self {
144        Self::from_bytes(Bytes::from_static(s))
145    }
146}
147
148impl std::fmt::Debug for Payload {
149    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150        f.debug_struct("Payload")
151            .field("len", &self.len)
152            .field("chunks", &self.chunks.len())
153            .finish()
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160    use assert2::check;
161
162    #[test]
163    fn empty_payload_round_trips() {
164        let p = Payload::new();
165        check!(p.is_empty());
166        check!(p.len() == 0);
167        check!(p.chunk_count() == 0);
168        check!(p.into_vec().is_empty());
169    }
170
171    #[test]
172    fn from_vec_is_single_chunk() {
173        let p = Payload::from_vec(b"hello".to_vec());
174        check!(p.len() == 5);
175        check!(p.chunk_count() == 1);
176    }
177
178    #[test]
179    fn push_drops_empty_chunks() {
180        let mut p = Payload::new();
181        p.push(Bytes::new());
182        p.push(Bytes::from_static(b"abc"));
183        p.push(Bytes::new());
184        check!(p.chunk_count() == 1);
185        check!(p.len() == 3);
186    }
187
188    #[test]
189    fn into_bytes_single_chunk_is_zero_copy() {
190        let original = Bytes::from(vec![1u8, 2, 3, 4, 5]);
191        let original_ptr = original.as_ptr();
192        let p = Payload::from_bytes(original);
193        let out = p.into_bytes();
194        check!(out.as_ptr() == original_ptr);
195    }
196
197    #[test]
198    fn into_bytes_multi_chunk_concatenates() {
199        let mut p = Payload::new();
200        p.push(Bytes::from_static(b"ab"));
201        p.push(Bytes::from_static(b"cde"));
202        p.push(Bytes::from_static(b"f"));
203        check!(p.chunk_count() == 3);
204        let out = p.into_bytes();
205        check!(&out[..] == b"abcdef");
206    }
207
208    #[test]
209    fn starts_with_walks_chunks() {
210        let mut p = Payload::new();
211        p.push(Bytes::from_static(&[0x1f]));
212        p.push(Bytes::from_static(&[0x8b, 0x08]));
213        check!(p.starts_with(&[0x1f, 0x8b]));
214        check!(p.starts_with(&[0x1f, 0x8b, 0x08]));
215        check!(!p.starts_with(&[0x1f, 0x8b, 0x08, 0x00]));
216        check!(!p.starts_with(&[0x1f, 0x00]));
217    }
218
219    #[test]
220    fn starts_with_empty_prefix_is_true() {
221        let p = Payload::from_vec(b"x".to_vec());
222        check!(p.starts_with(&[]));
223        let empty = Payload::new();
224        check!(empty.starts_with(&[]));
225        check!(!empty.starts_with(&[0]));
226    }
227}