umsh_ulcp/
reply.rs

1//! Reading what a device answered.
2//!
3//! A device answers a property request with the property, or with
4//! `PROP_LAST_STATUS` standing in its place — refused, absent, out of
5//! reach. Every reader of a reply has to tell those two apart before it can
6//! do anything with the bytes, and a multi-property answer has to do it per
7//! position, against the keys that were asked for. That reasoning lives
8//! here rather than in each caller.
9//!
10//! Nothing is copied: an [`Answer`] borrows from the reply it was read out
11//! of.
12
13use crate::frame::{Cmd, Frame, MultiEntries, ParseError, PropPayload};
14use crate::ids::prop;
15use crate::pui;
16use crate::status::Status;
17
18/// What occupied one position of a reply.
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub enum Answer<'a> {
21    /// The property's value, as the device reports it.
22    Value(&'a [u8]),
23    /// A status where a value was asked for.
24    Refused(Status),
25}
26
27impl<'a> Answer<'a> {
28    /// The value, or `None` when a status stood in for it.
29    pub const fn value(self) -> Option<&'a [u8]> {
30        match self {
31            Self::Value(value) => Some(value),
32            Self::Refused(_) => None,
33        }
34    }
35
36    /// The status, or `None` when the device answered with a value.
37    pub const fn status(self) -> Option<Status> {
38        match self {
39            Self::Value(_) => None,
40            Self::Refused(status) => Some(status),
41        }
42    }
43
44    /// Read one position: `PROP_LAST_STATUS` where something else was
45    /// asked for is a refusal, and anywhere else it is the value.
46    fn read(requested: u32, key: u32, value: &'a [u8]) -> Self {
47        if key == prop::LAST_STATUS && requested != prop::LAST_STATUS {
48            // A malformed status is still a refusal; there is no value
49            // here either way.
50            let status = pui::decode(value)
51                .map(|(code, _)| Status(code))
52                .unwrap_or(Status::FAILURE);
53            Self::Refused(status)
54        } else {
55            Self::Value(value)
56        }
57    }
58}
59
60/// The status a bare `PROP_LAST_STATUS` reply reports, if that is what
61/// this frame is.
62pub fn status_of(reply: &[u8]) -> Option<Status> {
63    let parsed = Frame::parse(reply).ok()?;
64    if parsed.command() != Some(Cmd::PropIs) {
65        return None;
66    }
67    let payload = PropPayload::parse(parsed.payload).ok()?;
68    if payload.key != prop::LAST_STATUS {
69        return None;
70    }
71    Some(Status(pui::decode(payload.value).ok()?.0))
72}
73
74/// Read a single-property reply against the property that was asked for.
75pub fn property(requested: u32, reply: &[u8]) -> Result<Answer<'_>, ParseError> {
76    let parsed = Frame::parse(reply)?;
77    let payload = PropPayload::parse(parsed.payload)?;
78    Ok(Answer::read(requested, payload.key, payload.value))
79}
80
81/// Why a multi-property reply could not be read as one.
82#[derive(Clone, Copy, Debug, PartialEq, Eq)]
83pub enum EntriesError {
84    /// The reply is a well-formed frame, but not a `CMD_PROP_ARE`. A
85    /// device without `CAP_CMD_MULTI` declines the whole request this way,
86    /// answering with the status [`status_of`] reads.
87    NotEntries,
88    /// The reply is not a readable frame.
89    Unreadable(ParseError),
90}
91
92impl From<ParseError> for EntriesError {
93    fn from(error: ParseError) -> Self {
94        Self::Unreadable(error)
95    }
96}
97
98/// Split a `CMD_PROP_ARE` into per-position answers, paired with the keys
99/// they were asked for.
100///
101/// A device may answer fewer positions than were asked for — it stops
102/// before a reply overflows rather than truncating one — so the iterator
103/// simply ends, and the caller reissues whatever is left over.
104pub fn entries<'a>(
105    requested: &'a [u32],
106    reply: &'a [u8],
107) -> Result<impl Iterator<Item = Result<(u32, Answer<'a>), ParseError>> + 'a, EntriesError> {
108    let parsed = Frame::parse(reply)?;
109    if parsed.command() != Some(Cmd::PropAre) {
110        return Err(EntriesError::NotEntries);
111    }
112    Ok(MultiEntries::new(parsed.payload)
113        .enumerate()
114        .map(move |(position, entry)| {
115            let entry = entry?;
116            // Past the end of what was asked for, the entry names itself.
117            let key = requested.get(position).copied().unwrap_or(entry.key);
118            Ok((key, Answer::read(key, entry.key, entry.value)))
119        }))
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use crate::frame;
126
127    fn is(key: u32, value: &[u8]) -> Vec<u8> {
128        let mut buf = [0u8; 128];
129        let len = frame::prop_is(&mut buf, 3, key, value).unwrap();
130        buf[..len].to_vec()
131    }
132
133    #[test]
134    fn a_status_in_place_of_a_value_reads_as_a_refusal() {
135        let reply = is(prop::LAST_STATUS, &[Status::PROP_NOT_FOUND.0 as u8]);
136        assert_eq!(
137            property(prop::DEV_NAME, &reply).unwrap(),
138            Answer::Refused(Status::PROP_NOT_FOUND)
139        );
140        // Asked for outright, the status is the value.
141        assert_eq!(
142            property(prop::LAST_STATUS, &reply).unwrap(),
143            Answer::Value(&[Status::PROP_NOT_FOUND.0 as u8])
144        );
145        assert_eq!(status_of(&reply), Some(Status::PROP_NOT_FOUND));
146    }
147
148    #[test]
149    fn each_position_is_read_against_the_key_it_answers() {
150        let mut buf = [0u8; 128];
151        let mut writer = frame::prop_are(&mut buf, 3).unwrap();
152        writer.write_entry(prop::DEV_NAME, b"Ridge").unwrap();
153        writer.write_status_entry(Status::PROP_NOT_FOUND).unwrap();
154        let len = writer.finish();
155        let reply = &buf[..len];
156
157        let requested = [prop::DEV_NAME, prop::HOST_KEY];
158        let read: Vec<_> = entries(&requested, reply)
159            .unwrap()
160            .map(Result::unwrap)
161            .collect();
162        assert_eq!(
163            read,
164            vec![
165                (prop::DEV_NAME, Answer::Value(b"Ridge".as_slice())),
166                (prop::HOST_KEY, Answer::Refused(Status::PROP_NOT_FOUND)),
167            ]
168        );
169    }
170
171    #[test]
172    fn a_device_that_declined_the_command_is_not_an_entry_list() {
173        let reply = is(prop::LAST_STATUS, &[Status::UNIMPLEMENTED.0 as u8]);
174        assert!(entries(&[prop::DEV_NAME], &reply).is_err());
175        assert_eq!(status_of(&reply), Some(Status::UNIMPLEMENTED));
176    }
177}