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    /// Restart the hardware (host to device): the device reboots as if
104    /// power-cycled, keeping every piece of persisted state. Unlike
105    /// `CMD_RST` (which returns protocol state to its post-reset values
106    /// with the device still running), this drops the link, and like
107    /// `CMD_FACTORY_RESET` it does not reply. Requires `CAP_REBOOT`; a
108    /// device without it answers `STATUS_UNIMPLEMENTED` instead.
109    Reboot = 16,
110    /// Delete every stored Bluetooth bond (host to device), along with the
111    /// pairing passkey and the pairing failure lockout, then enter pairing
112    /// mode so the device can be paired again. Bonded hosts lose their
113    /// bonds and any attached over Bluetooth are dropped, including the one
114    /// that sent the command. Requires `CAP_BLE`; a device that does not
115    /// manage its own bonds answers `STATUS_UNIMPLEMENTED`.
116    BleClearBonds = 17,
117    // 18 briefly held CMD_BLE_START_PAIRING; the pairing window is
118    // `PROP_BLE_PAIRING` now. A window that can be opened but not closed
119    // or observed was half a control, and only a property can publish
120    // the window closing on its own.
121    /// Get several property values (host to device). Requires
122    /// `CAP_CMD_MULTI`.
123    PropMultiGet = 21,
124    /// Set several property values in order (host to device). Requires
125    /// `CAP_CMD_MULTI`.
126    PropMultiSet = 22,
127    /// Multiple property value notification (device to host), answering
128    /// `CMD_PROP_MULTI_GET` or `CMD_PROP_MULTI_SET`. Never unsolicited.
129    PropAre = 23,
130}
131
132impl Cmd {
133    pub const fn from_u8(value: u8) -> Option<Self> {
134        match value {
135            0 => Some(Self::Nop),
136            1 => Some(Self::Reset),
137            2 => Some(Self::PropGet),
138            3 => Some(Self::PropSet),
139            4 => Some(Self::PropInsert),
140            5 => Some(Self::PropRemove),
141            6 => Some(Self::PropIs),
142            7 => Some(Self::PropInserted),
143            8 => Some(Self::PropRemoved),
144            9 => Some(Self::StrSend),
145            10 => Some(Self::StrRecv),
146            11 => Some(Self::QueueDrain),
147            12 => Some(Self::Save),
148            13 => Some(Self::Clear),
149            14 => Some(Self::Restore),
150            15 => Some(Self::FactoryReset),
151            16 => Some(Self::Reboot),
152            17 => Some(Self::BleClearBonds),
153            21 => Some(Self::PropMultiGet),
154            22 => Some(Self::PropMultiSet),
155            23 => Some(Self::PropAre),
156            _ => None,
157        }
158    }
159}
160
161#[derive(Clone, Copy, Debug, PartialEq, Eq)]
162pub enum ParseError {
163    /// The input ended before the structure was complete.
164    Truncated,
165    /// The header `FLG` pattern did not match; not a ULCP frame.
166    BadFlag,
167    /// A reserved header bit was set; the frame must be ignored.
168    ReservedBits,
169    /// The command identifier had its most significant bit set; the
170    /// frame must be ignored.
171    BadCommand,
172    /// A packed unsigned integer inside the payload was malformed.
173    BadPui,
174}
175
176impl From<pui::Error> for ParseError {
177    fn from(error: pui::Error) -> Self {
178        match error {
179            pui::Error::Truncated => Self::Truncated,
180            _ => Self::BadPui,
181        }
182    }
183}
184
185/// A parsed frame borrowing its payload from the input.
186///
187/// `cmd` is kept as the raw identifier so receivers can distinguish an
188/// unknown-but-well-formed command (respond with
189/// `STATUS_INVALID_COMMAND`) from a malformed frame (ignore).
190#[derive(Clone, Copy, Debug, PartialEq, Eq)]
191pub struct Frame<'a> {
192    pub header: Header,
193    pub cmd: u8,
194    pub payload: &'a [u8],
195}
196
197impl<'a> Frame<'a> {
198    pub fn parse(bytes: &'a [u8]) -> Result<Self, ParseError> {
199        let [header_byte, cmd, payload @ ..] = bytes else {
200            return Err(ParseError::Truncated);
201        };
202        if header_byte & HEADER_FLG_MASK != HEADER_FLG_PATTERN {
203            return Err(ParseError::BadFlag);
204        }
205        let header = Header::from_byte(*header_byte).ok_or(ParseError::ReservedBits)?;
206        if cmd & 0x80 != 0 {
207            return Err(ParseError::BadCommand);
208        }
209        Ok(Self {
210            header,
211            cmd: *cmd,
212            payload,
213        })
214    }
215
216    /// The command, if it is one defined by this crate.
217    pub const fn command(&self) -> Option<Cmd> {
218        Cmd::from_u8(self.cmd)
219    }
220}
221
222#[derive(Clone, Copy, Debug, PartialEq, Eq)]
223pub enum WriteError {
224    /// The output buffer cannot hold the frame.
225    BufferTooSmall,
226    /// The transaction identifier exceeds [`TID_MAX`].
227    InvalidTid,
228    /// A value exceeds the range of its wire representation.
229    ValueTooLarge,
230}
231
232impl From<pui::Error> for WriteError {
233    fn from(error: pui::Error) -> Self {
234        match error {
235            pui::Error::BufferTooSmall => Self::BufferTooSmall,
236            _ => Self::ValueTooLarge,
237        }
238    }
239}
240
241/// Incremental frame builder over a caller-provided buffer.
242pub struct FrameWriter<'a> {
243    buf: &'a mut [u8],
244    len: usize,
245}
246
247impl<'a> FrameWriter<'a> {
248    /// Start a frame with the given TID and command.
249    pub fn new(buf: &'a mut [u8], tid: u8, cmd: Cmd) -> Result<Self, WriteError> {
250        let header = Header::new(tid).ok_or(WriteError::InvalidTid)?;
251        let mut writer = Self { buf, len: 0 };
252        writer.write_u8(header.to_byte())?;
253        writer.write_u8(cmd as u8)?;
254        Ok(writer)
255    }
256
257    pub fn write_u8(&mut self, byte: u8) -> Result<(), WriteError> {
258        if self.len >= self.buf.len() {
259            return Err(WriteError::BufferTooSmall);
260        }
261        self.buf[self.len] = byte;
262        self.len += 1;
263        Ok(())
264    }
265
266    pub fn write_bytes(&mut self, bytes: &[u8]) -> Result<(), WriteError> {
267        let end = self
268            .len
269            .checked_add(bytes.len())
270            .ok_or(WriteError::BufferTooSmall)?;
271        if end > self.buf.len() {
272            return Err(WriteError::BufferTooSmall);
273        }
274        self.buf[self.len..end].copy_from_slice(bytes);
275        self.len = end;
276        Ok(())
277    }
278
279    pub fn write_pui(&mut self, value: u32) -> Result<(), WriteError> {
280        let written = pui::encode(value, &mut self.buf[self.len..])?;
281        self.len += written;
282        Ok(())
283    }
284
285    pub fn write_u16_le(&mut self, value: u16) -> Result<(), WriteError> {
286        self.write_bytes(&value.to_le_bytes())
287    }
288
289    pub fn write_u32_le(&mut self, value: u32) -> Result<(), WriteError> {
290        self.write_bytes(&value.to_le_bytes())
291    }
292
293    /// Bytes still available in the buffer.
294    pub const fn remaining(&self) -> usize {
295        self.buf.len() - self.len
296    }
297
298    /// Bytes written so far.
299    pub const fn len(&self) -> usize {
300        self.len
301    }
302
303    pub const fn is_empty(&self) -> bool {
304        self.len == 0
305    }
306
307    /// Append one multi-property entry: the combined length of the key
308    /// and value, then the key, then the value.
309    ///
310    /// The entry is written whole or not at all, so a caller that runs
311    /// out of room keeps a well-formed frame of the entries that fit.
312    pub fn write_entry(&mut self, key: u32, value: &[u8]) -> Result<(), WriteError> {
313        let total = entry_len(key, value.len()).ok_or(WriteError::ValueTooLarge)?;
314        if self.remaining() < total {
315            return Err(WriteError::BufferTooSmall);
316        }
317        let body = pui::encoded_len(key) + value.len();
318        self.write_pui(body as u32)?;
319        self.write_pui(key)?;
320        self.write_bytes(value)
321    }
322
323    /// Append an entry reporting a status in the position of the property
324    /// it answers.
325    pub fn write_status_entry(&mut self, status: Status) -> Result<(), WriteError> {
326        let mut value = [0u8; pui::MAX_LEN];
327        let len = pui::encode(status.0, &mut value)?;
328        self.write_entry(crate::ids::prop::LAST_STATUS, &value[..len])
329    }
330
331    /// Finish the frame, returning its total length in the buffer.
332    pub fn finish(self) -> usize {
333        self.len
334    }
335}
336
337/// Space a multi-property entry occupies, its length prefix included.
338///
339/// Returns `None` when the combined key and value exceed what the length
340/// prefix can express.
341pub const fn entry_len(key: u32, value_len: usize) -> Option<usize> {
342    let body = pui::encoded_len(key) + value_len;
343    if body > pui::MAX_VALUE as usize {
344        return None;
345    }
346    Some(pui::encoded_len(body as u32) + body)
347}
348
349/// Encode a `CMD_NOP` frame.
350pub fn nop(buf: &mut [u8], tid: u8) -> Result<usize, WriteError> {
351    Ok(FrameWriter::new(buf, tid, Cmd::Nop)?.finish())
352}
353
354/// Encode a `CMD_RST` frame.
355pub fn reset(buf: &mut [u8], tid: u8) -> Result<usize, WriteError> {
356    Ok(FrameWriter::new(buf, tid, Cmd::Reset)?.finish())
357}
358
359/// Encode a `CMD_PROP_GET` frame.
360pub fn prop_get(buf: &mut [u8], tid: u8, key: u32) -> Result<usize, WriteError> {
361    let mut writer = FrameWriter::new(buf, tid, Cmd::PropGet)?;
362    writer.write_pui(key)?;
363    Ok(writer.finish())
364}
365
366/// Encode a `CMD_PROP_SET` frame.
367pub fn prop_set(buf: &mut [u8], tid: u8, key: u32, value: &[u8]) -> Result<usize, WriteError> {
368    let mut writer = FrameWriter::new(buf, tid, Cmd::PropSet)?;
369    writer.write_pui(key)?;
370    writer.write_bytes(value)?;
371    Ok(writer.finish())
372}
373
374/// Encode a `CMD_PROP_IS` frame.
375pub fn prop_is(buf: &mut [u8], tid: u8, key: u32, value: &[u8]) -> Result<usize, WriteError> {
376    let mut writer = FrameWriter::new(buf, tid, Cmd::PropIs)?;
377    writer.write_pui(key)?;
378    writer.write_bytes(value)?;
379    Ok(writer.finish())
380}
381
382/// Encode a `CMD_PROP_INSERT` frame. `item` is one item in the
383/// property's item form, with no length prefix.
384pub fn prop_insert(buf: &mut [u8], tid: u8, key: u32, item: &[u8]) -> Result<usize, WriteError> {
385    let mut writer = FrameWriter::new(buf, tid, Cmd::PropInsert)?;
386    writer.write_pui(key)?;
387    writer.write_bytes(item)?;
388    Ok(writer.finish())
389}
390
391/// Encode a `CMD_PROP_REMOVE` frame. `selector` is the property's
392/// documented item selector, with no length prefix.
393pub fn prop_remove(
394    buf: &mut [u8],
395    tid: u8,
396    key: u32,
397    selector: &[u8],
398) -> Result<usize, WriteError> {
399    let mut writer = FrameWriter::new(buf, tid, Cmd::PropRemove)?;
400    writer.write_pui(key)?;
401    writer.write_bytes(selector)?;
402    Ok(writer.finish())
403}
404
405/// Encode a `CMD_PROP_INSERTED` frame. `digest` is the inserted item in
406/// the property's digest form — never in a form containing key material.
407pub fn prop_inserted(
408    buf: &mut [u8],
409    tid: u8,
410    key: u32,
411    digest: &[u8],
412) -> Result<usize, WriteError> {
413    let mut writer = FrameWriter::new(buf, tid, Cmd::PropInserted)?;
414    writer.write_pui(key)?;
415    writer.write_bytes(digest)?;
416    Ok(writer.finish())
417}
418
419/// Encode a `CMD_PROP_REMOVED` frame. `digest` is the removed item in
420/// the property's digest form.
421pub fn prop_removed(buf: &mut [u8], tid: u8, key: u32, digest: &[u8]) -> Result<usize, WriteError> {
422    let mut writer = FrameWriter::new(buf, tid, Cmd::PropRemoved)?;
423    writer.write_pui(key)?;
424    writer.write_bytes(digest)?;
425    Ok(writer.finish())
426}
427
428/// Encode a `CMD_PROP_MULTI_GET` frame: the property identifiers one
429/// after another, with no delimiters.
430pub fn prop_multi_get(buf: &mut [u8], tid: u8, keys: &[u32]) -> Result<usize, WriteError> {
431    let mut writer = FrameWriter::new(buf, tid, Cmd::PropMultiGet)?;
432    for &key in keys {
433        writer.write_pui(key)?;
434    }
435    Ok(writer.finish())
436}
437
438/// Encode a `CMD_PROP_MULTI_SET` frame from key and value pairs.
439pub fn prop_multi_set(
440    buf: &mut [u8],
441    tid: u8,
442    entries: &[(u32, &[u8])],
443) -> Result<usize, WriteError> {
444    let mut writer = FrameWriter::new(buf, tid, Cmd::PropMultiSet)?;
445    for &(key, value) in entries {
446        writer.write_entry(key, value)?;
447    }
448    Ok(writer.finish())
449}
450
451/// Begin a `CMD_PROP_ARE` frame, to which the caller appends entries with
452/// [`FrameWriter::write_entry`] and [`FrameWriter::write_status_entry`].
453pub fn prop_are(buf: &mut [u8], tid: u8) -> Result<FrameWriter<'_>, WriteError> {
454    FrameWriter::new(buf, tid, Cmd::PropAre)
455}
456
457/// One entry of a `CMD_PROP_MULTI_SET` or `CMD_PROP_ARE` payload.
458#[derive(Clone, Copy, Debug, PartialEq, Eq)]
459pub struct MultiEntry<'a> {
460    pub key: u32,
461    pub value: &'a [u8],
462}
463
464/// Iterator over the entries of a `CMD_PROP_MULTI_SET` or `CMD_PROP_ARE`
465/// payload.
466///
467/// A malformed entry yields one error and ends the iteration: the
468/// remaining bytes cannot be located once a length is untrustworthy.
469#[derive(Clone, Copy, Debug)]
470pub struct MultiEntries<'a> {
471    rest: &'a [u8],
472}
473
474impl<'a> MultiEntries<'a> {
475    pub const fn new(payload: &'a [u8]) -> Self {
476        Self { rest: payload }
477    }
478
479    /// The bytes not yet consumed, so a caller serving entries across
480    /// several calls can record where it stopped.
481    pub const fn remainder(&self) -> &'a [u8] {
482        self.rest
483    }
484
485    fn take_entry(&mut self) -> Result<MultiEntry<'a>, ParseError> {
486        let payload = core::mem::take(&mut self.rest);
487        let (body_len, consumed) = pui::decode(payload)?;
488        let after_len = &payload[consumed..];
489        let body_len = body_len as usize;
490        if after_len.len() < body_len {
491            return Err(ParseError::Truncated);
492        }
493        let (body, tail) = after_len.split_at(body_len);
494        let (key, key_len) = pui::decode(body)?;
495        self.rest = tail;
496        Ok(MultiEntry {
497            key,
498            value: &body[key_len..],
499        })
500    }
501}
502
503impl<'a> Iterator for MultiEntries<'a> {
504    type Item = Result<MultiEntry<'a>, ParseError>;
505
506    fn next(&mut self) -> Option<Self::Item> {
507        if self.rest.is_empty() {
508            return None;
509        }
510        Some(self.take_entry())
511    }
512}
513
514/// Iterator over the property identifiers of a `CMD_PROP_MULTI_GET`
515/// payload.
516#[derive(Clone, Copy, Debug)]
517pub struct MultiGetKeys<'a> {
518    rest: &'a [u8],
519}
520
521impl<'a> MultiGetKeys<'a> {
522    pub const fn new(payload: &'a [u8]) -> Self {
523        Self { rest: payload }
524    }
525
526    /// The bytes not yet consumed.
527    pub const fn remainder(&self) -> &'a [u8] {
528        self.rest
529    }
530}
531
532impl Iterator for MultiGetKeys<'_> {
533    type Item = Result<u32, ParseError>;
534
535    fn next(&mut self) -> Option<Self::Item> {
536        if self.rest.is_empty() {
537            return None;
538        }
539        let payload = core::mem::take(&mut self.rest);
540        match pui::decode(payload) {
541            Ok((key, consumed)) => {
542                self.rest = &payload[consumed..];
543                Some(Ok(key))
544            }
545            Err(error) => Some(Err(error.into())),
546        }
547    }
548}
549
550/// Encode a `CMD_QUEUE_DRAIN` frame (no payload).
551pub fn queue_drain(buf: &mut [u8], tid: u8) -> Result<usize, WriteError> {
552    Ok(FrameWriter::new(buf, tid, Cmd::QueueDrain)?.finish())
553}
554
555/// Encode a `CMD_SAVE` frame (no payload).
556pub fn save(buf: &mut [u8], tid: u8) -> Result<usize, WriteError> {
557    Ok(FrameWriter::new(buf, tid, Cmd::Save)?.finish())
558}
559
560/// Encode a `CMD_CLEAR` frame (no payload).
561pub fn clear(buf: &mut [u8], tid: u8) -> Result<usize, WriteError> {
562    Ok(FrameWriter::new(buf, tid, Cmd::Clear)?.finish())
563}
564
565/// Encode a `CMD_FACTORY_RESET` frame (no payload). The device erases all
566/// mutable state and reboots without replying.
567pub fn factory_reset(buf: &mut [u8], tid: u8) -> Result<usize, WriteError> {
568    Ok(FrameWriter::new(buf, tid, Cmd::FactoryReset)?.finish())
569}
570
571/// Encode a `CMD_REBOOT` frame (no payload). The device restarts without
572/// replying, keeping everything it has persisted.
573pub fn reboot(buf: &mut [u8], tid: u8) -> Result<usize, WriteError> {
574    Ok(FrameWriter::new(buf, tid, Cmd::Reboot)?.finish())
575}
576
577/// Encode a `CMD_BLE_CLEAR_BONDS` frame (no payload). The device answers
578/// once the bonds are gone, which over Bluetooth is the last thing the
579/// sender hears before it is dropped.
580pub fn ble_clear_bonds(buf: &mut [u8], tid: u8) -> Result<usize, WriteError> {
581    Ok(FrameWriter::new(buf, tid, Cmd::BleClearBonds)?.finish())
582}
583
584/// Encode a `CMD_RESTORE` frame (no payload).
585pub fn restore(buf: &mut [u8], tid: u8) -> Result<usize, WriteError> {
586    Ok(FrameWriter::new(buf, tid, Cmd::Restore)?.finish())
587}
588
589/// Encode a `CMD_PROP_IS` frame carrying `PROP_LAST_STATUS`.
590pub fn last_status(buf: &mut [u8], tid: u8, status: Status) -> Result<usize, WriteError> {
591    let mut writer = FrameWriter::new(buf, tid, Cmd::PropIs)?;
592    writer.write_pui(crate::ids::prop::LAST_STATUS)?;
593    writer.write_pui(status.0)?;
594    Ok(writer.finish())
595}
596
597fn stream_payload(
598    writer: &mut FrameWriter<'_>,
599    stream: u32,
600    data: &[u8],
601    metadata: &[u8],
602) -> Result<(), WriteError> {
603    let data_len = u16::try_from(data.len()).map_err(|_| WriteError::ValueTooLarge)?;
604    writer.write_pui(stream)?;
605    writer.write_u16_le(data_len)?;
606    writer.write_bytes(data)?;
607    writer.write_bytes(metadata)
608}
609
610/// Encode a `CMD_STR_SEND` frame.
611pub fn str_send(
612    buf: &mut [u8],
613    tid: u8,
614    stream: u32,
615    data: &[u8],
616    metadata: &[u8],
617) -> Result<usize, WriteError> {
618    let mut writer = FrameWriter::new(buf, tid, Cmd::StrSend)?;
619    stream_payload(&mut writer, stream, data, metadata)?;
620    Ok(writer.finish())
621}
622
623/// Encode a `CMD_STR_RECV` frame. Always uses TID zero.
624pub fn str_recv(
625    buf: &mut [u8],
626    stream: u32,
627    data: &[u8],
628    metadata: &[u8],
629) -> Result<usize, WriteError> {
630    let mut writer = FrameWriter::new(buf, TID_UNSOLICITED, Cmd::StrRecv)?;
631    stream_payload(&mut writer, stream, data, metadata)?;
632    Ok(writer.finish())
633}
634
635/// Payload of `CMD_PROP_GET`, `CMD_PROP_SET`, and `CMD_PROP_IS`.
636///
637/// For `CMD_PROP_GET` the value is empty.
638#[derive(Clone, Copy, Debug, PartialEq, Eq)]
639pub struct PropPayload<'a> {
640    pub key: u32,
641    pub value: &'a [u8],
642}
643
644impl<'a> PropPayload<'a> {
645    pub fn parse(payload: &'a [u8]) -> Result<Self, ParseError> {
646        let (key, consumed) = pui::decode(payload)?;
647        Ok(Self {
648            key,
649            value: &payload[consumed..],
650        })
651    }
652}
653
654/// Payload of `CMD_STR_SEND` and `CMD_STR_RECV`.
655#[derive(Clone, Copy, Debug, PartialEq, Eq)]
656pub struct StreamPayload<'a> {
657    pub stream: u32,
658    pub data: &'a [u8],
659    /// Stream-defined trailing metadata; may be empty.
660    pub metadata: &'a [u8],
661}
662
663impl<'a> StreamPayload<'a> {
664    pub fn parse(payload: &'a [u8]) -> Result<Self, ParseError> {
665        let (stream, consumed) = pui::decode(payload)?;
666        let rest = &payload[consumed..];
667        let [len_lo, len_hi, rest @ ..] = rest else {
668            return Err(ParseError::Truncated);
669        };
670        let data_len = usize::from(u16::from_le_bytes([*len_lo, *len_hi]));
671        if rest.len() < data_len {
672            return Err(ParseError::Truncated);
673        }
674        let (data, metadata) = rest.split_at(data_len);
675        Ok(Self {
676            stream,
677            data,
678            metadata,
679        })
680    }
681}
682
683#[cfg(test)]
684mod tests {
685    use super::*;
686    use crate::ids::{prop, stream};
687
688    #[test]
689    fn header_round_trip() {
690        for tid in 0..=TID_MAX {
691            let header = Header::new(tid).unwrap();
692            assert_eq!(header.tid(), tid);
693            assert_eq!(Header::from_byte(header.to_byte()), Some(header));
694        }
695        assert_eq!(Header::new(TID_MAX + 1), None);
696    }
697
698    #[test]
699    fn header_rejects_bad_bytes() {
700        // Wrong FLG patterns.
701        assert_eq!(Header::from_byte(0x00), None);
702        assert_eq!(Header::from_byte(0x40), None);
703        assert_eq!(Header::from_byte(0xC0), None);
704        // Each reserved bit set individually.
705        for bit in [0x08u8, 0x10, 0x20] {
706            assert_eq!(Header::from_byte(HEADER_FLG_PATTERN | bit), None);
707        }
708    }
709
710    #[test]
711    fn nop_frame() {
712        let mut buf = [0u8; 8];
713        let len = nop(&mut buf, 3).unwrap();
714        assert_eq!(&buf[..len], &[0x83, 0x00]);
715
716        let frame = Frame::parse(&buf[..len]).unwrap();
717        assert_eq!(frame.header.tid(), 3);
718        assert_eq!(frame.command(), Some(Cmd::Nop));
719        assert!(frame.payload.is_empty());
720    }
721
722    #[test]
723    fn prop_get_round_trip() {
724        let mut buf = [0u8; 8];
725        let len = prop_get(&mut buf, 1, prop::PHY_DUTY_LIMIT).unwrap();
726        assert_eq!(&buf[..len], &[0x81, 0x02, 0xD6, 0x25]);
727
728        let frame = Frame::parse(&buf[..len]).unwrap();
729        assert_eq!(frame.command(), Some(Cmd::PropGet));
730        let payload = PropPayload::parse(frame.payload).unwrap();
731        assert_eq!(payload.key, prop::PHY_DUTY_LIMIT);
732        assert!(payload.value.is_empty());
733    }
734
735    #[test]
736    fn prop_set_round_trip() {
737        let mut buf = [0u8; 16];
738        let len = prop_set(&mut buf, 2, prop::PHY_FREQ, &906_875u32.to_le_bytes()).unwrap();
739
740        let frame = Frame::parse(&buf[..len]).unwrap();
741        assert_eq!(frame.header.tid(), 2);
742        assert_eq!(frame.command(), Some(Cmd::PropSet));
743        let payload = PropPayload::parse(frame.payload).unwrap();
744        assert_eq!(payload.key, prop::PHY_FREQ);
745        assert_eq!(payload.value, &906_875u32.to_le_bytes());
746    }
747
748    #[test]
749    fn last_status_frame() {
750        let mut buf = [0u8; 8];
751        let len = last_status(&mut buf, 5, Status::DUTY_LIMIT).unwrap();
752        assert_eq!(&buf[..len], &[0x85, 0x06, 0x00, 0x20]);
753
754        let frame = Frame::parse(&buf[..len]).unwrap();
755        let payload = PropPayload::parse(frame.payload).unwrap();
756        assert_eq!(payload.key, prop::LAST_STATUS);
757        let (code, consumed) = crate::pui::decode(payload.value).unwrap();
758        assert_eq!(Status(code), Status::DUTY_LIMIT);
759        assert_eq!(consumed, payload.value.len());
760    }
761
762    #[test]
763    fn stream_round_trip() {
764        let mut buf = [0u8; 32];
765        let data = [0xDEu8, 0xAD, 0xBE, 0xEF];
766        let meta = [0x7Fu8, 0x00];
767        let len = str_send(&mut buf, 4, stream::PHY_RAW, &data, &meta).unwrap();
768
769        let frame = Frame::parse(&buf[..len]).unwrap();
770        assert_eq!(frame.command(), Some(Cmd::StrSend));
771        let payload = StreamPayload::parse(frame.payload).unwrap();
772        assert_eq!(payload.stream, stream::PHY_RAW);
773        assert_eq!(payload.data, &data);
774        assert_eq!(payload.metadata, &meta);
775    }
776
777    #[test]
778    fn stream_without_metadata() {
779        let mut buf = [0u8; 16];
780        let len = str_recv(&mut buf, stream::PHY_RAW, &[0xAA], &[]).unwrap();
781
782        let frame = Frame::parse(&buf[..len]).unwrap();
783        assert_eq!(frame.header.tid(), TID_UNSOLICITED);
784        let payload = StreamPayload::parse(frame.payload).unwrap();
785        assert_eq!(payload.data, &[0xAA]);
786        assert!(payload.metadata.is_empty());
787    }
788
789    #[test]
790    fn stream_truncated_data() {
791        // Claims 4 data bytes but carries 2.
792        let payload = [0x71, 0x04, 0x00, 0xAA, 0xBB];
793        assert_eq!(StreamPayload::parse(&payload), Err(ParseError::Truncated));
794    }
795
796    #[test]
797    fn parse_rejects_malformed() {
798        assert_eq!(Frame::parse(&[]), Err(ParseError::Truncated));
799        assert_eq!(Frame::parse(&[0x80]), Err(ParseError::Truncated));
800        assert_eq!(Frame::parse(&[0x00, 0x00]), Err(ParseError::BadFlag));
801        assert_eq!(Frame::parse(&[0x88, 0x00]), Err(ParseError::ReservedBits));
802        assert_eq!(Frame::parse(&[0x80, 0x80]), Err(ParseError::BadCommand));
803    }
804
805    #[test]
806    fn unknown_command_is_well_formed() {
807        let frame = Frame::parse(&[0x81, 0x12]).unwrap();
808        assert_eq!(frame.cmd, 18);
809        assert_eq!(frame.command(), None);
810    }
811
812    #[test]
813    fn every_assigned_command_round_trips() {
814        for id in (0..=17u8).chain(21..=23) {
815            let cmd = Cmd::from_u8(id).unwrap_or_else(|| panic!("command {id} unassigned"));
816            assert_eq!(cmd as u8, id);
817        }
818        for id in (18..=20u8).chain(24..=127) {
819            assert_eq!(Cmd::from_u8(id), None, "command {id} should be unassigned");
820        }
821    }
822
823    #[test]
824    fn insert_remove_round_trip() {
825        let mut buf = [0u8; 80];
826        let item = [0xA5u8; 33];
827        let len = prop_insert(&mut buf, 3, prop::HOST_RX_FILTERS, &item).unwrap();
828        let frame = Frame::parse(&buf[..len]).unwrap();
829        assert_eq!(frame.command(), Some(Cmd::PropInsert));
830        let payload = PropPayload::parse(frame.payload).unwrap();
831        assert_eq!(payload.key, prop::HOST_RX_FILTERS);
832        assert_eq!(payload.value, &item);
833
834        let len = prop_remove(&mut buf, 4, prop::HOST_PEER_KEYS, &item[..32]).unwrap();
835        let frame = Frame::parse(&buf[..len]).unwrap();
836        assert_eq!(frame.command(), Some(Cmd::PropRemove));
837        let payload = PropPayload::parse(frame.payload).unwrap();
838        assert_eq!(payload.key, prop::HOST_PEER_KEYS);
839        assert_eq!(payload.value, &item[..32]);
840    }
841
842    #[test]
843    fn inserted_removed_round_trip() {
844        let mut buf = [0u8; 48];
845        let digest = [0x42u8; 32];
846        let len = prop_inserted(&mut buf, 5, prop::HOST_PEER_KEYS, &digest).unwrap();
847        let frame = Frame::parse(&buf[..len]).unwrap();
848        assert_eq!(frame.command(), Some(Cmd::PropInserted));
849        let payload = PropPayload::parse(frame.payload).unwrap();
850        assert_eq!(payload.value, &digest);
851
852        let len = prop_removed(
853            &mut buf,
854            TID_UNSOLICITED,
855            prop::HOST_CHANNEL_KEYS,
856            &[0x12, 0x34],
857        )
858        .unwrap();
859        let frame = Frame::parse(&buf[..len]).unwrap();
860        assert_eq!(frame.command(), Some(Cmd::PropRemoved));
861        assert_eq!(frame.header.tid(), TID_UNSOLICITED);
862        let payload = PropPayload::parse(frame.payload).unwrap();
863        assert_eq!(payload.key, prop::HOST_CHANNEL_KEYS);
864        assert_eq!(payload.value, &[0x12, 0x34]);
865    }
866
867    #[test]
868    fn payloadless_full_commands() {
869        let mut buf = [0u8; 4];
870        for (encode, cmd) in [
871            (
872                queue_drain as fn(&mut [u8], u8) -> Result<usize, WriteError>,
873                Cmd::QueueDrain,
874            ),
875            (save, Cmd::Save),
876            (clear, Cmd::Clear),
877            (restore, Cmd::Restore),
878            (factory_reset, Cmd::FactoryReset),
879            (reboot, Cmd::Reboot),
880            (ble_clear_bonds, Cmd::BleClearBonds),
881        ] {
882            let len = encode(&mut buf, 2).unwrap();
883            assert_eq!(len, 2);
884            let frame = Frame::parse(&buf[..len]).unwrap();
885            assert_eq!(frame.command(), Some(cmd));
886            assert!(frame.payload.is_empty());
887        }
888    }
889
890    #[test]
891    fn multi_get_round_trip() {
892        let mut buf = [0u8; 32];
893        let keys = [prop::CAPS, prop::PHY_DUTY_LIMIT, prop::DEV_ADMINS];
894        let len = prop_multi_get(&mut buf, 6, &keys).unwrap();
895
896        let frame = Frame::parse(&buf[..len]).unwrap();
897        assert_eq!(frame.header.tid(), 6);
898        assert_eq!(frame.command(), Some(Cmd::PropMultiGet));
899        let decoded: Result<std::vec::Vec<_>, _> = MultiGetKeys::new(frame.payload).collect();
900        assert_eq!(decoded.unwrap(), keys);
901    }
902
903    #[test]
904    fn multi_set_round_trip() {
905        let mut buf = [0u8; 64];
906        let long = [0xA5u8; 32];
907        let entries: [(u32, &[u8]); 3] = [
908            (prop::PHY_FREQ, &906_875u32.to_le_bytes()),
909            (prop::DEV_ADMINS, &long),
910            (prop::PHY_TX_POWER, &[]),
911        ];
912        let len = prop_multi_set(&mut buf, 2, &entries).unwrap();
913
914        let frame = Frame::parse(&buf[..len]).unwrap();
915        assert_eq!(frame.command(), Some(Cmd::PropMultiSet));
916        let decoded: std::vec::Vec<_> = MultiEntries::new(frame.payload)
917            .map(|entry| entry.unwrap())
918            .map(|entry| (entry.key, entry.value))
919            .collect();
920        assert_eq!(decoded, entries);
921    }
922
923    #[test]
924    fn are_carries_values_and_statuses() {
925        let mut buf = [0u8; 32];
926        let mut writer = prop_are(&mut buf, 4).unwrap();
927        writer.write_entry(prop::PHY_TX_POWER, &[14]).unwrap();
928        writer.write_status_entry(Status::PROP_NOT_FOUND).unwrap();
929        let len = writer.finish();
930
931        let frame = Frame::parse(&buf[..len]).unwrap();
932        assert_eq!(frame.header.tid(), 4);
933        assert_eq!(frame.command(), Some(Cmd::PropAre));
934        let entries: std::vec::Vec<_> = MultiEntries::new(frame.payload)
935            .map(|entry| entry.unwrap())
936            .collect();
937        assert_eq!(entries.len(), 2);
938        assert_eq!(entries[0].key, prop::PHY_TX_POWER);
939        assert_eq!(entries[0].value, &[14]);
940        assert_eq!(entries[1].key, prop::LAST_STATUS);
941        let (code, _) = crate::pui::decode(entries[1].value).unwrap();
942        assert_eq!(Status(code), Status::PROP_NOT_FOUND);
943    }
944
945    #[test]
946    fn entry_length_matches_what_is_written() {
947        let mut buf = [0u8; 512];
948        // A value long enough to push the length prefix to two bytes.
949        for value_len in [0usize, 1, 125, 126, 127, 300] {
950            let value = std::vec![0x5Au8; value_len];
951            for key in [prop::CAPS, prop::PHY_DUTY_LIMIT] {
952                let mut writer = prop_are(&mut buf, 1).unwrap();
953                let before = writer.len();
954                writer.write_entry(key, &value).unwrap();
955                assert_eq!(
956                    writer.len() - before,
957                    entry_len(key, value_len).unwrap(),
958                    "key {key}, value length {value_len}"
959                );
960            }
961        }
962    }
963
964    #[test]
965    fn entry_that_does_not_fit_leaves_the_frame_intact() {
966        let mut buf = [0u8; 12];
967        let mut writer = prop_are(&mut buf, 1).unwrap();
968        writer.write_entry(prop::PHY_TX_POWER, &[7]).unwrap();
969        let after_first = writer.len();
970        assert_eq!(
971            writer.write_entry(prop::CAPS, &[0u8; 32]),
972            Err(WriteError::BufferTooSmall)
973        );
974        assert_eq!(writer.len(), after_first);
975
976        let len = writer.finish();
977        let frame = Frame::parse(&buf[..len]).unwrap();
978        let entries: std::vec::Vec<_> = MultiEntries::new(frame.payload)
979            .map(|entry| entry.unwrap())
980            .collect();
981        assert_eq!(entries.len(), 1);
982        assert_eq!(entries[0].value, &[7]);
983    }
984
985    #[test]
986    fn malformed_entries_end_the_iteration() {
987        // Body length 9 with only three bytes behind it.
988        let mut entries = MultiEntries::new(&[0x09, 0x71, 0xAA, 0xBB]);
989        assert_eq!(entries.next(), Some(Err(ParseError::Truncated)));
990        assert_eq!(entries.next(), None);
991
992        // A truncated key PUI inside an otherwise well-framed entry.
993        let mut entries = MultiEntries::new(&[0x01, 0x80]);
994        assert_eq!(entries.next(), Some(Err(ParseError::Truncated)));
995        assert_eq!(entries.next(), None);
996
997        let mut keys = MultiGetKeys::new(&[0x71, 0x80]);
998        assert_eq!(keys.next(), Some(Ok(113)));
999        assert_eq!(keys.next(), Some(Err(ParseError::Truncated)));
1000        assert_eq!(keys.next(), None);
1001    }
1002
1003    #[test]
1004    fn empty_multi_payloads_yield_nothing() {
1005        assert_eq!(MultiEntries::new(&[]).next(), None);
1006        assert_eq!(MultiGetKeys::new(&[]).next(), None);
1007    }
1008
1009    #[test]
1010    fn writer_reports_overflow() {
1011        let mut buf = [0u8; 3];
1012        assert_eq!(
1013            prop_set(&mut buf, 1, prop::PHY_FREQ, &[0; 8]),
1014            Err(WriteError::BufferTooSmall)
1015        );
1016    }
1017}