umsh_ulcp/
host.rs

1//! Transport-independent host transaction primitives.
2//!
3//! These small state/value types are shared by desktop and mobile hosts so
4//! transaction identifiers and property-notification classification cannot
5//! drift between otherwise platform-specific session drivers.
6
7use crate::frame::{self, Cmd, Frame, PropPayload};
8
9/// Cyclic allocator for the non-zero ULCP transaction identifiers.
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub struct TidAllocator {
12    next: u8,
13}
14
15impl TidAllocator {
16    /// Start a fresh protocol session at transaction identifier 1.
17    pub const fn new() -> Self {
18        Self { next: 1 }
19    }
20
21    /// Allocate the next identifier, wrapping from `TID_MAX` back to 1.
22    pub fn allocate(&mut self) -> u8 {
23        let tid = self.next;
24        self.next = if tid >= frame::TID_MAX { 1 } else { tid + 1 };
25        tid
26    }
27}
28
29impl Default for TidAllocator {
30    fn default() -> Self {
31        Self::new()
32    }
33}
34
35/// Which property notification command carried a value.
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub enum PropertyNotificationKind {
38    Is,
39    Inserted,
40    Removed,
41}
42
43impl PropertyNotificationKind {
44    pub const fn from_command(command: Cmd) -> Option<Self> {
45        match command {
46            Cmd::PropIs => Some(Self::Is),
47            Cmd::PropInserted => Some(Self::Inserted),
48            Cmd::PropRemoved => Some(Self::Removed),
49            _ => None,
50        }
51    }
52}
53
54/// A validated property notification borrowing its value from the frame.
55#[derive(Clone, Copy, Debug, PartialEq, Eq)]
56pub struct PropertyNotification<'a> {
57    pub tid: u8,
58    pub kind: PropertyNotificationKind,
59    pub key: u32,
60    pub value: &'a [u8],
61}
62
63impl<'a> PropertyNotification<'a> {
64    /// Parse exactly the three property notification commands accepted by a
65    /// host session. Requests, streams, and unknown commands are rejected.
66    pub fn parse(bytes: &'a [u8]) -> Result<Self, PropertyNotificationError> {
67        let frame = Frame::parse(bytes).map_err(|_| PropertyNotificationError::MalformedFrame)?;
68        Self::from_frame(&frame)
69    }
70
71    pub fn from_frame(frame: &Frame<'a>) -> Result<Self, PropertyNotificationError> {
72        let kind = frame
73            .command()
74            .and_then(PropertyNotificationKind::from_command)
75            .ok_or(PropertyNotificationError::UnexpectedCommand)?;
76        let payload = PropPayload::parse(frame.payload)
77            .map_err(|_| PropertyNotificationError::MalformedPayload)?;
78        Ok(Self {
79            tid: frame.header.tid(),
80            kind,
81            key: payload.key,
82            value: payload.value,
83        })
84    }
85}
86
87#[derive(Clone, Copy, Debug, PartialEq, Eq)]
88pub enum PropertyNotificationError {
89    MalformedFrame,
90    UnexpectedCommand,
91    MalformedPayload,
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn transaction_ids_never_allocate_zero() {
100        let mut allocator = TidAllocator::new();
101        let ids: [u8; 10] = core::array::from_fn(|_| allocator.allocate());
102        assert_eq!(ids, [1, 2, 3, 4, 5, 6, 7, 1, 2, 3]);
103    }
104
105    #[test]
106    fn parses_property_notification() {
107        let mut bytes = [0; 16];
108        let len = frame::prop_is(&mut bytes, 3, 0x1234, &[5, 6]).unwrap();
109        assert_eq!(
110            PropertyNotification::parse(&bytes[..len]).unwrap(),
111            PropertyNotification {
112                tid: 3,
113                kind: PropertyNotificationKind::Is,
114                key: 0x1234,
115                value: &[5, 6],
116            }
117        );
118    }
119
120    #[test]
121    fn rejects_non_property_command() {
122        let mut bytes = [0; 8];
123        let len = frame::save(&mut bytes, 1).unwrap();
124        assert_eq!(
125            PropertyNotification::parse(&bytes[..len]),
126            Err(PropertyNotificationError::UnexpectedCommand)
127        );
128    }
129}