use serde::Deserialize;
pub const DEFAULT_AP_COUNTRY: &str = "US";
#[derive(Debug, Default, Deserialize)]
struct RawRoot {
#[serde(default)]
network: RawNetwork,
}
#[derive(Debug, Default, Deserialize)]
struct RawNetwork {
#[serde(default)]
regulatory: RawRegulatory,
}
#[derive(Debug, Default, Deserialize)]
struct RawRegulatory {
#[serde(default)]
mode: Option<String>,
#[serde(default)]
region: Option<String>,
}
fn is_usable_country(s: &str) -> bool {
s.len() == 2 && s.chars().all(|c| c.is_ascii_alphabetic())
}
pub fn from_yaml(text: &str) -> String {
let raw: RawRoot = serde_norway::from_str(text).unwrap_or_default();
let pinned = raw
.network
.regulatory
.mode
.as_deref()
.is_some_and(|m| m.trim().eq_ignore_ascii_case("region"));
if !pinned {
return DEFAULT_AP_COUNTRY.to_string();
}
raw.network
.regulatory
.region
.as_deref()
.map(str::trim)
.map(str::to_ascii_uppercase)
.filter(|r| is_usable_country(r))
.unwrap_or_else(|| DEFAULT_AP_COUNTRY.to_string())
}
pub fn load_from(path: &std::path::Path) -> String {
match std::fs::read_to_string(path) {
Ok(text) => from_yaml(&text),
Err(_) => DEFAULT_AP_COUNTRY.to_string(),
}
}
pub fn configured_hotspot_password() -> String {
configured_hotspot_password_from(&crate::aux_ports::config_path())
}
pub fn configured_hotspot_password_from(path: &std::path::Path) -> String {
let Ok(text) = std::fs::read_to_string(path) else {
return String::new();
};
configured_hotspot_password_from_yaml(&text)
}
pub fn configured_hotspot_password_from_yaml(text: &str) -> String {
#[derive(Debug, Default, serde::Deserialize)]
struct Root {
#[serde(default)]
network: Net,
}
#[derive(Debug, Default, serde::Deserialize)]
struct Net {
#[serde(default)]
hotspot: Hotspot,
}
#[derive(Debug, Default, serde::Deserialize)]
struct Hotspot {
#[serde(default)]
password: Option<String>,
}
let raw: Root = serde_norway::from_str(text).unwrap_or_default();
raw.network
.hotspot
.password
.map(|p| p.trim().to_string())
.unwrap_or_default()
}
pub fn configured_ap_interface() -> String {
configured_ap_interface_from(&crate::aux_ports::config_path())
}
pub fn configured_ap_interface_from(path: &std::path::Path) -> String {
let Ok(text) = std::fs::read_to_string(path) else {
return String::new();
};
configured_ap_interface_from_yaml(&text)
}
pub fn configured_ap_interface_from_yaml(text: &str) -> String {
#[derive(Debug, Default, serde::Deserialize)]
struct Root {
#[serde(default)]
network: Net,
}
#[derive(Debug, Default, serde::Deserialize)]
struct Net {
#[serde(default)]
hotspot: Hotspot,
}
#[derive(Debug, Default, serde::Deserialize)]
struct Hotspot {
#[serde(default)]
interface: Option<String>,
}
let raw: Root = serde_norway::from_str(text).unwrap_or_default();
raw.network
.hotspot
.interface
.map(|p| p.trim().to_string())
.unwrap_or_default()
}
pub fn load() -> String {
load_from(&crate::aux_ports::config_path())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_unpinned_unit_matches_the_radios_own_default() {
assert_eq!(from_yaml(""), "US");
assert_eq!(
from_yaml("network:\n regulatory:\n mode: unrestricted\n"),
"US"
);
}
#[test]
fn a_pinned_region_is_honoured() {
assert_eq!(
from_yaml("network:\n regulatory:\n mode: region\n region: IN\n"),
"IN"
);
assert_eq!(
from_yaml("network:\n regulatory:\n mode: region\n region: de\n"),
"DE",
"an operator's lowercase entry is still a pin"
);
}
#[test]
fn a_region_without_the_mode_is_not_a_pin() {
assert_eq!(from_yaml("network:\n regulatory:\n region: IN\n"), "US");
}
#[test]
fn a_malformed_region_falls_back_rather_than_reaching_hostapd() {
for bad in ["", "U", "USA", "00", "1N", "!!"] {
let text = format!("network:\n regulatory:\n mode: region\n region: {bad}\n");
assert_eq!(
from_yaml(&text),
"US",
"region {bad:?} must not reach hostapd"
);
}
}
#[test]
fn malformed_yaml_falls_back_rather_than_panicking() {
assert_eq!(from_yaml("network: [not a map"), "US");
}
#[test]
fn a_missing_file_yields_the_default() {
assert_eq!(
load_from(std::path::Path::new("/nonexistent/ados/config.yaml")),
"US"
);
}
}