Skip to main content

hap_tlv8/
map.rs

1//! Convenience wrapper over parsed TLV8 items with typed getters.
2//!
3//! [`Tlv8Map`] owns the reassembled `(type, value)` items produced by
4//! [`crate::Tlv8Reader::parse`] and offers ergonomic lookup. [`Tlv8Map::get`]
5//! returns the raw value bytes of the *first* item with a matching type; the
6//! `get_uN` helpers decode fixed-width little-endian integers.
7//!
8//! Nested TLV8 values are returned as raw bytes by [`Tlv8Map::get`]; re-parse
9//! them with [`Tlv8Map::parse`].
10
11use crate::error::{Result, Tlv8Error};
12use crate::reader::Tlv8Reader;
13
14/// An owned, queryable view over reassembled TLV8 items.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct Tlv8Map {
17    items: Vec<(u8, Vec<u8>)>,
18}
19
20impl Tlv8Map {
21    /// Parse a TLV8 byte stream and wrap the reassembled items.
22    ///
23    /// # Errors
24    ///
25    /// Propagates any [`Tlv8Error`] from [`Tlv8Reader::parse`].
26    pub fn parse(bytes: &[u8]) -> Result<Self> {
27        Ok(Self::from_items(Tlv8Reader::parse(bytes)?))
28    }
29
30    /// Wrap an already-parsed item list (no validation).
31    pub fn from_items(items: Vec<(u8, Vec<u8>)>) -> Self {
32        Self { items }
33    }
34
35    /// Borrow the underlying items in stream order.
36    pub fn items(&self) -> &[(u8, Vec<u8>)] {
37        &self.items
38    }
39
40    /// Consume the map and return the underlying items.
41    pub fn into_items(self) -> Vec<(u8, Vec<u8>)> {
42        self.items
43    }
44
45    /// Value bytes of the first item with type `ty`, if present.
46    pub fn get(&self, ty: u8) -> Option<&[u8]> {
47        self.items
48            .iter()
49            .find(|(t, _)| *t == ty)
50            .map(|(_, v)| v.as_slice())
51    }
52
53    fn get_uint(&self, ty: u8, width: usize) -> Result<Option<u64>> {
54        let Some(bytes) = self.get(ty) else {
55            return Ok(None);
56        };
57        if bytes.len() > width {
58            return Err(Tlv8Error::IntegerTooLarge {
59                requested: width,
60                actual: bytes.len(),
61            });
62        }
63        let mut buf = [0u8; 8];
64        buf[..bytes.len()].copy_from_slice(bytes);
65        Ok(Some(u64::from_le_bytes(buf)))
66    }
67
68    /// Decode the first item of type `ty` as a `u8`.
69    ///
70    /// # Errors
71    ///
72    /// [`Tlv8Error::IntegerTooLarge`] if the stored value has more than 1 byte.
73    pub fn get_u8(&self, ty: u8) -> Result<Option<u8>> {
74        // value is masked to a single byte by the width=1 length check above.
75        #[allow(clippy::cast_possible_truncation)]
76        Ok(self.get_uint(ty, 1)?.map(|v| v as u8))
77    }
78
79    /// Decode the first item of type `ty` as a little-endian `u16`.
80    ///
81    /// # Errors
82    ///
83    /// [`Tlv8Error::IntegerTooLarge`] if the stored value has more than 2 bytes.
84    pub fn get_u16(&self, ty: u8) -> Result<Option<u16>> {
85        // high bytes are guaranteed zero by the width=2 length check above.
86        #[allow(clippy::cast_possible_truncation)]
87        Ok(self.get_uint(ty, 2)?.map(|v| v as u16))
88    }
89
90    /// Decode the first item of type `ty` as a little-endian `u32`.
91    ///
92    /// # Errors
93    ///
94    /// [`Tlv8Error::IntegerTooLarge`] if the stored value has more than 4 bytes.
95    pub fn get_u32(&self, ty: u8) -> Result<Option<u32>> {
96        // high bytes are guaranteed zero by the width=4 length check above.
97        #[allow(clippy::cast_possible_truncation)]
98        Ok(self.get_uint(ty, 4)?.map(|v| v as u32))
99    }
100
101    /// Decode the first item of type `ty` as a little-endian `u64`.
102    ///
103    /// # Errors
104    ///
105    /// [`Tlv8Error::IntegerTooLarge`] if the stored value has more than 8 bytes.
106    pub fn get_u64(&self, ty: u8) -> Result<Option<u64>> {
107        self.get_uint(ty, 8)
108    }
109}
110
111#[cfg(test)]
112// CLAUDE.md test-code carve-out: unwrap/expect allowed with documented reason.
113#[allow(clippy::unwrap_used, clippy::expect_used)]
114mod tests {
115    use super::*;
116    use crate::Tlv8Error;
117
118    #[test]
119    fn get_returns_first_matching_value() {
120        let map = Tlv8Map::parse(&[0x01, 0x02, 0xAB, 0xCD, 0x02, 0x01, 0xEE]).unwrap();
121        assert_eq!(map.get(0x01), Some(&[0xAB, 0xCD][..]));
122        assert_eq!(map.get(0x02), Some(&[0xEE][..]));
123        assert_eq!(map.get(0x09), None);
124    }
125
126    #[test]
127    fn get_u8_decodes_single_byte() {
128        let map = Tlv8Map::parse(&[0x02, 0x01, 0x2A]).unwrap();
129        assert_eq!(map.get_u8(0x02).unwrap(), Some(0x2A));
130        assert_eq!(map.get_u8(0x09).unwrap(), None);
131    }
132
133    #[test]
134    fn get_u16_decodes_le() {
135        let map = Tlv8Map::parse(&[0x03, 0x02, 0x34, 0x12]).unwrap();
136        assert_eq!(map.get_u16(0x03).unwrap(), Some(0x1234));
137    }
138
139    #[test]
140    fn get_u32_decodes_le() {
141        let map = Tlv8Map::parse(&[0x04, 0x04, 0xBE, 0xBA, 0xFE, 0xCA]).unwrap();
142        assert_eq!(map.get_u32(0x04).unwrap(), Some(0xCAFE_BABE));
143    }
144
145    #[test]
146    fn get_u64_decodes_le() {
147        let bytes = [0x05, 0x08, 0xEF, 0xCD, 0xAB, 0x89, 0x67, 0x45, 0x23, 0x01];
148        let map = Tlv8Map::parse(&bytes).unwrap();
149        assert_eq!(map.get_u64(0x05).unwrap(), Some(0x0123_4567_89AB_CDEF));
150    }
151
152    #[test]
153    fn get_uint_accepts_shorter_than_width() {
154        // a 1-byte value read as u32 zero-extends.
155        let map = Tlv8Map::parse(&[0x04, 0x01, 0x07]).unwrap();
156        assert_eq!(map.get_u32(0x04).unwrap(), Some(7));
157    }
158
159    #[test]
160    fn get_u16_rejects_oversized_value() {
161        let map = Tlv8Map::parse(&[0x03, 0x03, 0x01, 0x02, 0x03]).unwrap();
162        assert_eq!(
163            map.get_u16(0x03).unwrap_err(),
164            Tlv8Error::IntegerTooLarge {
165                requested: 2,
166                actual: 3
167            }
168        );
169    }
170
171    #[test]
172    fn from_items_and_into_items_round_trip() {
173        let items = vec![(0x01_u8, vec![0xAA]), (0x02, vec![0xBB, 0xCC])];
174        let map = Tlv8Map::from_items(items.clone());
175        assert_eq!(map.items(), &items[..]);
176        assert_eq!(map.into_items(), items);
177    }
178}