Skip to main content

spvirit_codec/
segment.rs

1//! Reassembly of segmented PVA messages.
2
3use crate::error::{DecodeError, DecodeResult};
4
5/// Default ceiling on a reassembled message, in bytes. Chosen to admit large
6/// NTNDArray frames while keeping the rewritten `payload_length` inside `u32`.
7pub const DEFAULT_MAX_MESSAGE_BYTES: usize = 268_435_456; // 256 MiB
8
9/// What [`SegmentReassembler::push`] did with the frame it was given.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum SegmentOutcome {
12    /// A complete message: the first segment's header with the segment bits
13    /// cleared and `payload_length` rewritten to the concatenated total,
14    /// followed by the concatenated payloads.
15    Complete(Vec<u8>),
16    /// A first or middle segment was absorbed. Push more frames.
17    Pending,
18    /// A control frame, returned verbatim. Reassembly state is untouched —
19    /// control frames are legal between the segments of one message.
20    Control(Vec<u8>),
21}
22
23struct Pending {
24    header: [u8; 8],
25    payloads: Vec<Vec<u8>>,
26    total: usize,
27}
28
29/// Reassembles segmented PVA messages.
30///
31/// Sans-io: the caller reads a header and payload off the wire and pushes
32/// them in. One instance per connection, because a message's segments may be
33/// separated by control frames that the caller handles in between.
34pub struct SegmentReassembler {
35    max_bytes: usize,
36    pending: Option<Pending>,
37}
38
39impl SegmentReassembler {
40    /// A reassembler with the [`DEFAULT_MAX_MESSAGE_BYTES`] cap.
41    pub fn new() -> Self {
42        Self::with_max_bytes(DEFAULT_MAX_MESSAGE_BYTES)
43    }
44
45    /// A reassembler with a custom cap on the reassembled message size.
46    pub fn with_max_bytes(max_bytes: usize) -> Self {
47        Self { max_bytes, pending: None }
48    }
49
50    /// Bytes of payload currently held for an in-progress message.
51    pub fn pending_bytes(&self) -> usize {
52        self.pending.as_ref().map_or(0, |p| p.total)
53    }
54
55    /// Discard any in-progress message. Call this when the connection resets.
56    pub fn reset(&mut self) {
57        self.pending = None;
58    }
59
60    /// Feed one frame. See [`SegmentOutcome`].
61    ///
62    /// On any `Err` the in-progress message is discarded and the reassembler
63    /// is ready for the next one; the caller decides whether to keep the
64    /// connection.
65    pub fn push(&mut self, header: [u8; 8], payload: Vec<u8>) -> DecodeResult<SegmentOutcome> {
66        let flags = header[2];
67
68        if (flags & 0x01) != 0 {
69            let mut out = Vec::with_capacity(8 + payload.len());
70            out.extend_from_slice(&header);
71            out.extend_from_slice(&payload);
72            return Ok(SegmentOutcome::Control(out));
73        }
74
75        let command = header[3];
76        match (flags & 0x30) >> 4 {
77            // Unsegmented.
78            0 => {
79                if let Some(p) = self.pending.take() {
80                    return Err(DecodeError::SegmentInterrupted {
81                        expected: p.header[3],
82                        got: command,
83                    });
84                }
85                let mut out = Vec::with_capacity(8 + payload.len());
86                out.extend_from_slice(&header);
87                out.extend_from_slice(&payload);
88                Ok(SegmentOutcome::Complete(out))
89            }
90            // First segment.
91            1 => {
92                if self.pending.take().is_some() {
93                    return Err(DecodeError::UnexpectedSegment { flags });
94                }
95                if payload.len() > self.max_bytes {
96                    return Err(DecodeError::MessageTooLarge {
97                        total: payload.len(),
98                        limit: self.max_bytes,
99                    });
100                }
101                self.pending = Some(Pending {
102                    header,
103                    total: payload.len(),
104                    payloads: vec![payload],
105                });
106                Ok(SegmentOutcome::Pending)
107            }
108            // Last (2) or middle (3).
109            code => {
110                let mut p = match self.pending.take() {
111                    Some(p) => p,
112                    None => return Err(DecodeError::UnexpectedSegment { flags }),
113                };
114                if p.header[3] != command || (p.header[2] & 0x40) != (flags & 0x40) {
115                    return Err(DecodeError::SegmentCommandMismatch {
116                        expected: p.header[3],
117                        got: command,
118                    });
119                }
120                let total = p.total + payload.len();
121                if total > self.max_bytes {
122                    return Err(DecodeError::MessageTooLarge { total, limit: self.max_bytes });
123                }
124                p.total = total;
125                p.payloads.push(payload);
126                if code == 2 {
127                    Ok(SegmentOutcome::Complete(finish(p)))
128                } else {
129                    self.pending = Some(p);
130                    Ok(SegmentOutcome::Pending)
131                }
132            }
133        }
134    }
135}
136
137impl Default for SegmentReassembler {
138    fn default() -> Self {
139        Self::new()
140    }
141}
142
143/// Rebuild a standalone message from the first segment's header.
144fn finish(p: Pending) -> Vec<u8> {
145    let mut header = p.header;
146    let is_be = (header[2] & 0x80) != 0;
147    header[2] &= !0x30;
148    let total = p.total as u32;
149    let len = if is_be { total.to_be_bytes() } else { total.to_le_bytes() };
150    header[4..8].copy_from_slice(&len);
151
152    let mut out = Vec::with_capacity(8 + p.total);
153    out.extend_from_slice(&header);
154    for payload in p.payloads {
155        out.extend_from_slice(&payload);
156    }
157    out
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163    use crate::error::DecodeError;
164
165    /// Build an 8-byte PVA header. `seg` is the raw 2-bit segment code:
166    /// 0 unsegmented, 1 first, 2 last, 3 middle.
167    fn hdr(command: u8, seg: u8, payload_len: u32, is_be: bool) -> [u8; 8] {
168        let mut flags = 0x40u8; // server direction
169        if is_be {
170            flags |= 0x80;
171        }
172        flags |= seg << 4;
173        let mut h = [0u8; 8];
174        h[0] = 0xCA;
175        h[1] = 2;
176        h[2] = flags;
177        h[3] = command;
178        let l = if is_be { payload_len.to_be_bytes() } else { payload_len.to_le_bytes() };
179        h[4..8].copy_from_slice(&l);
180        h
181    }
182
183    fn control_hdr() -> [u8; 8] {
184        let mut h = hdr(3, 0, 0, false);
185        h[2] |= 0x01;
186        h
187    }
188
189    #[test]
190    fn three_segments_reassemble_into_one_message() {
191        let mut r = SegmentReassembler::new();
192        assert_eq!(r.push(hdr(13, 1, 2, false), vec![1, 2]).unwrap(), SegmentOutcome::Pending);
193        assert_eq!(r.push(hdr(13, 3, 2, false), vec![3, 4]).unwrap(), SegmentOutcome::Pending);
194        let out = match r.push(hdr(13, 2, 2, false), vec![5, 6]).unwrap() {
195            SegmentOutcome::Complete(b) => b,
196            other => panic!("expected Complete, got {other:?}"),
197        };
198        assert_eq!(&out[8..], &[1, 2, 3, 4, 5, 6]);
199        assert_eq!(out[0], 0xCA, "magic byte preserved");
200        assert_eq!(out[1], 2, "version byte preserved");
201        assert_eq!(out[2] & 0x30, 0, "segment bits must be cleared");
202        assert_eq!(u32::from_le_bytes(out[4..8].try_into().unwrap()), 6);
203        assert_eq!(out[3], 13, "command byte preserved");
204        assert_eq!(r.pending_bytes(), 0);
205    }
206
207    #[test]
208    fn big_endian_length_is_rewritten_big_endian() {
209        let mut r = SegmentReassembler::new();
210        r.push(hdr(13, 1, 2, true), vec![1, 2]).unwrap();
211        let out = match r.push(hdr(13, 2, 1, true), vec![3]).unwrap() {
212            SegmentOutcome::Complete(b) => b,
213            other => panic!("expected Complete, got {other:?}"),
214        };
215        assert_eq!(u32::from_be_bytes(out[4..8].try_into().unwrap()), 3);
216    }
217
218    #[test]
219    fn unsegmented_message_passes_straight_through() {
220        let mut r = SegmentReassembler::new();
221        let out = match r.push(hdr(11, 0, 3, false), vec![7, 8, 9]).unwrap() {
222            SegmentOutcome::Complete(b) => b,
223            other => panic!("expected Complete, got {other:?}"),
224        };
225        assert_eq!(&out[8..], &[7, 8, 9]);
226    }
227
228    #[test]
229    fn control_frame_between_segments_does_not_disturb_reassembly() {
230        let mut r = SegmentReassembler::new();
231        r.push(hdr(13, 1, 2, false), vec![1, 2]).unwrap();
232        match r.push(control_hdr(), vec![]).unwrap() {
233            SegmentOutcome::Control(b) => assert_eq!(b.len(), 8),
234            other => panic!("expected Control, got {other:?}"),
235        }
236        assert_eq!(r.pending_bytes(), 2, "pending state survives the control frame");
237        let out = match r.push(hdr(13, 2, 2, false), vec![3, 4]).unwrap() {
238            SegmentOutcome::Complete(b) => b,
239            other => panic!("expected Complete, got {other:?}"),
240        };
241        assert_eq!(&out[8..], &[1, 2, 3, 4]);
242    }
243
244    #[test]
245    fn unsegmented_message_mid_reassembly_is_an_error() {
246        let mut r = SegmentReassembler::new();
247        r.push(hdr(13, 1, 2, false), vec![1, 2]).unwrap();
248        assert_eq!(
249            r.push(hdr(11, 0, 1, false), vec![9]).unwrap_err(),
250            DecodeError::SegmentInterrupted { expected: 13, got: 11 }
251        );
252        assert_eq!(r.pending_bytes(), 0, "state is reset after the error");
253    }
254
255    #[test]
256    fn orphan_middle_and_last_segments_are_errors() {
257        let mut r = SegmentReassembler::new();
258        assert!(matches!(
259            r.push(hdr(13, 3, 1, false), vec![1]).unwrap_err(),
260            DecodeError::UnexpectedSegment { .. }
261        ));
262        assert!(matches!(
263            r.push(hdr(13, 2, 1, false), vec![1]).unwrap_err(),
264            DecodeError::UnexpectedSegment { .. }
265        ));
266    }
267
268    #[test]
269    fn second_first_segment_while_pending_is_an_error() {
270        let mut r = SegmentReassembler::new();
271        r.push(hdr(13, 1, 1, false), vec![1]).unwrap();
272        assert!(matches!(
273            r.push(hdr(13, 1, 1, false), vec![2]).unwrap_err(),
274            DecodeError::UnexpectedSegment { .. }
275        ));
276        assert_eq!(r.pending_bytes(), 0);
277    }
278
279    #[test]
280    fn command_mismatch_across_segments_is_an_error() {
281        let mut r = SegmentReassembler::new();
282        r.push(hdr(13, 1, 1, false), vec![1]).unwrap();
283        assert_eq!(
284            r.push(hdr(11, 3, 1, false), vec![2]).unwrap_err(),
285            DecodeError::SegmentCommandMismatch { expected: 13, got: 11 }
286        );
287        assert_eq!(r.pending_bytes(), 0);
288    }
289
290    #[test]
291    fn exceeding_the_cap_errors_and_leaves_the_reassembler_reusable() {
292        let mut r = SegmentReassembler::with_max_bytes(4);
293        r.push(hdr(13, 1, 3, false), vec![1, 2, 3]).unwrap();
294        assert_eq!(
295            r.push(hdr(13, 3, 3, false), vec![4, 5, 6]).unwrap_err(),
296            DecodeError::MessageTooLarge { total: 6, limit: 4 }
297        );
298        assert_eq!(r.pending_bytes(), 0);
299        // Still usable for the next message.
300        let out = match r.push(hdr(11, 0, 1, false), vec![9]).unwrap() {
301            SegmentOutcome::Complete(b) => b,
302            other => panic!("expected Complete, got {other:?}"),
303        };
304        assert_eq!(&out[8..], &[9]);
305    }
306
307    #[test]
308    fn reset_discards_pending_state() {
309        let mut r = SegmentReassembler::new();
310        r.push(hdr(13, 1, 2, false), vec![1, 2]).unwrap();
311        assert_eq!(r.pending_bytes(), 2);
312        r.reset();
313        assert_eq!(r.pending_bytes(), 0);
314    }
315}