Skip to main content

osdp/
multipart.rs

1//! Multi-part message engine.
2//!
3//! # Spec: ยง5.10, Table 4
4//!
5//! When a command or reply payload is larger than a single packet supports,
6//! the data block is prefixed with a 6-byte multi-part header:
7//!
8//! ```text
9//! +---------------+--------------+--------------------+
10//! | MpSizeTotal   | MpOffset     | MpFragmentSize     |
11//! | (u16 LE)      | (u16 LE)     | (u16 LE)           |
12//! +---------------+--------------+--------------------+
13//! ```
14//!
15//! Rules enforced by this module:
16//! - First fragment is always at offset 0.
17//! - Fragments are strictly sequential (no gaps, no reorder).
18//! - All fragments report the same `MpSizeTotal`.
19//! - Sender may abort by setting `MpOffset >= MpSizeTotal` and
20//!   `MpFragmentSize = 0`.
21
22use crate::error::{Error, MultipartError};
23
24/// Multi-part header on the wire.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct MultipartHeader {
27    /// Total combined size of all fragments.
28    pub total: u16,
29    /// Byte offset of this fragment's data within the combined transfer.
30    pub offset: u16,
31    /// Length of this fragment's data.
32    pub fragment: u16,
33}
34
35impl MultipartHeader {
36    /// Encoded size on the wire.
37    pub const WIRE_LEN: usize = 6;
38
39    /// Encode into a 6-byte array.
40    pub const fn encode(self) -> [u8; Self::WIRE_LEN] {
41        let t = self.total.to_le_bytes();
42        let o = self.offset.to_le_bytes();
43        let f = self.fragment.to_le_bytes();
44        [t[0], t[1], o[0], o[1], f[0], f[1]]
45    }
46
47    /// Decode from a 6-byte array.
48    pub fn decode(bytes: &[u8]) -> Result<Self, Error> {
49        if bytes.len() < Self::WIRE_LEN {
50            return Err(Error::Truncated {
51                have: bytes.len(),
52                need: Self::WIRE_LEN,
53            });
54        }
55        Ok(Self {
56            total: u16::from_le_bytes([bytes[0], bytes[1]]),
57            offset: u16::from_le_bytes([bytes[2], bytes[3]]),
58            fragment: u16::from_le_bytes([bytes[4], bytes[5]]),
59        })
60    }
61
62    /// `true` if the header signals an abort.
63    pub const fn is_abort(self) -> bool {
64        self.fragment == 0 && self.offset >= self.total
65    }
66}
67
68#[cfg(feature = "alloc")]
69mod alloc_impls {
70    use super::*;
71    use alloc::vec::Vec;
72
73    /// Iterator-style splitter for outgoing multi-part transfers.
74    #[derive(Debug, Clone)]
75    pub struct MultipartTx<'a> {
76        body: &'a [u8],
77        offset: u16,
78        fragment_size: u16,
79    }
80
81    impl<'a> MultipartTx<'a> {
82        /// New splitter. `fragment_size` is the maximum payload size that
83        /// fits into a single packet (excluding the 6-byte multi-part
84        /// header).
85        pub fn new(body: &'a [u8], fragment_size: u16) -> Result<Self, Error> {
86            if fragment_size == 0 {
87                return Err(Error::Multipart(MultipartError::BadFragmentSize));
88            }
89            if body.len() > u16::MAX as usize {
90                return Err(Error::BufferOverflow {
91                    need: body.len(),
92                    have: u16::MAX as usize,
93                });
94            }
95            Ok(Self {
96                body,
97                offset: 0,
98                fragment_size,
99            })
100        }
101
102        /// Total transfer size.
103        pub fn total(&self) -> u16 {
104            self.body.len() as u16
105        }
106
107        /// Bytes still queued.
108        pub fn remaining(&self) -> u16 {
109            self.total().saturating_sub(self.offset)
110        }
111    }
112
113    impl<'a> Iterator for MultipartTx<'a> {
114        type Item = (MultipartHeader, &'a [u8]);
115
116        fn next(&mut self) -> Option<Self::Item> {
117            if self.offset as usize >= self.body.len() {
118                return None;
119            }
120            let take =
121                (self.body.len() - self.offset as usize).min(self.fragment_size as usize) as u16;
122            let frag = &self.body[self.offset as usize..self.offset as usize + take as usize];
123            let header = MultipartHeader {
124                total: self.body.len() as u16,
125                offset: self.offset,
126                fragment: take,
127            };
128            self.offset += take;
129            Some((header, frag))
130        }
131    }
132
133    /// Reassembler for incoming multi-part transfers.
134    #[derive(Debug, Clone, Default)]
135    pub struct MultipartRx {
136        buf: Vec<u8>,
137        total: Option<u16>,
138    }
139
140    impl MultipartRx {
141        /// New empty reassembler.
142        pub fn new() -> Self {
143            Self::default()
144        }
145
146        /// Reset state, dropping any partial transfer.
147        pub fn reset(&mut self) {
148            self.buf.clear();
149            self.total = None;
150        }
151
152        /// Feed one fragment. Returns `Ok(Some(_))` once the transfer is
153        /// complete (and resets internal state), `Ok(None)` if more
154        /// fragments are required.
155        pub fn push(
156            &mut self,
157            header: MultipartHeader,
158            data: &[u8],
159        ) -> Result<Option<Vec<u8>>, Error> {
160            if header.is_abort() {
161                self.reset();
162                return Err(Error::Multipart(MultipartError::Aborted));
163            }
164
165            if header.fragment as usize != data.len() {
166                return Err(Error::Multipart(MultipartError::BadFragmentSize));
167            }
168
169            match self.total {
170                None => {
171                    if header.offset != 0 {
172                        return Err(Error::Multipart(MultipartError::UnexpectedFirstOffset(
173                            header.offset,
174                        )));
175                    }
176                    self.total = Some(header.total);
177                    self.buf.clear();
178                    self.buf.reserve(header.total as usize);
179                }
180                Some(t) if t != header.total => {
181                    return Err(Error::Multipart(MultipartError::InconsistentTotal {
182                        first: t,
183                        now: header.total,
184                    }));
185                }
186                _ => {
187                    if header.offset as usize != self.buf.len() {
188                        return Err(Error::Multipart(MultipartError::OutOfOrderOffset {
189                            expected: self.buf.len() as u16,
190                            got: header.offset,
191                        }));
192                    }
193                }
194            }
195
196            if (header.offset as usize) + data.len() > header.total as usize {
197                return Err(Error::Multipart(MultipartError::OverflowsTotal));
198            }
199
200            self.buf.extend_from_slice(data);
201
202            if self.buf.len() == header.total as usize {
203                let total = header.total;
204                let mut out = core::mem::take(&mut self.buf);
205                self.total = None;
206                out.truncate(total as usize);
207                Ok(Some(out))
208            } else {
209                Ok(None)
210            }
211        }
212
213        /// Bytes received so far (only meaningful while a transfer is in progress).
214        pub fn progress(&self) -> usize {
215            self.buf.len()
216        }
217    }
218
219    #[cfg(test)]
220    mod tests {
221        use super::*;
222
223        #[test]
224        fn split_then_assemble() {
225            let body: Vec<u8> = (0u16..1024).flat_map(|n| n.to_le_bytes()).collect();
226            let tx = MultipartTx::new(&body, 200).unwrap();
227            let mut rx = MultipartRx::new();
228            let mut out = None;
229            for (h, frag) in tx {
230                if let Some(v) = rx.push(h, frag).unwrap() {
231                    out = Some(v);
232                }
233            }
234            assert_eq!(out.unwrap(), body);
235        }
236
237        #[test]
238        fn reject_out_of_order() {
239            let mut rx = MultipartRx::new();
240            let h0 = MultipartHeader {
241                total: 8,
242                offset: 0,
243                fragment: 4,
244            };
245            rx.push(h0, &[0u8; 4]).unwrap();
246            let bogus = MultipartHeader {
247                total: 8,
248                offset: 6,
249                fragment: 2,
250            };
251            assert!(rx.push(bogus, &[0u8; 2]).is_err());
252        }
253
254        #[test]
255        fn reject_inconsistent_total() {
256            let mut rx = MultipartRx::new();
257            let h0 = MultipartHeader {
258                total: 8,
259                offset: 0,
260                fragment: 4,
261            };
262            rx.push(h0, &[0u8; 4]).unwrap();
263            let bogus = MultipartHeader {
264                total: 12,
265                offset: 4,
266                fragment: 4,
267            };
268            assert!(rx.push(bogus, &[0u8; 4]).is_err());
269        }
270
271        #[test]
272        fn abort_signal() {
273            let mut rx = MultipartRx::new();
274            let h0 = MultipartHeader {
275                total: 8,
276                offset: 0,
277                fragment: 4,
278            };
279            rx.push(h0, &[0u8; 4]).unwrap();
280            let abort = MultipartHeader {
281                total: 8,
282                offset: 8,
283                fragment: 0,
284            };
285            assert!(matches!(
286                rx.push(abort, &[]),
287                Err(Error::Multipart(MultipartError::Aborted))
288            ));
289        }
290
291        #[test]
292        fn header_roundtrip() {
293            let h = MultipartHeader {
294                total: 0x1234,
295                offset: 0x0050,
296                fragment: 0x0010,
297            };
298            let bytes = h.encode();
299            assert_eq!(MultipartHeader::decode(&bytes).unwrap(), h);
300        }
301    }
302}
303
304#[cfg(feature = "alloc")]
305pub use alloc_impls::{MultipartRx, MultipartTx};