umsh_ulcp/
temperature.rs

1//! Ordered temperature arrays. Capacities belong to senders, not the wire codec.
2
3use crate::items::{encode_prefixed_item, fixed_items, prefixed_items};
4
5pub const UNKNOWN: u16 = 0xffff;
6pub const NAME_MAX_BYTES: usize = 64;
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum Error {
10    Malformed,
11    BufferTooSmall,
12}
13
14/// Validate the complete array before exposing any readings (in tenths of kelvin).
15pub fn readings(value: &[u8]) -> Result<impl ExactSizeIterator<Item = Option<u16>> + Clone, Error> {
16    Ok(fixed_items::<2>(value)
17        .map_err(|_| Error::Malformed)?
18        .map(|bytes| {
19            let value = u16::from_le_bytes(*bytes);
20            (value != UNKNOWN).then_some(value)
21        }))
22}
23
24/// Encode raw readings, including `UNKNOWN`, without a count prefix.
25pub fn encode_readings(values: &[u16], out: &mut [u8]) -> Result<usize, Error> {
26    let len = values.len().checked_mul(2).ok_or(Error::BufferTooSmall)?;
27    if out.len() < len {
28        return Err(Error::BufferTooSmall);
29    }
30    for (value, bytes) in values.iter().zip(out[..len].chunks_exact_mut(2)) {
31        bytes.copy_from_slice(&value.to_le_bytes());
32    }
33    Ok(len)
34}
35
36pub fn valid_name(name: &str) -> bool {
37    (1..=NAME_MAX_BYTES).contains(&name.len()) && !name.as_bytes().contains(&0)
38}
39
40/// Validate every prefix, UTF-8 string, length, and NUL constraint up front.
41pub fn names(value: &[u8]) -> Result<impl Iterator<Item = &str> + Clone, Error> {
42    for item in prefixed_items(value) {
43        let name = core::str::from_utf8(item.map_err(|_| Error::Malformed)?)
44            .map_err(|_| Error::Malformed)?;
45        if !valid_name(name) {
46            return Err(Error::Malformed);
47        }
48    }
49    Ok(prefixed_items(value).map(|item| {
50        core::str::from_utf8(item.expect("validated name prefix")).expect("validated UTF-8")
51    }))
52}
53
54pub fn encode_names(names: &[&str], out: &mut [u8]) -> Result<usize, Error> {
55    let mut len = 0usize;
56    for name in names {
57        if !valid_name(name) {
58            return Err(Error::Malformed);
59        }
60        // Lengths 1–64 have a one-byte PUI prefix.
61        len = len
62            .checked_add(1 + name.len())
63            .ok_or(Error::BufferTooSmall)?;
64    }
65    if len > out.len() {
66        return Err(Error::BufferTooSmall);
67    }
68    let mut offset = 0;
69    for name in names {
70        offset += encode_prefixed_item(name.as_bytes(), &mut out[offset..])
71            .map_err(|_| Error::BufferTooSmall)?;
72    }
73    Ok(offset)
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn temperature_readings_boundaries_and_order() {
82        let values = [2981, 0, 0xfffe, UNKNOWN];
83        let mut out = [0; 8];
84        assert_eq!(encode_readings(&values, &mut out), Ok(8));
85        assert_eq!(out, [0xa5, 0x0b, 0, 0, 0xfe, 0xff, 0xff, 0xff]);
86        assert_eq!(
87            readings(&out).unwrap().collect::<Vec<_>>(),
88            [Some(2981), Some(0), Some(65534), None]
89        );
90        assert_eq!(readings(&[]).unwrap().len(), 0);
91        assert!(readings(&[0]).is_err());
92        assert_eq!(
93            encode_readings(&values, &mut [0; 7]),
94            Err(Error::BufferTooSmall)
95        );
96        assert_eq!(encode_readings(&[], &mut []), Ok(0));
97    }
98
99    #[test]
100    fn temperature_names_validate_complete_array() {
101        let mut out = [0; 128];
102        let len = encode_names(&["Die", "電池", "Die"], &mut out).unwrap();
103        assert_eq!(
104            names(&out[..len]).unwrap().collect::<Vec<_>>(),
105            ["Die", "電池", "Die"]
106        );
107        assert_eq!(names(&[]).unwrap().count(), 0);
108        for bad in [
109            &[0][..],
110            &[1, 0],
111            &[1, 0xff],
112            &[2, b'a'],
113            &[0x80],
114            &[1, b'a', 0],
115        ] {
116            assert!(names(bad).is_err(), "{bad:?}");
117        }
118        assert_eq!(
119            encode_names(&["ab"], &mut [0; 2]),
120            Err(Error::BufferTooSmall)
121        );
122        for bad in ["", "a\0b", &"a".repeat(65)] {
123            assert_eq!(encode_names(&[bad], &mut out), Err(Error::Malformed));
124        }
125        assert!(encode_names(&[&"a".repeat(64)], &mut out).is_ok());
126        let too_long = [&[65][..], &[b'a'; 65]].concat();
127        assert!(names(&too_long).is_err());
128        // Reference inventory capacities do not limit host decoding.
129        assert_eq!(names(&b"\x01a".repeat(17)).unwrap().count(), 17);
130    }
131}