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    /// `CMD_PROP_ARE`: several properties at once. Its payload is a
42    /// list of entries rather than one key and value, so it has no
43    /// [`PropertyNotification`] form — iterate it with
44    /// [`crate::frame::MultiEntries`].
45    Are,
46}
47
48impl PropertyNotificationKind {
49    pub const fn from_command(command: Cmd) -> Option<Self> {
50        match command {
51            Cmd::PropIs => Some(Self::Is),
52            Cmd::PropInserted => Some(Self::Inserted),
53            Cmd::PropRemoved => Some(Self::Removed),
54            Cmd::PropAre => Some(Self::Are),
55            _ => None,
56        }
57    }
58
59    /// The command this kind was decoded from.
60    pub const fn command(self) -> Cmd {
61        match self {
62            Self::Is => Cmd::PropIs,
63            Self::Inserted => Cmd::PropInserted,
64            Self::Removed => Cmd::PropRemoved,
65            Self::Are => Cmd::PropAre,
66        }
67    }
68
69    /// Whether this notification carries one key and value.
70    pub const fn is_single_property(self) -> bool {
71        !matches!(self, Self::Are)
72    }
73}
74
75/// A validated property notification borrowing its value from the frame.
76#[derive(Clone, Copy, Debug, PartialEq, Eq)]
77pub struct PropertyNotification<'a> {
78    pub tid: u8,
79    pub kind: PropertyNotificationKind,
80    pub key: u32,
81    pub value: &'a [u8],
82}
83
84impl<'a> PropertyNotification<'a> {
85    /// Parse exactly the three property notification commands accepted by a
86    /// host session. Requests, streams, and unknown commands are rejected.
87    pub fn parse(bytes: &'a [u8]) -> Result<Self, PropertyNotificationError> {
88        let frame = Frame::parse(bytes).map_err(|_| PropertyNotificationError::MalformedFrame)?;
89        Self::from_frame(&frame)
90    }
91
92    pub fn from_frame(frame: &Frame<'a>) -> Result<Self, PropertyNotificationError> {
93        let kind = frame
94            .command()
95            .and_then(PropertyNotificationKind::from_command)
96            .filter(|kind| kind.is_single_property())
97            .ok_or(PropertyNotificationError::UnexpectedCommand)?;
98        let payload = PropPayload::parse(frame.payload)
99            .map_err(|_| PropertyNotificationError::MalformedPayload)?;
100        Ok(Self {
101            tid: frame.header.tid(),
102            kind,
103            key: payload.key,
104            value: payload.value,
105        })
106    }
107}
108
109#[derive(Clone, Copy, Debug, PartialEq, Eq)]
110pub enum PropertyNotificationError {
111    MalformedFrame,
112    UnexpectedCommand,
113    MalformedPayload,
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn transaction_ids_never_allocate_zero() {
122        let mut allocator = TidAllocator::new();
123        let ids: [u8; 10] = core::array::from_fn(|_| allocator.allocate());
124        assert_eq!(ids, [1, 2, 3, 4, 5, 6, 7, 1, 2, 3]);
125    }
126
127    #[test]
128    fn parses_property_notification() {
129        let mut bytes = [0; 16];
130        let len = frame::prop_is(&mut bytes, 3, 0x1234, &[5, 6]).unwrap();
131        assert_eq!(
132            PropertyNotification::parse(&bytes[..len]).unwrap(),
133            PropertyNotification {
134                tid: 3,
135                kind: PropertyNotificationKind::Is,
136                key: 0x1234,
137                value: &[5, 6],
138            }
139        );
140    }
141
142    #[test]
143    fn multi_property_notifications_have_no_single_property_form() {
144        let mut bytes = [0; 32];
145        let mut writer = frame::prop_are(&mut bytes, 2).unwrap();
146        writer.write_entry(0x1234, &[7]).unwrap();
147        let len = writer.finish();
148        assert_eq!(
149            PropertyNotification::parse(&bytes[..len]),
150            Err(PropertyNotificationError::UnexpectedCommand)
151        );
152        assert_eq!(
153            PropertyNotificationKind::from_command(Cmd::PropAre),
154            Some(PropertyNotificationKind::Are)
155        );
156        assert!(!PropertyNotificationKind::Are.is_single_property());
157    }
158
159    #[test]
160    fn rejects_non_property_command() {
161        let mut bytes = [0; 8];
162        let len = frame::save(&mut bytes, 1).unwrap();
163        assert_eq!(
164            PropertyNotification::parse(&bytes[..len]),
165            Err(PropertyNotificationError::UnexpectedCommand)
166        );
167    }
168}