umsh_ulcp/
frame.rs

1//! ULCP frame grammar.
2//!
3//! A frame is a one-byte header, a one-byte command identifier, and a
4//! command-defined payload. Frame length is provided by the framing
5//! layer (see [`crate::hdlc`] for asynchronous serial links).
6
7use crate::pui;
8use crate::status::Status;
9
10/// Mask of the two most significant header bits (the `FLG` field).
11pub const HEADER_FLG_MASK: u8 = 0xC0;
12/// Required value of the `FLG` field (`0b10` in the top two bits).
13pub const HEADER_FLG_PATTERN: u8 = 0x80;
14/// Mask of the three reserved header bits, which must be zero.
15pub const HEADER_RESERVED_MASK: u8 = 0x38;
16/// Mask of the three-bit transaction identifier.
17pub const HEADER_TID_MASK: u8 = 0x07;
18
19/// TID reserved for unsolicited commands and stream traffic.
20pub const TID_UNSOLICITED: u8 = 0;
21/// Largest usable transaction identifier.
22pub const TID_MAX: u8 = 7;
23
24/// Validated frame header byte.
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub struct Header(u8);
27
28impl Header {
29    /// Build a header for the given transaction identifier.
30    ///
31    /// Returns `None` if `tid` exceeds [`TID_MAX`].
32    pub const fn new(tid: u8) -> Option<Self> {
33        if tid <= TID_MAX {
34            Some(Self(HEADER_FLG_PATTERN | tid))
35        } else {
36            None
37        }
38    }
39
40    /// Validate a received header byte.
41    ///
42    /// Returns `None` when the `FLG` pattern does not match (the frame
43    /// is not a ULCP frame) or a reserved bit is set (the
44    /// frame must be ignored).
45    pub const fn from_byte(byte: u8) -> Option<Self> {
46        if byte & HEADER_FLG_MASK == HEADER_FLG_PATTERN && byte & HEADER_RESERVED_MASK == 0 {
47            Some(Self(byte))
48        } else {
49            None
50        }
51    }
52
53    pub const fn to_byte(self) -> u8 {
54        self.0
55    }
56
57    pub const fn tid(self) -> u8 {
58        self.0 & HEADER_TID_MASK
59    }
60}
61
62/// Command identifiers defined by the minimal and full specs.
63#[derive(Clone, Copy, Debug, PartialEq, Eq)]
64#[repr(u8)]
65pub enum Cmd {
66    /// No-operation liveness check (host to device).
67    Nop = 0,
68    /// Software reset request (host to device).
69    Reset = 1,
70    /// Get property value (host to device).
71    PropGet = 2,
72    /// Set property value (host to device).
73    PropSet = 3,
74    /// Insert an item into a multi-value property (host to device).
75    PropInsert = 4,
76    /// Remove an item from a multi-value property (host to device).
77    PropRemove = 5,
78    /// Property value notification (device to host).
79    PropIs = 6,
80    /// Item-inserted notification (device to host).
81    PropInserted = 7,
82    /// Item-removed notification (device to host).
83    PropRemoved = 8,
84    /// Send data to a stream (host to device).
85    StrSend = 9,
86    /// Data received from a stream (device to host).
87    StrRecv = 10,
88    /// Deliver queued inbound frames (host to device).
89    QueueDrain = 11,
90    /// Save state to non-volatile storage (host to device).
91    Save = 12,
92    /// Erase all saved state (host to device).
93    Clear = 13,
94    /// Restore state from the saved snapshot (host to device).
95    Restore = 14,
96    /// Factory reset (host to device): erase ALL mutable state — saved
97    /// provisioning, device identity, BLE bonds, pairing PIN, and every
98    /// other persisted journal — then reboot. Unlike `CMD_CLEAR` (which
99    /// exempts bonds and the PIN and leaves the live session running),
100    /// this returns the radio to a blank factory state and does not reply:
101    /// the reboot drops the link.
102    FactoryReset = 15,
103}
104
105impl Cmd {
106    pub const fn from_u8(value: u8) -> Option<Self> {
107        match value {
108            0 => Some(Self::Nop),
109            1 => Some(Self::Reset),
110            2 => Some(Self::PropGet),
111            3 => Some(Self::PropSet),
112            4 => Some(Self::PropInsert),
113            5 => Some(Self::PropRemove),
114            6 => Some(Self::PropIs),
115            7 => Some(Self::PropInserted),
116            8 => Some(Self::PropRemoved),
117            9 => Some(Self::StrSend),
118            10 => Some(Self::StrRecv),
119            11 => Some(Self::QueueDrain),
120            12 => Some(Self::Save),
121            13 => Some(Self::Clear),
122            14 => Some(Self::Restore),
123            15 => Some(Self::FactoryReset),
124            _ => None,
125        }
126    }
127}
128
129#[derive(Clone, Copy, Debug, PartialEq, Eq)]
130pub enum ParseError {
131    /// The input ended before the structure was complete.
132    Truncated,
133    /// The header `FLG` pattern did not match; not a ULCP frame.
134    BadFlag,
135    /// A reserved header bit was set; the frame must be ignored.
136    ReservedBits,
137    /// The command identifier had its most significant bit set; the
138    /// frame must be ignored.
139    BadCommand,
140    /// A packed unsigned integer inside the payload was malformed.
141    BadPui,
142}
143
144impl From<pui::Error> for ParseError {
145    fn from(error: pui::Error) -> Self {
146        match error {
147            pui::Error::Truncated => Self::Truncated,
148            _ => Self::BadPui,
149        }
150    }
151}
152
153/// A parsed frame borrowing its payload from the input.
154///
155/// `cmd` is kept as the raw identifier so receivers can distinguish an
156/// unknown-but-well-formed command (respond with
157/// `STATUS_INVALID_COMMAND`) from a malformed frame (ignore).
158#[derive(Clone, Copy, Debug, PartialEq, Eq)]
159pub struct Frame<'a> {
160    pub header: Header,
161    pub cmd: u8,
162    pub payload: &'a [u8],
163}
164
165impl<'a> Frame<'a> {
166    pub fn parse(bytes: &'a [u8]) -> Result<Self, ParseError> {
167        let [header_byte, cmd, payload @ ..] = bytes else {
168            return Err(ParseError::Truncated);
169        };
170        if header_byte & HEADER_FLG_MASK != HEADER_FLG_PATTERN {
171            return Err(ParseError::BadFlag);
172        }
173        let header = Header::from_byte(*header_byte).ok_or(ParseError::ReservedBits)?;
174        if cmd & 0x80 != 0 {
175            return Err(ParseError::BadCommand);
176        }
177        Ok(Self {
178            header,
179            cmd: *cmd,
180            payload,
181        })
182    }
183
184    /// The command, if it is one defined by this crate.
185    pub const fn command(&self) -> Option<Cmd> {
186        Cmd::from_u8(self.cmd)
187    }
188}
189
190#[derive(Clone, Copy, Debug, PartialEq, Eq)]
191pub enum WriteError {
192    /// The output buffer cannot hold the frame.
193    BufferTooSmall,
194    /// The transaction identifier exceeds [`TID_MAX`].
195    InvalidTid,
196    /// A value exceeds the range of its wire representation.
197    ValueTooLarge,
198}
199
200impl From<pui::Error> for WriteError {
201    fn from(error: pui::Error) -> Self {
202        match error {
203            pui::Error::BufferTooSmall => Self::BufferTooSmall,
204            _ => Self::ValueTooLarge,
205        }
206    }
207}
208
209/// Incremental frame builder over a caller-provided buffer.
210pub struct FrameWriter<'a> {
211    buf: &'a mut [u8],
212    len: usize,
213}
214
215impl<'a> FrameWriter<'a> {
216    /// Start a frame with the given TID and command.
217    pub fn new(buf: &'a mut [u8], tid: u8, cmd: Cmd) -> Result<Self, WriteError> {
218        let header = Header::new(tid).ok_or(WriteError::InvalidTid)?;
219        let mut writer = Self { buf, len: 0 };
220        writer.write_u8(header.to_byte())?;
221        writer.write_u8(cmd as u8)?;
222        Ok(writer)
223    }
224
225    pub fn write_u8(&mut self, byte: u8) -> Result<(), WriteError> {
226        if self.len >= self.buf.len() {
227            return Err(WriteError::BufferTooSmall);
228        }
229        self.buf[self.len] = byte;
230        self.len += 1;
231        Ok(())
232    }
233
234    pub fn write_bytes(&mut self, bytes: &[u8]) -> Result<(), WriteError> {
235        let end = self
236            .len
237            .checked_add(bytes.len())
238            .ok_or(WriteError::BufferTooSmall)?;
239        if end > self.buf.len() {
240            return Err(WriteError::BufferTooSmall);
241        }
242        self.buf[self.len..end].copy_from_slice(bytes);
243        self.len = end;
244        Ok(())
245    }
246
247    pub fn write_pui(&mut self, value: u32) -> Result<(), WriteError> {
248        let written = pui::encode(value, &mut self.buf[self.len..])?;
249        self.len += written;
250        Ok(())
251    }
252
253    pub fn write_u16_le(&mut self, value: u16) -> Result<(), WriteError> {
254        self.write_bytes(&value.to_le_bytes())
255    }
256
257    pub fn write_u32_le(&mut self, value: u32) -> Result<(), WriteError> {
258        self.write_bytes(&value.to_le_bytes())
259    }
260
261    /// Finish the frame, returning its total length in the buffer.
262    pub fn finish(self) -> usize {
263        self.len
264    }
265}
266
267/// Encode a `CMD_NOP` frame.
268pub fn nop(buf: &mut [u8], tid: u8) -> Result<usize, WriteError> {
269    Ok(FrameWriter::new(buf, tid, Cmd::Nop)?.finish())
270}
271
272/// Encode a `CMD_RST` frame.
273pub fn reset(buf: &mut [u8], tid: u8) -> Result<usize, WriteError> {
274    Ok(FrameWriter::new(buf, tid, Cmd::Reset)?.finish())
275}
276
277/// Encode a `CMD_PROP_GET` frame.
278pub fn prop_get(buf: &mut [u8], tid: u8, key: u32) -> Result<usize, WriteError> {
279    let mut writer = FrameWriter::new(buf, tid, Cmd::PropGet)?;
280    writer.write_pui(key)?;
281    Ok(writer.finish())
282}
283
284/// Encode a `CMD_PROP_SET` frame.
285pub fn prop_set(buf: &mut [u8], tid: u8, key: u32, value: &[u8]) -> Result<usize, WriteError> {
286    let mut writer = FrameWriter::new(buf, tid, Cmd::PropSet)?;
287    writer.write_pui(key)?;
288    writer.write_bytes(value)?;
289    Ok(writer.finish())
290}
291
292/// Encode a `CMD_PROP_IS` frame.
293pub fn prop_is(buf: &mut [u8], tid: u8, key: u32, value: &[u8]) -> Result<usize, WriteError> {
294    let mut writer = FrameWriter::new(buf, tid, Cmd::PropIs)?;
295    writer.write_pui(key)?;
296    writer.write_bytes(value)?;
297    Ok(writer.finish())
298}
299
300/// Encode a `CMD_PROP_INSERT` frame. `item` is one item in the
301/// property's item form, with no length prefix.
302pub fn prop_insert(buf: &mut [u8], tid: u8, key: u32, item: &[u8]) -> Result<usize, WriteError> {
303    let mut writer = FrameWriter::new(buf, tid, Cmd::PropInsert)?;
304    writer.write_pui(key)?;
305    writer.write_bytes(item)?;
306    Ok(writer.finish())
307}
308
309/// Encode a `CMD_PROP_REMOVE` frame. `selector` is the property's
310/// documented item selector, with no length prefix.
311pub fn prop_remove(
312    buf: &mut [u8],
313    tid: u8,
314    key: u32,
315    selector: &[u8],
316) -> Result<usize, WriteError> {
317    let mut writer = FrameWriter::new(buf, tid, Cmd::PropRemove)?;
318    writer.write_pui(key)?;
319    writer.write_bytes(selector)?;
320    Ok(writer.finish())
321}
322
323/// Encode a `CMD_PROP_INSERTED` frame. `digest` is the inserted item in
324/// the property's digest form — never in a form containing key material.
325pub fn prop_inserted(
326    buf: &mut [u8],
327    tid: u8,
328    key: u32,
329    digest: &[u8],
330) -> Result<usize, WriteError> {
331    let mut writer = FrameWriter::new(buf, tid, Cmd::PropInserted)?;
332    writer.write_pui(key)?;
333    writer.write_bytes(digest)?;
334    Ok(writer.finish())
335}
336
337/// Encode a `CMD_PROP_REMOVED` frame. `digest` is the removed item in
338/// the property's digest form.
339pub fn prop_removed(buf: &mut [u8], tid: u8, key: u32, digest: &[u8]) -> Result<usize, WriteError> {
340    let mut writer = FrameWriter::new(buf, tid, Cmd::PropRemoved)?;
341    writer.write_pui(key)?;
342    writer.write_bytes(digest)?;
343    Ok(writer.finish())
344}
345
346/// Encode a `CMD_QUEUE_DRAIN` frame (no payload).
347pub fn queue_drain(buf: &mut [u8], tid: u8) -> Result<usize, WriteError> {
348    Ok(FrameWriter::new(buf, tid, Cmd::QueueDrain)?.finish())
349}
350
351/// Encode a `CMD_SAVE` frame (no payload).
352pub fn save(buf: &mut [u8], tid: u8) -> Result<usize, WriteError> {
353    Ok(FrameWriter::new(buf, tid, Cmd::Save)?.finish())
354}
355
356/// Encode a `CMD_CLEAR` frame (no payload).
357pub fn clear(buf: &mut [u8], tid: u8) -> Result<usize, WriteError> {
358    Ok(FrameWriter::new(buf, tid, Cmd::Clear)?.finish())
359}
360
361/// Encode a `CMD_FACTORY_RESET` frame (no payload). The device erases all
362/// mutable state and reboots without replying.
363pub fn factory_reset(buf: &mut [u8], tid: u8) -> Result<usize, WriteError> {
364    Ok(FrameWriter::new(buf, tid, Cmd::FactoryReset)?.finish())
365}
366
367/// Encode a `CMD_RESTORE` frame (no payload).
368pub fn restore(buf: &mut [u8], tid: u8) -> Result<usize, WriteError> {
369    Ok(FrameWriter::new(buf, tid, Cmd::Restore)?.finish())
370}
371
372/// Encode a `CMD_PROP_IS` frame carrying `PROP_LAST_STATUS`.
373pub fn last_status(buf: &mut [u8], tid: u8, status: Status) -> Result<usize, WriteError> {
374    let mut writer = FrameWriter::new(buf, tid, Cmd::PropIs)?;
375    writer.write_pui(crate::ids::prop::LAST_STATUS)?;
376    writer.write_pui(status.0)?;
377    Ok(writer.finish())
378}
379
380fn stream_payload(
381    writer: &mut FrameWriter<'_>,
382    stream: u32,
383    data: &[u8],
384    metadata: &[u8],
385) -> Result<(), WriteError> {
386    let data_len = u16::try_from(data.len()).map_err(|_| WriteError::ValueTooLarge)?;
387    writer.write_pui(stream)?;
388    writer.write_u16_le(data_len)?;
389    writer.write_bytes(data)?;
390    writer.write_bytes(metadata)
391}
392
393/// Encode a `CMD_STR_SEND` frame.
394pub fn str_send(
395    buf: &mut [u8],
396    tid: u8,
397    stream: u32,
398    data: &[u8],
399    metadata: &[u8],
400) -> Result<usize, WriteError> {
401    let mut writer = FrameWriter::new(buf, tid, Cmd::StrSend)?;
402    stream_payload(&mut writer, stream, data, metadata)?;
403    Ok(writer.finish())
404}
405
406/// Encode a `CMD_STR_RECV` frame. Always uses TID zero.
407pub fn str_recv(
408    buf: &mut [u8],
409    stream: u32,
410    data: &[u8],
411    metadata: &[u8],
412) -> Result<usize, WriteError> {
413    let mut writer = FrameWriter::new(buf, TID_UNSOLICITED, Cmd::StrRecv)?;
414    stream_payload(&mut writer, stream, data, metadata)?;
415    Ok(writer.finish())
416}
417
418/// Payload of `CMD_PROP_GET`, `CMD_PROP_SET`, and `CMD_PROP_IS`.
419///
420/// For `CMD_PROP_GET` the value is empty.
421#[derive(Clone, Copy, Debug, PartialEq, Eq)]
422pub struct PropPayload<'a> {
423    pub key: u32,
424    pub value: &'a [u8],
425}
426
427impl<'a> PropPayload<'a> {
428    pub fn parse(payload: &'a [u8]) -> Result<Self, ParseError> {
429        let (key, consumed) = pui::decode(payload)?;
430        Ok(Self {
431            key,
432            value: &payload[consumed..],
433        })
434    }
435}
436
437/// Payload of `CMD_STR_SEND` and `CMD_STR_RECV`.
438#[derive(Clone, Copy, Debug, PartialEq, Eq)]
439pub struct StreamPayload<'a> {
440    pub stream: u32,
441    pub data: &'a [u8],
442    /// Stream-defined trailing metadata; may be empty.
443    pub metadata: &'a [u8],
444}
445
446impl<'a> StreamPayload<'a> {
447    pub fn parse(payload: &'a [u8]) -> Result<Self, ParseError> {
448        let (stream, consumed) = pui::decode(payload)?;
449        let rest = &payload[consumed..];
450        let [len_lo, len_hi, rest @ ..] = rest else {
451            return Err(ParseError::Truncated);
452        };
453        let data_len = usize::from(u16::from_le_bytes([*len_lo, *len_hi]));
454        if rest.len() < data_len {
455            return Err(ParseError::Truncated);
456        }
457        let (data, metadata) = rest.split_at(data_len);
458        Ok(Self {
459            stream,
460            data,
461            metadata,
462        })
463    }
464}
465
466#[cfg(test)]
467mod tests {
468    use super::*;
469    use crate::ids::{prop, stream};
470
471    #[test]
472    fn header_round_trip() {
473        for tid in 0..=TID_MAX {
474            let header = Header::new(tid).unwrap();
475            assert_eq!(header.tid(), tid);
476            assert_eq!(Header::from_byte(header.to_byte()), Some(header));
477        }
478        assert_eq!(Header::new(TID_MAX + 1), None);
479    }
480
481    #[test]
482    fn header_rejects_bad_bytes() {
483        // Wrong FLG patterns.
484        assert_eq!(Header::from_byte(0x00), None);
485        assert_eq!(Header::from_byte(0x40), None);
486        assert_eq!(Header::from_byte(0xC0), None);
487        // Each reserved bit set individually.
488        for bit in [0x08u8, 0x10, 0x20] {
489            assert_eq!(Header::from_byte(HEADER_FLG_PATTERN | bit), None);
490        }
491    }
492
493    #[test]
494    fn nop_frame() {
495        let mut buf = [0u8; 8];
496        let len = nop(&mut buf, 3).unwrap();
497        assert_eq!(&buf[..len], &[0x83, 0x00]);
498
499        let frame = Frame::parse(&buf[..len]).unwrap();
500        assert_eq!(frame.header.tid(), 3);
501        assert_eq!(frame.command(), Some(Cmd::Nop));
502        assert!(frame.payload.is_empty());
503    }
504
505    #[test]
506    fn prop_get_round_trip() {
507        let mut buf = [0u8; 8];
508        let len = prop_get(&mut buf, 1, prop::PHY_DUTY_LIMIT).unwrap();
509        assert_eq!(&buf[..len], &[0x81, 0x02, 0xD6, 0x25]);
510
511        let frame = Frame::parse(&buf[..len]).unwrap();
512        assert_eq!(frame.command(), Some(Cmd::PropGet));
513        let payload = PropPayload::parse(frame.payload).unwrap();
514        assert_eq!(payload.key, prop::PHY_DUTY_LIMIT);
515        assert!(payload.value.is_empty());
516    }
517
518    #[test]
519    fn prop_set_round_trip() {
520        let mut buf = [0u8; 16];
521        let len = prop_set(&mut buf, 2, prop::PHY_FREQ, &906_875u32.to_le_bytes()).unwrap();
522
523        let frame = Frame::parse(&buf[..len]).unwrap();
524        assert_eq!(frame.header.tid(), 2);
525        assert_eq!(frame.command(), Some(Cmd::PropSet));
526        let payload = PropPayload::parse(frame.payload).unwrap();
527        assert_eq!(payload.key, prop::PHY_FREQ);
528        assert_eq!(payload.value, &906_875u32.to_le_bytes());
529    }
530
531    #[test]
532    fn last_status_frame() {
533        let mut buf = [0u8; 8];
534        let len = last_status(&mut buf, 5, Status::DUTY_LIMIT).unwrap();
535        assert_eq!(&buf[..len], &[0x85, 0x06, 0x00, 0x20]);
536
537        let frame = Frame::parse(&buf[..len]).unwrap();
538        let payload = PropPayload::parse(frame.payload).unwrap();
539        assert_eq!(payload.key, prop::LAST_STATUS);
540        let (code, consumed) = crate::pui::decode(payload.value).unwrap();
541        assert_eq!(Status(code), Status::DUTY_LIMIT);
542        assert_eq!(consumed, payload.value.len());
543    }
544
545    #[test]
546    fn stream_round_trip() {
547        let mut buf = [0u8; 32];
548        let data = [0xDEu8, 0xAD, 0xBE, 0xEF];
549        let meta = [0x7Fu8, 0x00];
550        let len = str_send(&mut buf, 4, stream::PHY_RAW, &data, &meta).unwrap();
551
552        let frame = Frame::parse(&buf[..len]).unwrap();
553        assert_eq!(frame.command(), Some(Cmd::StrSend));
554        let payload = StreamPayload::parse(frame.payload).unwrap();
555        assert_eq!(payload.stream, stream::PHY_RAW);
556        assert_eq!(payload.data, &data);
557        assert_eq!(payload.metadata, &meta);
558    }
559
560    #[test]
561    fn stream_without_metadata() {
562        let mut buf = [0u8; 16];
563        let len = str_recv(&mut buf, stream::PHY_RAW, &[0xAA], &[]).unwrap();
564
565        let frame = Frame::parse(&buf[..len]).unwrap();
566        assert_eq!(frame.header.tid(), TID_UNSOLICITED);
567        let payload = StreamPayload::parse(frame.payload).unwrap();
568        assert_eq!(payload.data, &[0xAA]);
569        assert!(payload.metadata.is_empty());
570    }
571
572    #[test]
573    fn stream_truncated_data() {
574        // Claims 4 data bytes but carries 2.
575        let payload = [0x71, 0x04, 0x00, 0xAA, 0xBB];
576        assert_eq!(StreamPayload::parse(&payload), Err(ParseError::Truncated));
577    }
578
579    #[test]
580    fn parse_rejects_malformed() {
581        assert_eq!(Frame::parse(&[]), Err(ParseError::Truncated));
582        assert_eq!(Frame::parse(&[0x80]), Err(ParseError::Truncated));
583        assert_eq!(Frame::parse(&[0x00, 0x00]), Err(ParseError::BadFlag));
584        assert_eq!(Frame::parse(&[0x88, 0x00]), Err(ParseError::ReservedBits));
585        assert_eq!(Frame::parse(&[0x80, 0x80]), Err(ParseError::BadCommand));
586    }
587
588    #[test]
589    fn unknown_command_is_well_formed() {
590        let frame = Frame::parse(&[0x81, 0x10]).unwrap();
591        assert_eq!(frame.cmd, 16);
592        assert_eq!(frame.command(), None);
593    }
594
595    #[test]
596    fn every_assigned_command_round_trips() {
597        for id in 0..=15u8 {
598            let cmd = Cmd::from_u8(id).unwrap_or_else(|| panic!("command {id} unassigned"));
599            assert_eq!(cmd as u8, id);
600        }
601        assert_eq!(Cmd::from_u8(16), None);
602    }
603
604    #[test]
605    fn insert_remove_round_trip() {
606        let mut buf = [0u8; 80];
607        let item = [0xA5u8; 33];
608        let len = prop_insert(&mut buf, 3, prop::HOST_RX_FILTERS, &item).unwrap();
609        let frame = Frame::parse(&buf[..len]).unwrap();
610        assert_eq!(frame.command(), Some(Cmd::PropInsert));
611        let payload = PropPayload::parse(frame.payload).unwrap();
612        assert_eq!(payload.key, prop::HOST_RX_FILTERS);
613        assert_eq!(payload.value, &item);
614
615        let len = prop_remove(&mut buf, 4, prop::HOST_PEER_KEYS, &item[..32]).unwrap();
616        let frame = Frame::parse(&buf[..len]).unwrap();
617        assert_eq!(frame.command(), Some(Cmd::PropRemove));
618        let payload = PropPayload::parse(frame.payload).unwrap();
619        assert_eq!(payload.key, prop::HOST_PEER_KEYS);
620        assert_eq!(payload.value, &item[..32]);
621    }
622
623    #[test]
624    fn inserted_removed_round_trip() {
625        let mut buf = [0u8; 48];
626        let digest = [0x42u8; 32];
627        let len = prop_inserted(&mut buf, 5, prop::HOST_PEER_KEYS, &digest).unwrap();
628        let frame = Frame::parse(&buf[..len]).unwrap();
629        assert_eq!(frame.command(), Some(Cmd::PropInserted));
630        let payload = PropPayload::parse(frame.payload).unwrap();
631        assert_eq!(payload.value, &digest);
632
633        let len = prop_removed(
634            &mut buf,
635            TID_UNSOLICITED,
636            prop::HOST_CHANNEL_KEYS,
637            &[0x12, 0x34],
638        )
639        .unwrap();
640        let frame = Frame::parse(&buf[..len]).unwrap();
641        assert_eq!(frame.command(), Some(Cmd::PropRemoved));
642        assert_eq!(frame.header.tid(), TID_UNSOLICITED);
643        let payload = PropPayload::parse(frame.payload).unwrap();
644        assert_eq!(payload.key, prop::HOST_CHANNEL_KEYS);
645        assert_eq!(payload.value, &[0x12, 0x34]);
646    }
647
648    #[test]
649    fn payloadless_full_commands() {
650        let mut buf = [0u8; 4];
651        for (encode, cmd) in [
652            (
653                queue_drain as fn(&mut [u8], u8) -> Result<usize, WriteError>,
654                Cmd::QueueDrain,
655            ),
656            (save, Cmd::Save),
657            (clear, Cmd::Clear),
658            (restore, Cmd::Restore),
659            (factory_reset, Cmd::FactoryReset),
660        ] {
661            let len = encode(&mut buf, 2).unwrap();
662            assert_eq!(len, 2);
663            let frame = Frame::parse(&buf[..len]).unwrap();
664            assert_eq!(frame.command(), Some(cmd));
665            assert!(frame.payload.is_empty());
666        }
667    }
668
669    #[test]
670    fn writer_reports_overflow() {
671        let mut buf = [0u8; 3];
672        assert_eq!(
673            prop_set(&mut buf, 1, prop::PHY_FREQ, &[0; 8]),
674            Err(WriteError::BufferTooSmall)
675        );
676    }
677}