umsh_node_mgmt/
envelope.rs

1//! The Node Management payload envelope.
2//!
3//! ```text
4//! +-------+---------+------+----------+
5//! | TOKEN | OPTIONS | 0xFF |  FRAME   |
6//! +-------+---------+------+----------+
7//!   2 B    variable   1 B    variable
8//! ```
9//!
10//! Request and Response payloads share this format; direction lives
11//! entirely in the payload type. The frame extends to the end of the
12//! payload, so the end marker is always present.
13
14use umsh_core::options::{OptionDecoder, OptionEncoder};
15use umsh_core::{EncodeError, ParseError};
16use umsh_ulcp::pui;
17
18/// Option 1 — the continuation handle for a read spanning several
19/// exchanges. Critical: a device that cannot honor a cursor must say so
20/// rather than answer from the beginning.
21pub const OPT_CURSOR: u16 = 1;
22/// Option 2 — approximate octets not yet returned. Elective: it drives a
23/// progress bar and nothing else.
24pub const OPT_REMAINING: u16 = 2;
25
26/// Longest cursor the format permits.
27pub const CURSOR_MAX: usize = 8;
28
29/// The two-octet token, opaque to everyone but the administrator that
30/// chose it.
31pub type Token = [u8; 2];
32
33/// A parsed envelope borrowing from the payload it was read out of.
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub struct Envelope<'a> {
36    pub token: Token,
37    /// The cursor to present in the *next* request, in a response; the
38    /// position being continued, in a request.
39    pub cursor: Option<&'a [u8]>,
40    /// Advisory count of octets not yet returned.
41    pub remaining: Option<u32>,
42    /// Exactly one ULCP frame, unparsed.
43    pub frame: &'a [u8],
44}
45
46/// Why an envelope could not be read.
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48pub enum EnvelopeError {
49    /// The payload ended before the envelope was complete, or the option
50    /// block is malformed.
51    Malformed(ParseError),
52    /// An option this implementation does not recognize, in the critical
53    /// (odd-numbered) range. A device answers `STATUS_UNIMPLEMENTED`; an
54    /// administrator treats the exchange as failed.
55    UnknownCritical(u16),
56    /// A recognized option carried a value it cannot hold: a cursor
57    /// outside 1–8 octets, or a REMAINING that is not a packed unsigned
58    /// integer.
59    InvalidOptionValue(u16),
60}
61
62impl From<ParseError> for EnvelopeError {
63    fn from(error: ParseError) -> Self {
64        Self::Malformed(error)
65    }
66}
67
68impl<'a> Envelope<'a> {
69    /// A bare envelope carrying `frame` and nothing else.
70    pub const fn new(token: Token, frame: &'a [u8]) -> Self {
71        Self {
72            token,
73            cursor: None,
74            remaining: None,
75            frame,
76        }
77    }
78
79    pub const fn with_cursor(mut self, cursor: &'a [u8]) -> Self {
80        self.cursor = Some(cursor);
81        self
82    }
83
84    pub const fn with_remaining(mut self, remaining: u32) -> Self {
85        self.remaining = Some(remaining);
86        self
87    }
88
89    /// Read an envelope out of a payload, the payload type byte already
90    /// stripped.
91    pub fn parse(payload: &'a [u8]) -> Result<Self, EnvelopeError> {
92        let [token0, token1, rest @ ..] = payload else {
93            return Err(EnvelopeError::Malformed(ParseError::Truncated));
94        };
95
96        let mut cursor = None;
97        let mut remaining = None;
98
99        let mut decoder = OptionDecoder::new(rest);
100        for result in decoder.by_ref() {
101            let (number, value) = result?;
102            match number {
103                OPT_CURSOR => {
104                    if value.is_empty() || value.len() > CURSOR_MAX {
105                        return Err(EnvelopeError::InvalidOptionValue(number));
106                    }
107                    cursor = Some(value);
108                }
109                OPT_REMAINING => {
110                    let (parsed, consumed) = pui::decode(value)
111                        .map_err(|_| EnvelopeError::InvalidOptionValue(number))?;
112                    if consumed != value.len() {
113                        return Err(EnvelopeError::InvalidOptionValue(number));
114                    }
115                    remaining = Some(parsed);
116                }
117                // Odd is critical, even is elective, as in MAC command
118                // options. Skipping the elective ones is the whole point
119                // of the distinction.
120                _ if number % 2 == 1 => return Err(EnvelopeError::UnknownCritical(number)),
121                _ => {}
122            }
123        }
124
125        // The frame follows the end marker, so a payload that never
126        // reached one has no frame at all — an empty frame, which the
127        // caller answers `STATUS_PARSE_ERROR`.
128        Ok(Self {
129            token: [*token0, *token1],
130            cursor,
131            remaining,
132            frame: decoder.remainder(),
133        })
134    }
135
136    /// Write the envelope, returning its length.
137    pub fn encode(&self, buf: &mut [u8]) -> Result<usize, EncodeError> {
138        if buf.len() < 2 {
139            return Err(EncodeError::BufferTooSmall);
140        }
141        buf[0] = self.token[0];
142        buf[1] = self.token[1];
143        let mut pos = 2;
144
145        {
146            let mut enc = OptionEncoder::new(&mut buf[pos..]);
147            if let Some(cursor) = self.cursor {
148                if cursor.is_empty() || cursor.len() > CURSOR_MAX {
149                    return Err(EncodeError::OptionValueTooLarge);
150                }
151                enc.put(OPT_CURSOR, cursor)?;
152            }
153            if let Some(remaining) = self.remaining {
154                let mut value = [0u8; pui::MAX_LEN];
155                let len = pui::encode(remaining, &mut value)
156                    .map_err(|_| EncodeError::OptionValueTooLarge)?;
157                enc.put(OPT_REMAINING, &value[..len])?;
158            }
159            // Always present: the frame follows.
160            enc.end_marker()?;
161            pos += enc.finish();
162        }
163
164        if pos + self.frame.len() > buf.len() {
165            return Err(EncodeError::BufferTooSmall);
166        }
167        buf[pos..pos + self.frame.len()].copy_from_slice(self.frame);
168        Ok(pos + self.frame.len())
169    }
170
171    /// Octets this envelope costs a payload beyond its frame.
172    pub fn overhead(&self) -> usize {
173        let mut scratch = [0u8; OVERHEAD_MAX];
174        let bare = Self {
175            frame: &[],
176            ..*self
177        };
178        // Nothing here can overflow `scratch`: the cursor is bounded at
179        // encode time and REMAINING is a packed unsigned integer.
180        bare.encode(&mut scratch).unwrap_or(scratch.len())
181    }
182}
183
184/// Worst-case envelope overhead: token, the longest cursor, the longest
185/// REMAINING, and the end marker.
186///
187/// Sizing a payload budget against this rather than against a particular
188/// envelope keeps a fragment that acquires a cursor from overflowing the
189/// frame it was measured for.
190pub const OVERHEAD_MAX: usize = 2      // token
191    + 1 + CURSOR_MAX                   // CURSOR header (delta 1, len 8) + value
192    + 1 + pui::MAX_LEN                 // REMAINING header + value
193    + 1; // end marker
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    const FRAME: &[u8] = &[0x80, 0x02, 0x01];
200
201    fn round_trip(envelope: &Envelope<'_>) -> ([u8; 64], usize) {
202        let mut buf = [0u8; 64];
203        let len = envelope.encode(&mut buf).expect("encode");
204        let parsed = Envelope::parse(&buf[..len]).expect("parse");
205        assert_eq!(&parsed, envelope);
206        (buf, len)
207    }
208
209    #[test]
210    fn a_bare_envelope_is_token_marker_frame() {
211        let (buf, len) = round_trip(&Envelope::new([0xAB, 0xCD], FRAME));
212        assert_eq!(&buf[..len], &[0xAB, 0xCD, 0xFF, 0x80, 0x02, 0x01]);
213    }
214
215    #[test]
216    fn options_round_trip() {
217        round_trip(&Envelope::new([1, 2], FRAME).with_cursor(&[9, 8, 7]));
218        round_trip(&Envelope::new([1, 2], FRAME).with_remaining(0));
219        round_trip(&Envelope::new([1, 2], FRAME).with_remaining(pui::MAX_VALUE));
220        round_trip(
221            &Envelope::new([1, 2], FRAME)
222                .with_cursor(&[0; CURSOR_MAX])
223                .with_remaining(300),
224        );
225    }
226
227    #[test]
228    fn an_empty_frame_parses_as_an_empty_frame() {
229        // The device answers this STATUS_PARSE_ERROR rather than
230        // treating the payload itself as malformed.
231        let envelope = Envelope::parse(&[1, 2, 0xFF]).expect("parse");
232        assert!(envelope.frame.is_empty());
233    }
234
235    #[test]
236    fn a_payload_shorter_than_a_token_is_malformed() {
237        assert_eq!(
238            Envelope::parse(&[1]),
239            Err(EnvelopeError::Malformed(ParseError::Truncated))
240        );
241    }
242
243    #[test]
244    fn an_unknown_critical_option_is_reported_and_an_elective_one_is_not() {
245        let mut buf = [0u8; 32];
246        let encoded = {
247            let mut enc = OptionEncoder::new(&mut buf[2..]);
248            enc.put(3, &[0]).unwrap();
249            enc.end_marker().unwrap();
250            2 + enc.finish()
251        };
252        assert_eq!(
253            Envelope::parse(&buf[..encoded]),
254            Err(EnvelopeError::UnknownCritical(3))
255        );
256
257        let mut buf = [0u8; 32];
258        let encoded = {
259            let mut enc = OptionEncoder::new(&mut buf[2..]);
260            enc.put(4, &[0]).unwrap();
261            enc.end_marker().unwrap();
262            2 + enc.finish()
263        };
264        let envelope = Envelope::parse(&buf[..encoded]).expect("elective options are skipped");
265        assert_eq!(envelope.cursor, None);
266    }
267
268    #[test]
269    fn a_cursor_outside_one_to_eight_octets_is_rejected() {
270        for width in [0usize, CURSOR_MAX + 1] {
271            let mut buf = [0u8; 32];
272            let encoded = {
273                let mut enc = OptionEncoder::new(&mut buf[2..]);
274                enc.put(OPT_CURSOR, &[0u8; 16][..width]).unwrap();
275                enc.end_marker().unwrap();
276                2 + enc.finish()
277            };
278            assert_eq!(
279                Envelope::parse(&buf[..encoded]),
280                Err(EnvelopeError::InvalidOptionValue(OPT_CURSOR))
281            );
282        }
283    }
284
285    #[test]
286    fn a_remaining_that_is_not_one_whole_pui_is_rejected() {
287        for value in [&[][..], &[0xFF, 0xFF, 0xFF, 0x7F][..], &[0x00, 0x00][..]] {
288            let mut buf = [0u8; 32];
289            let encoded = {
290                let mut enc = OptionEncoder::new(&mut buf[2..]);
291                enc.put(OPT_REMAINING, value).unwrap();
292                enc.end_marker().unwrap();
293                2 + enc.finish()
294            };
295            assert_eq!(
296                Envelope::parse(&buf[..encoded]),
297                Err(EnvelopeError::InvalidOptionValue(OPT_REMAINING))
298            );
299        }
300    }
301
302    #[test]
303    fn a_buffer_one_octet_short_fails_rather_than_truncating() {
304        let envelope = Envelope::new([1, 2], FRAME);
305        let mut buf = [0u8; 64];
306        let len = envelope.encode(&mut buf).unwrap();
307        assert_eq!(
308            envelope.encode(&mut buf[..len - 1]),
309            Err(EncodeError::BufferTooSmall)
310        );
311    }
312
313    #[test]
314    fn overhead_bounds_every_envelope_this_crate_can_write() {
315        let worst = Envelope::new([0, 0], &[])
316            .with_cursor(&[0; CURSOR_MAX])
317            .with_remaining(pui::MAX_VALUE);
318        assert_eq!(worst.overhead(), OVERHEAD_MAX);
319        assert!(Envelope::new([0, 0], &[]).overhead() <= OVERHEAD_MAX);
320    }
321}