#[cfg(not(feature = "std"))]
#[allow(unused_imports)]
use alloc::{string::String, vec, vec::Vec};
use crate::ParseError;
#[non_exhaustive]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum WifiSecurity {
Wpa,
Wep,
None,
}
impl WifiSecurity {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::Wpa => "WPA",
Self::Wep => "WEP",
Self::None => "nopass",
}
}
fn from_wire(value: &str) -> Self {
match value.to_ascii_uppercase().as_str() {
"WPA" | "WPA2" | "WPA3" => Self::Wpa,
"WEP" => Self::Wep,
_ => Self::None, }
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WifiConfig {
ssid: String,
password: Option<String>,
security: WifiSecurity,
hidden: bool,
}
impl WifiConfig {
pub fn parse(s: &str) -> Result<Self, ParseError> {
let rest = strip_wifi_prefix(s).ok_or(ParseError::InvalidFormat)?;
let mut ssid: Option<String> = None;
let mut password = None;
let mut security = WifiSecurity::None;
let mut hidden = false;
for field in split_fields(rest) {
if field.is_empty() {
continue;
}
let Some((key, value)) = field.split_once(':') else {
continue; };
match key {
"S" => ssid = Some(unescape(value)),
"T" => security = WifiSecurity::from_wire(value),
"P" => {
let pw = unescape(value);
password = if pw.is_empty() { None } else { Some(pw) };
}
"H" => hidden = value.eq_ignore_ascii_case("true"),
_ => {}
}
}
let ssid = ssid.ok_or(ParseError::MissingField("S (ssid)"))?;
Ok(Self { ssid, password, security, hidden })
}
#[must_use]
pub fn ssid(&self) -> &str {
&self.ssid
}
#[must_use]
pub fn password(&self) -> Option<&str> {
self.password.as_deref()
}
#[must_use]
pub fn security(&self) -> WifiSecurity {
self.security
}
#[must_use]
pub fn hidden(&self) -> bool {
self.hidden
}
}
pub fn encode_wifi(ssid: &str, password: &str, auth: &str) -> String {
let mut payload = String::from("WIFI:T:");
payload.push_str(auth);
payload.push_str(";S:");
push_escaped(&mut payload, ssid);
payload.push_str(";P:");
push_escaped(&mut payload, password);
payload.push_str(";;");
payload
}
fn strip_wifi_prefix(s: &str) -> Option<&str> {
const PREFIX: &[u8] = b"WIFI:";
let bytes = s.as_bytes();
if bytes.len() >= PREFIX.len() && bytes[..PREFIX.len()].eq_ignore_ascii_case(PREFIX) {
Some(&s[PREFIX.len()..])
} else {
None
}
}
fn split_fields(rest: &str) -> Vec<String> {
let mut fields = Vec::new();
let mut cur = String::new();
let mut chars = rest.chars();
while let Some(c) = chars.next() {
if c == '\\' {
cur.push('\\');
if let Some(next) = chars.next() {
cur.push(next);
}
} else if c == ';' {
fields.push(core::mem::take(&mut cur));
} else {
cur.push(c);
}
}
fields.push(cur);
fields
}
fn unescape(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars();
while let Some(c) = chars.next() {
if c == '\\' {
if let Some(next) = chars.next() {
out.push(next);
}
} else {
out.push(c);
}
}
out
}
fn push_escaped(out: &mut String, s: &str) {
for c in s.chars() {
if matches!(c, ';' | ',' | '"' | '\\' | ':') {
out.push('\\');
}
out.push(c);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trip_special_chars() {
let original = "a;b,c\"d\\e:f";
let payload = encode_wifi(original, "p\\a;ss", "WPA");
let cfg = WifiConfig::parse(&payload).unwrap();
assert_eq!(cfg.ssid(), original);
assert_eq!(cfg.password(), Some("p\\a;ss"));
assert_eq!(cfg.security(), WifiSecurity::Wpa);
assert!(!cfg.hidden());
}
#[test]
fn parse_accepts_any_field_order() {
let s = "WIFI:S:MyNet;T:WEP;P:secret;H:true;;";
let cfg = WifiConfig::parse(s).unwrap();
assert_eq!(cfg.ssid(), "MyNet");
assert_eq!(cfg.password(), Some("secret"));
assert_eq!(cfg.security(), WifiSecurity::Wep);
assert!(cfg.hidden());
}
#[test]
fn parse_unescapes_semicolon_in_ssid() {
let cfg = WifiConfig::parse("WIFI:T:WPA;S:My\\;Net;P:pw;;").unwrap();
assert_eq!(cfg.ssid(), "My;Net");
}
#[test]
fn parse_nopass_security() {
let cfg = WifiConfig::parse("WIFI:S:Open;T:nopass;;").unwrap();
assert_eq!(cfg.security(), WifiSecurity::None);
assert_eq!(cfg.password(), None);
}
#[test]
fn parse_missing_prefix_errors() {
assert_eq!(WifiConfig::parse("S:Net;T:WPA;;"), Err(ParseError::InvalidFormat));
}
#[test]
fn parse_missing_ssid_errors() {
assert_eq!(WifiConfig::parse("WIFI:T:WPA;P:pw;;"), Err(ParseError::MissingField("S (ssid)")));
}
#[test]
fn security_round_trips() {
for sec in [WifiSecurity::Wpa, WifiSecurity::Wep, WifiSecurity::None] {
let payload = encode_wifi("net", "", sec.as_str());
assert_eq!(WifiConfig::parse(&payload).unwrap().security(), sec);
}
}
#[test]
fn escaped_helper_preserves_behavior() {
let mut out = String::new();
push_escaped(&mut out, "a;b,c\"d\\e:f");
assert_eq!(out, "a\\;b\\,c\\\"d\\\\e\\:f");
}
}