umsh_ulcp_device/
known_networks.rs

1//! Bounded, secret-bearing station configuration. Never format credentials.
2use heapless::Vec;
3use umsh_ulcp::{Status, items, wifi};
4
5pub const MAX_NETWORKS: usize = 4;
6
7#[derive(Clone, PartialEq, Eq)]
8pub struct KnownNetworks {
9    entries: Vec<Vec<u8, { wifi::NETWORK_ENTRY_MAX_LEN }>, MAX_NETWORKS>,
10}
11
12impl Default for KnownNetworks {
13    fn default() -> Self {
14        Self {
15            entries: Vec::new(),
16        }
17    }
18}
19
20impl KnownNetworks {
21    pub fn entries(&self) -> impl Iterator<Item = &[u8]> {
22        self.entries.iter().map(|entry| entry.as_slice())
23    }
24
25    pub fn get(&self, ssid: &[u8]) -> Option<wifi::NetworkEntry<'_>> {
26        self.entries()
27            .filter_map(|bytes| wifi::NetworkEntry::decode(bytes).ok())
28            .find(|entry| entry.ssid == ssid)
29    }
30
31    pub fn insert(&mut self, bytes: &[u8]) -> Result<(), Status> {
32        let entry = wifi::NetworkEntry::decode(bytes).map_err(|_| Status::INVALID_ARGUMENT)?;
33        entry.validate().map_err(|_| Status::INVALID_ARGUMENT)?;
34        let index = self.entries.iter().position(|stored| {
35            wifi::NetworkEntry::decode(stored).is_ok_and(|old| old.ssid == entry.ssid)
36        });
37        let next = Vec::from_slice(bytes).map_err(|_| Status::NOMEM)?;
38        if let Some(index) = index {
39            self.entries[index].fill(0);
40            self.entries[index] = next;
41        } else {
42            self.entries.push(next).map_err(|_| Status::NOMEM)?;
43        }
44        Ok(())
45    }
46
47    pub fn remove(&mut self, ssid: &[u8]) -> Result<(), Status> {
48        let index = self
49            .entries
50            .iter()
51            .position(|stored| wifi::NetworkEntry::decode(stored).is_ok_and(|old| old.ssid == ssid))
52            .ok_or(Status::ITEM_NOT_FOUND)?;
53        self.entries[index].fill(0);
54        self.entries.remove(index);
55        Ok(())
56    }
57
58    /// Build the complete candidate before committing any mutation.
59    pub fn replacement(
60        bytes: &[u8],
61        validate: impl Fn(&wifi::NetworkEntry<'_>) -> Result<(), Status>,
62    ) -> Result<Self, Status> {
63        let mut candidate = Self::default();
64        for item in items::prefixed_items(bytes) {
65            let item = item.map_err(|_| Status::INVALID_ARGUMENT)?;
66            let entry = wifi::NetworkEntry::decode(item).map_err(|_| Status::INVALID_ARGUMENT)?;
67            entry.validate().map_err(|_| Status::INVALID_ARGUMENT)?;
68            if candidate.get(entry.ssid).is_some() {
69                return Err(Status::INVALID_ARGUMENT);
70            }
71            validate(&entry)?;
72            candidate.insert(item)?;
73        }
74        Ok(candidate)
75    }
76
77    pub fn encode_reported(&self, out: &mut [u8]) -> Result<usize, Status> {
78        let mut len = 0;
79        for bytes in self.entries() {
80            let entry = wifi::NetworkEntry::decode(bytes).map_err(|_| Status::INTERNAL_ERROR)?;
81            let mut item = [0; 3 + wifi::SSID_MAX_LEN];
82            let n = entry
83                .encode_reported(&mut item)
84                .map_err(|_| Status::INTERNAL_ERROR)?;
85            len +=
86                items::encode_prefixed_item(&item[..n], out.get_mut(len..).ok_or(Status::NOMEM)?)
87                    .map_err(|_| Status::NOMEM)?;
88        }
89        Ok(len)
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    fn entry(ssid: &[u8], password: &[u8]) -> std::vec::Vec<u8> {
98        let mut bytes = std::vec![0, 2, ssid.len() as u8];
99        bytes.extend_from_slice(ssid);
100        bytes.extend_from_slice(password);
101        bytes
102    }
103
104    #[test]
105    fn replacement_rejects_duplicates_and_reads_redact_secrets() {
106        let item = entry(b"office", b"password");
107        let mut bytes = std::vec![item.len() as u8];
108        bytes.extend_from_slice(&item);
109        let table = KnownNetworks::replacement(&bytes, |_| Ok(())).unwrap();
110        let mut out = [0; 144];
111        let len = table.encode_reported(&mut out).unwrap();
112        assert_eq!(&out[..len], b"\x09\x00\x02\x06office");
113        let mut duplicate = bytes.clone();
114        duplicate.extend_from_slice(&bytes);
115        assert!(matches!(
116            KnownNetworks::replacement(&duplicate, |_| Ok(())),
117            Err(Status::INVALID_ARGUMENT)
118        ));
119        assert_eq!(table.get(b"office").unwrap().credential, b"password");
120    }
121
122    #[test]
123    fn capacity_and_bytewise_names() {
124        let mut table = KnownNetworks::default();
125        for n in 0..4 {
126            table.insert(&entry(&[0xff, n], b"password")).unwrap();
127        }
128        assert_eq!(
129            table.insert(&entry(b"fifth", b"password")),
130            Err(Status::NOMEM)
131        );
132        table.insert(&entry(&[0xff, 0], b"replaced")).unwrap();
133        assert_eq!(table.get(&[0xff, 0]).unwrap().credential, b"replaced");
134        table.remove(&[0xff, 0]).unwrap();
135        assert!(table.get(&[0xff, 0]).is_none());
136    }
137}