Skip to main content

nexus_net/buf/
write_buf.rs

1/// Flat byte slab for outbound protocol frames.
2///
3/// sk_buff headroom model: payload is appended at the tail, protocol
4/// headers are prepended into reserved headroom. The result is one
5/// contiguous [`data()`](WriteBuf::data) slice for the write syscall.
6///
7/// Fixed capacity. No growth.
8///
9/// # Layout
10///
11/// ```text
12/// [ headroom          | payload data         | tailroom    ]
13/// ^                   ^                      ^             ^
14/// 0                   head                   tail          buf.len()
15/// ```
16///
17/// After [`clear()`](WriteBuf::clear): `head = headroom`, `tail = headroom`.
18///
19/// # Modes
20///
21/// **One-shot message** (sk_buff style):
22/// [`prepend()`](WriteBuf::prepend) / [`append()`](WriteBuf::append) to
23/// build a single frame, [`data()`](WriteBuf::data) to send,
24/// [`clear()`](WriteBuf::clear) between frames. This is what most
25/// protocol-frame builders want.
26///
27/// **Cursor FIFO**: [`spare()`](WriteBuf::spare) /
28/// [`filled()`](WriteBuf::filled) to stage bytes (e.g. from a sans-IO
29/// codec writing into the tail region), [`data()`](WriteBuf::data) to
30/// send, [`advance(n)`](WriteBuf::advance) after partial writes.
31/// Auto-reset on full drain means the buffer is reusable across cycles
32/// without an explicit `clear()`. This is what TLS adapters use for
33/// ciphertext staging.
34///
35/// # Examples
36///
37/// ```
38/// use nexus_net::buf::WriteBuf;
39///
40/// let mut wbuf = WriteBuf::new(128, 14);
41///
42/// // Build message: payload first, then header
43/// wbuf.append(b"Hello, world!");
44/// wbuf.prepend(&[0x81, 0x0D]); // WS text frame header
45///
46/// // data() = contiguous [header | payload]
47/// assert_eq!(&wbuf.data()[..2], &[0x81, 0x0D]);
48/// assert_eq!(&wbuf.data()[2..], b"Hello, world!");
49///
50/// // For partial writes:
51/// // let n = socket.write(wbuf.data())?;
52/// // wbuf.advance(n);
53/// ```
54pub struct WriteBuf {
55    buf: Box<[u8]>,
56    head: usize,
57    tail: usize,
58    reset_offset: usize,
59}
60
61impl WriteBuf {
62    /// Create with total capacity and reserved headroom.
63    ///
64    /// Usable tailroom = capacity - headroom.
65    ///
66    /// # Panics
67    /// Panics if `headroom >= capacity`.
68    #[must_use]
69    pub fn new(capacity: usize, headroom: usize) -> Self {
70        assert!(
71            headroom < capacity,
72            "headroom ({headroom}) must be less than capacity ({capacity})"
73        );
74        Self {
75            buf: vec![0u8; capacity].into_boxed_slice(),
76            head: headroom,
77            tail: headroom,
78            reset_offset: headroom,
79        }
80    }
81
82    // =========================================================================
83    // Build outbound data
84    // =========================================================================
85
86    /// Prepend bytes into headroom (protocol headers).
87    /// Moves head backward.
88    ///
89    /// # Panics
90    /// Panics if `src.len() > self.headroom()`.
91    #[inline]
92    pub fn prepend(&mut self, src: &[u8]) {
93        if src.len() > self.headroom() {
94            Self::panic_headroom(src.len(), self.headroom());
95        }
96        let new_head = self.head - src.len();
97        self.buf[new_head..self.head].copy_from_slice(src);
98        self.head = new_head;
99    }
100
101    /// Append bytes at tail (payload data).
102    ///
103    /// # Panics
104    /// Panics if `src.len() > self.tailroom()`.
105    #[inline]
106    pub fn append(&mut self, src: &[u8]) {
107        if src.len() > self.tailroom() {
108            Self::panic_tailroom(src.len(), self.tailroom());
109        }
110        self.buf[self.tail..self.tail + src.len()].copy_from_slice(src);
111        self.tail += src.len();
112    }
113
114    /// Extend the buffer with `n` zeroed bytes at the tail.
115    /// No heap allocation — zeroes directly in the existing buffer.
116    ///
117    /// # Panics
118    /// Panics if `n > self.tailroom()`.
119    #[inline]
120    pub fn extend_zeroed(&mut self, n: usize) {
121        if n > self.tailroom() {
122            Self::panic_tailroom(n, self.tailroom());
123        }
124        self.buf[self.tail..self.tail + n].fill(0);
125        self.tail += n;
126    }
127
128    // =========================================================================
129    // Send side
130    // =========================================================================
131
132    /// Complete outbound data (contiguous: headers + payload).
133    #[inline]
134    pub fn data(&self) -> &[u8] {
135        &self.buf[self.head..self.tail]
136    }
137
138    /// Mutable access to outbound data.
139    /// For in-place operations like XOR masking.
140    #[inline]
141    pub fn data_mut(&mut self) -> &mut [u8] {
142        &mut self.buf[self.head..self.tail]
143    }
144
145    /// Writable tail region for direct in-place writes.
146    ///
147    /// Pair with [`filled()`](Self::filled) to commit bytes after a
148    /// successful write. Used by sans-IO codecs that produce bytes
149    /// directly (e.g. `TlsCodec::write_tls_to(&mut buf.spare())`).
150    ///
151    /// Returns `buf[tail .. buf.len()]`. May be empty if tail has
152    /// reached the buffer boundary.
153    #[inline]
154    pub fn spare(&mut self) -> &mut [u8] {
155        &mut self.buf[self.tail..]
156    }
157
158    /// Commit `n` bytes written into [`spare()`](Self::spare).
159    ///
160    /// # Panics
161    /// Panics if `n` would push tail past the buffer boundary.
162    #[inline]
163    pub fn filled(&mut self, n: usize) {
164        let new_tail = self.tail + n;
165        if new_tail > self.buf.len() {
166            Self::panic_filled(n, self.tail, self.buf.len());
167        }
168        self.tail = new_tail;
169    }
170
171    /// Consume `n` bytes from front after a partial write.
172    ///
173    /// If the buffer becomes empty after advance, resets head and tail
174    /// to `reset_offset` (free — no memmove, just cursor reset). This
175    /// makes WriteBuf usable as a cursor FIFO: encrypt → drain partial →
176    /// encrypt more → drain more, with auto-reclaim when fully drained.
177    /// Calling [`clear()`](Self::clear) after a fully-drained
178    /// `advance()` is now redundant.
179    ///
180    /// # Panics
181    /// Panics if `n > self.len()`.
182    #[inline]
183    pub fn advance(&mut self, n: usize) {
184        if n > self.len() {
185            Self::panic_advance(n, self.len());
186        }
187        self.head += n;
188        if self.head == self.tail {
189            self.head = self.reset_offset;
190            self.tail = self.reset_offset;
191        }
192    }
193
194    // =========================================================================
195    // Capacity queries
196    // =========================================================================
197
198    /// Bytes available for prepend.
199    #[inline]
200    pub fn headroom(&self) -> usize {
201        self.head
202    }
203
204    /// Bytes available for append.
205    #[inline]
206    pub fn tailroom(&self) -> usize {
207        self.buf.len() - self.tail
208    }
209
210    /// Bytes of outbound data.
211    #[inline]
212    pub fn len(&self) -> usize {
213        self.tail - self.head
214    }
215
216    /// Whether the buffer has no outbound data.
217    #[inline]
218    pub fn is_empty(&self) -> bool {
219        self.head == self.tail
220    }
221
222    /// Remove `n` bytes from the end. For Content-Length backfill
223    /// after shifting body bytes left.
224    ///
225    /// # Panics
226    /// Panics if `n > self.len()`.
227    pub fn shrink_tail(&mut self, n: usize) {
228        if n > self.len() {
229            Self::panic_shrink(n, self.len());
230        }
231        self.tail -= n;
232    }
233
234    /// Reset for next message. Cursors return to headroom offset.
235    pub fn clear(&mut self) {
236        self.head = self.reset_offset;
237        self.tail = self.reset_offset;
238    }
239
240    #[cold]
241    #[inline(never)]
242    fn panic_headroom(needed: usize, available: usize) -> ! {
243        panic!("prepend: {needed} bytes exceeds headroom ({available})")
244    }
245
246    #[cold]
247    #[inline(never)]
248    fn panic_tailroom(needed: usize, available: usize) -> ! {
249        panic!("append: {needed} bytes exceeds tailroom ({available})")
250    }
251
252    #[cold]
253    #[inline(never)]
254    fn panic_advance(n: usize, len: usize) -> ! {
255        panic!("advance({n}) exceeds data length ({len})")
256    }
257
258    #[cold]
259    #[inline(never)]
260    fn panic_shrink(n: usize, len: usize) -> ! {
261        panic!("shrink_tail({n}) exceeds data length ({len})")
262    }
263
264    #[cold]
265    #[inline(never)]
266    fn panic_filled(n: usize, tail: usize, end: usize) -> ! {
267        panic!("filled({n}) would exceed buffer (tail={tail}, end={end})")
268    }
269}
270
271/// Write adapter over WriteBuf's spare region.
272///
273/// Implements `std::io::Write` for direct serialization into a
274/// pre-allocated buffer. Used by REST `body_writer` and WS
275/// `encode_text_writer` / `encode_binary_writer`.
276pub struct WriteBufWriter<'a> {
277    buf: &'a mut WriteBuf,
278    written: usize,
279}
280
281impl<'a> WriteBufWriter<'a> {
282    /// Create a writer over the WriteBuf's spare region.
283    pub fn new(buf: &'a mut WriteBuf) -> Self {
284        Self { buf, written: 0 }
285    }
286
287    /// Bytes written so far.
288    pub fn written(&self) -> usize {
289        self.written
290    }
291}
292
293impl std::io::Write for WriteBufWriter<'_> {
294    fn write(&mut self, data: &[u8]) -> std::io::Result<usize> {
295        let available = self.buf.tailroom();
296        if data.len() > available {
297            return Err(std::io::Error::new(
298                std::io::ErrorKind::WriteZero,
299                "write exceeds buffer capacity",
300            ));
301        }
302        self.buf.append(data);
303        self.written += data.len();
304        Ok(data.len())
305    }
306
307    fn flush(&mut self) -> std::io::Result<()> {
308        Ok(())
309    }
310}
311
312#[cfg(test)]
313mod tests {
314    use super::*;
315
316    #[test]
317    fn new_layout() {
318        let buf = WriteBuf::new(128, 14);
319        assert_eq!(buf.buf.len(), 128);
320        assert_eq!(buf.headroom(), 14);
321        assert_eq!(buf.tailroom(), 114);
322        assert!(buf.is_empty());
323    }
324
325    #[test]
326    fn append_data() {
327        let mut buf = WriteBuf::new(128, 14);
328        buf.append(b"Hello");
329        assert_eq!(buf.data(), b"Hello");
330        assert_eq!(buf.len(), 5);
331    }
332
333    #[test]
334    fn prepend_data() {
335        let mut buf = WriteBuf::new(128, 14);
336        buf.append(b"World");
337        buf.prepend(b"Hello");
338        assert_eq!(buf.data(), b"HelloWorld");
339        assert_eq!(buf.len(), 10);
340    }
341
342    #[test]
343    fn prepend_then_append() {
344        let mut buf = WriteBuf::new(128, 14);
345        buf.append(b"payload");
346        buf.prepend(&[0x81, 0x07]); // WS-like header
347        let d = buf.data();
348        assert_eq!(&d[..2], &[0x81, 0x07]);
349        assert_eq!(&d[2..], b"payload");
350    }
351
352    #[test]
353    fn advance_partial_write() {
354        let mut buf = WriteBuf::new(128, 14);
355        buf.append(b"Hello, world!");
356        buf.advance(7);
357        assert_eq!(buf.data(), b"world!");
358        assert_eq!(buf.len(), 6);
359    }
360
361    #[test]
362    fn headroom_tailroom_tracking() {
363        let mut buf = WriteBuf::new(128, 14);
364        assert_eq!(buf.headroom(), 14);
365        assert_eq!(buf.tailroom(), 114);
366
367        buf.append(b"12345");
368        assert_eq!(buf.headroom(), 14);
369        assert_eq!(buf.tailroom(), 109);
370
371        buf.prepend(b"AB");
372        assert_eq!(buf.headroom(), 12);
373        assert_eq!(buf.tailroom(), 109);
374    }
375
376    #[test]
377    fn clear_resets() {
378        let mut buf = WriteBuf::new(128, 14);
379        buf.append(b"data");
380        buf.prepend(b"hdr");
381        buf.clear();
382        assert!(buf.is_empty());
383        assert_eq!(buf.headroom(), 14);
384        assert_eq!(buf.tailroom(), 114);
385    }
386
387    #[test]
388    fn multiple_cycles() {
389        let mut buf = WriteBuf::new(64, 10);
390        for i in 0u8..5 {
391            buf.clear();
392            buf.append(&[i; 4]);
393            buf.prepend(&[0xFF, i]);
394            assert_eq!(buf.len(), 6);
395            assert_eq!(buf.data()[0], 0xFF);
396            assert_eq!(buf.data()[1], i);
397            assert_eq!(&buf.data()[2..], &[i; 4]);
398        }
399    }
400
401    #[test]
402    #[should_panic(expected = "headroom")]
403    fn prepend_exceeds_headroom() {
404        let mut buf = WriteBuf::new(64, 4);
405        buf.prepend(&[0; 8]); // 8 > 4 headroom
406    }
407
408    #[test]
409    #[should_panic(expected = "tailroom")]
410    fn append_exceeds_tailroom() {
411        let mut buf = WriteBuf::new(16, 4);
412        buf.append(&[0; 16]); // only 12 tailroom
413    }
414
415    #[test]
416    #[should_panic(expected = "headroom")]
417    fn headroom_ge_capacity_panics() {
418        let _ = WriteBuf::new(10, 10);
419    }
420
421    #[test]
422    #[should_panic(expected = "advance")]
423    fn advance_exceeds_data() {
424        let mut buf = WriteBuf::new(64, 10);
425        buf.append(b"Hi");
426        buf.advance(5);
427    }
428
429    #[test]
430    fn zero_length_operations() {
431        let mut buf = WriteBuf::new(32, 8);
432        buf.append(b"");
433        buf.prepend(b"");
434        assert!(buf.is_empty());
435        buf.advance(0);
436        assert!(buf.is_empty());
437    }
438
439    #[test]
440    fn advance_auto_resets_on_empty() {
441        let mut buf = WriteBuf::new(64, 10);
442        buf.append(b"Hello");
443        buf.advance(5);
444        assert!(buf.is_empty());
445        // Auto-reset: headroom returns to its initial value, no clear() needed.
446        assert_eq!(buf.headroom(), 10);
447        assert_eq!(buf.tailroom(), 54);
448    }
449
450    #[test]
451    fn advance_partial_does_not_reset() {
452        let mut buf = WriteBuf::new(64, 10);
453        buf.append(b"Hello");
454        buf.advance(2);
455        // Still has data — no reset.
456        assert_eq!(buf.data(), b"llo");
457        assert_eq!(buf.headroom(), 12);
458    }
459
460    #[test]
461    fn cursor_fifo_cycle() {
462        // Demonstrates the cursor-FIFO use case: write into spare,
463        // commit with filled, partially drain via advance, repeat.
464        // Ciphertext-style cycling without per-step memmove.
465        let mut buf = WriteBuf::new(32, 0);
466
467        buf.spare()[..10].copy_from_slice(b"0123456789");
468        buf.filled(10);
469        assert_eq!(buf.data(), b"0123456789");
470
471        buf.advance(4);
472        assert_eq!(buf.data(), b"456789");
473
474        // Spare reflects the post-tail region — head is mid-buffer.
475        assert_eq!(buf.spare().len(), 22);
476        buf.spare()[..3].copy_from_slice(b"ABC");
477        buf.filled(3);
478        assert_eq!(buf.data(), b"456789ABC");
479
480        buf.advance(9);
481        assert!(buf.is_empty());
482        // Auto-reset gave us full tailroom back.
483        assert_eq!(buf.tailroom(), 32);
484    }
485
486    #[test]
487    fn spare_is_post_tail_region() {
488        let mut buf = WriteBuf::new(32, 8);
489        // tail starts at headroom offset → spare is buf[8..32]
490        assert_eq!(buf.spare().len(), 24);
491        buf.append(b"hi");
492        // tail advanced by 2 → spare shrinks by 2
493        assert_eq!(buf.spare().len(), 22);
494    }
495
496    #[test]
497    #[should_panic(expected = "filled")]
498    fn filled_exceeds_buffer() {
499        let mut buf = WriteBuf::new(16, 0);
500        buf.filled(32);
501    }
502}