umsh_ulcp_device/
known_networks.rs1use 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 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}