1#[cfg(not(feature = "std"))]
10#[allow(unused_imports)]
11use alloc::{string::String, vec, vec::Vec};
12
13use crate::ParseError;
14
15#[non_exhaustive]
17#[derive(Debug, Copy, Clone, PartialEq, Eq)]
18pub enum WifiSecurity {
19 Wpa,
21 Wep,
23 None,
25}
26
27impl WifiSecurity {
28 #[must_use]
30 pub fn as_str(&self) -> &'static str {
31 match self {
32 Self::Wpa => "WPA",
33 Self::Wep => "WEP",
34 Self::None => "nopass",
35 }
36 }
37
38 fn from_wire(value: &str) -> Self {
39 match value.to_ascii_uppercase().as_str() {
40 "WPA" | "WPA2" | "WPA3" => Self::Wpa,
41 "WEP" => Self::Wep,
42 _ => Self::None, }
44 }
45}
46
47#[non_exhaustive]
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct WifiConfig {
54 ssid: String,
56 password: Option<String>,
58 security: WifiSecurity,
60 hidden: bool,
62}
63
64impl WifiConfig {
65 pub fn parse(s: &str) -> Result<Self, ParseError> {
75 let rest = strip_wifi_prefix(s).ok_or(ParseError::InvalidFormat)?;
76
77 let mut ssid: Option<String> = None;
78 let mut password = None;
79 let mut security = WifiSecurity::None;
80 let mut hidden = false;
81
82 for field in split_fields(rest) {
83 if field.is_empty() {
84 continue;
85 }
86 let Some((key, value)) = field.split_once(':') else {
87 continue; };
89 match key {
90 "S" => ssid = Some(unescape(value)),
91 "T" => security = WifiSecurity::from_wire(value),
92 "P" => {
93 let pw = unescape(value);
94 password = if pw.is_empty() { None } else { Some(pw) };
95 }
96 "H" => hidden = value.eq_ignore_ascii_case("true"),
97 _ => {}
98 }
99 }
100
101 let ssid = ssid.ok_or(ParseError::MissingField("S (ssid)"))?;
102 Ok(Self { ssid, password, security, hidden })
103 }
104
105 #[must_use]
107 pub fn ssid(&self) -> &str {
108 &self.ssid
109 }
110
111 #[must_use]
113 pub fn password(&self) -> Option<&str> {
114 self.password.as_deref()
115 }
116
117 #[must_use]
119 pub fn security(&self) -> WifiSecurity {
120 self.security
121 }
122
123 #[must_use]
125 pub fn hidden(&self) -> bool {
126 self.hidden
127 }
128}
129
130pub fn encode_wifi(ssid: &str, password: &str, auth: &str) -> String {
136 let mut payload = String::from("WIFI:T:");
137 payload.push_str(auth);
138 payload.push_str(";S:");
139 push_escaped(&mut payload, ssid);
140 payload.push_str(";P:");
141 push_escaped(&mut payload, password);
142 payload.push_str(";;");
143 payload
144}
145
146fn strip_wifi_prefix(s: &str) -> Option<&str> {
148 const PREFIX: &[u8] = b"WIFI:";
149 let bytes = s.as_bytes();
150 if bytes.len() >= PREFIX.len() && bytes[..PREFIX.len()].eq_ignore_ascii_case(PREFIX) {
151 Some(&s[PREFIX.len()..])
153 } else {
154 None
155 }
156}
157
158fn split_fields(rest: &str) -> Vec<String> {
161 let mut fields = Vec::new();
162 let mut cur = String::new();
163 let mut chars = rest.chars();
164 while let Some(c) = chars.next() {
165 if c == '\\' {
166 cur.push('\\');
167 if let Some(next) = chars.next() {
168 cur.push(next);
169 }
170 } else if c == ';' {
171 fields.push(core::mem::take(&mut cur));
172 } else {
173 cur.push(c);
174 }
175 }
176 fields.push(cur);
177 fields
178}
179
180fn unescape(s: &str) -> String {
182 let mut out = String::with_capacity(s.len());
183 let mut chars = s.chars();
184 while let Some(c) = chars.next() {
185 if c == '\\' {
186 if let Some(next) = chars.next() {
187 out.push(next);
188 }
189 } else {
190 out.push(c);
191 }
192 }
193 out
194}
195
196fn push_escaped(out: &mut String, s: &str) {
198 for c in s.chars() {
199 if matches!(c, ';' | ',' | '"' | '\\' | ':') {
200 out.push('\\');
201 }
202 out.push(c);
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn round_trip_special_chars() {
212 let original = "a;b,c\"d\\e:f";
214 let payload = encode_wifi(original, "p\\a;ss", "WPA");
215 let cfg = WifiConfig::parse(&payload).unwrap();
216 assert_eq!(cfg.ssid(), original);
217 assert_eq!(cfg.password(), Some("p\\a;ss"));
218 assert_eq!(cfg.security(), WifiSecurity::Wpa);
219 assert!(!cfg.hidden());
220 }
221
222 #[test]
223 fn parse_accepts_any_field_order() {
224 let s = "WIFI:S:MyNet;T:WEP;P:secret;H:true;;";
226 let cfg = WifiConfig::parse(s).unwrap();
227 assert_eq!(cfg.ssid(), "MyNet");
228 assert_eq!(cfg.password(), Some("secret"));
229 assert_eq!(cfg.security(), WifiSecurity::Wep);
230 assert!(cfg.hidden());
231 }
232
233 #[test]
234 fn parse_unescapes_semicolon_in_ssid() {
235 let cfg = WifiConfig::parse("WIFI:T:WPA;S:My\\;Net;P:pw;;").unwrap();
236 assert_eq!(cfg.ssid(), "My;Net");
237 }
238
239 #[test]
240 fn parse_nopass_security() {
241 let cfg = WifiConfig::parse("WIFI:S:Open;T:nopass;;").unwrap();
242 assert_eq!(cfg.security(), WifiSecurity::None);
243 assert_eq!(cfg.password(), None);
244 }
245
246 #[test]
247 fn parse_missing_prefix_errors() {
248 assert_eq!(WifiConfig::parse("S:Net;T:WPA;;"), Err(ParseError::InvalidFormat));
249 }
250
251 #[test]
252 fn parse_missing_ssid_errors() {
253 assert_eq!(WifiConfig::parse("WIFI:T:WPA;P:pw;;"), Err(ParseError::MissingField("S (ssid)")));
254 }
255
256 #[test]
257 fn security_round_trips() {
258 for sec in [WifiSecurity::Wpa, WifiSecurity::Wep, WifiSecurity::None] {
259 let payload = encode_wifi("net", "", sec.as_str());
260 assert_eq!(WifiConfig::parse(&payload).unwrap().security(), sec);
261 }
262 }
263
264 #[test]
265 fn escaped_helper_preserves_behavior() {
266 let mut out = String::new();
268 push_escaped(&mut out, "a;b,c\"d\\e:f");
269 assert_eq!(out, "a\\;b\\,c\\\"d\\\\e\\:f");
270 }
271}