use std::cell::Cell;
use std::collections::HashMap;
use std::rc::Rc;
use crabshell_reactive::Signal;
use serde_json::Value as Json;
use zbus::{Connection, Message, OwnedObjectPath, Variant};
use super::{NM, by_number};
use crate::Shared;
use crate::dbus::{self, CALL_TIMEOUT, Daemon, Props, set_changed};
pub(super) const SETTINGS: &str = "org.freedesktop.NetworkManager.Settings";
pub(super) const SETTINGS_PATH: &str = "/org/freedesktop/NetworkManager/Settings";
pub(super) const SAVED: &str = "org.freedesktop.NetworkManager.Settings.Connection";
const MAX_SAVED: usize = 256;
const MAX_REPLY: usize = 64 * 1024;
const MAX_KEPT: usize = 1024 * 1024;
#[derive(Clone, Debug, PartialEq)]
pub struct SavedConnection {
pub uuid: String,
pub id: String,
pub kind: String,
pub settings: Json,
}
impl SavedConnection {
fn named(settings: &HashMap<String, Props>) -> SavedConnection {
let about = settings.get("connection");
let text = |name| about.map_or(String::new(), |props| dbus::text(props, name));
SavedConnection {
uuid: text("uuid"),
id: text("id"),
kind: text("type"),
settings: Json::Null,
}
}
fn weight(&self) -> usize {
size_of::<SavedConnection>() + self.uuid.len() + self.id.len() + self.kind.len()
}
}
fn settings_json(settings: &HashMap<String, Props>, cost: &mut usize) -> Json {
let mut groups = serde_json::Map::new();
for (group, props) in settings {
let mut entries = serde_json::Map::new();
for (name, value) in props {
*cost += size_of::<String>() + name.len();
entries.insert(name.clone(), json(value, cost));
}
*cost += size_of::<String>() + group.len() + size_of::<Json>();
groups.insert(group.clone(), Json::Object(entries));
}
*cost += size_of::<Json>();
Json::Object(groups)
}
pub(super) fn json(value: &Variant<'_>, cost: &mut usize) -> Json {
let node = match value {
Variant::Variant(inner) => return json(inner, cost),
Variant::U8(n) => Json::from(*n),
Variant::Bool(b) => Json::from(*b),
Variant::I16(n) => Json::from(*n),
Variant::U16(n) => Json::from(*n),
Variant::I32(n) => Json::from(*n),
Variant::U32(n) => Json::from(*n),
Variant::I64(n) => Json::from(*n),
Variant::U64(n) => Json::from(*n),
Variant::F64(n) => Json::from(*n),
Variant::Str(text) => text_json(text.as_str().to_owned(), cost),
Variant::Signature(signature) => text_json(signature.to_string(), cost),
Variant::ObjectPath(path) => text_json(path.as_str().to_owned(), cost),
Variant::Array(array) => array.inner().iter().map(|item| json(item, cost)).collect(),
Variant::Structure(structure) => {
let fields = structure.fields().iter();
fields.map(|field| json(field, cost)).collect()
}
Variant::Dict(dict) => {
let mut entries = serde_json::Map::new();
for (key, value) in dict.iter() {
let key = match key {
Variant::Str(text) => text.as_str().to_owned(),
key => json(key, &mut 0).to_string(),
};
*cost += size_of::<String>() + key.len();
entries.insert(key, json(value, cost));
}
Json::Object(entries)
}
Variant::Fd(_) => Json::Null,
};
*cost += size_of::<Json>();
node
}
fn text_json(text: String, cost: &mut usize) -> Json {
*cost += text.len();
Json::String(text)
}
pub(super) struct SavedConnections {
shared: Rc<Shared>,
pub(super) list: Signal<Vec<SavedConnection>>,
followed: Cell<bool>,
generation: Cell<u64>,
}
impl SavedConnections {
pub(super) fn new(shared: Rc<Shared>) -> Rc<SavedConnections> {
let list = shared.root.signal(Vec::new());
Rc::new(SavedConnections {
shared,
list,
followed: Cell::new(false),
generation: Cell::new(0),
})
}
pub(super) fn follow(self: &Rc<Self>) {
if self.followed.replace(true) {
return;
}
if let Some(address) = self.shared.config.system_bus.clone() {
let follow = dbus::follow(Rc::clone(&self.shared), Rc::clone(self), address);
self.shared.spawn(follow);
}
}
}
impl Daemon for SavedConnections {
const NAME: &'static str = NM;
fn rules() -> Vec<String> {
vec![
format!("type='signal',sender='{NM}',path='{SETTINGS_PATH}',interface='{SETTINGS}'"),
format!("type='signal',sender='{NM}',interface='{SAVED}',member='Updated'"),
]
}
fn alive(&self) -> bool {
self.list.try_with(|_| ()).is_some()
}
fn clear(&self) -> u64 {
set_changed(self.list, Vec::new());
self.generation.set(self.generation.get() + 1);
self.generation.get()
}
async fn read(self: Rc<Self>, connection: Connection, generation: u64) {
let shared = &self.shared;
let call = connection.call_method(
Some(NM),
SETTINGS_PATH,
Some(SETTINGS),
"ListConnections",
&(),
);
let listed = async {
let reply = shared.timeout(CALL_TIMEOUT, call).await?;
reply.body().deserialize::<Vec<OwnedObjectPath>>()
};
let mut paths = match listed.await {
Ok(paths) => paths,
Err(error) => {
return tracing::warn!(%error, "couldn't list NetworkManager's saved connections");
}
};
paths.sort_by(|a, b| by_number(a.as_str(), b.as_str()));
let (mut list, mut room) = (Vec::new(), MAX_KEPT);
let (mut without_settings, mut left_out) = (0, 0);
for path in paths.iter().take(MAX_SAVED).map(|path| path.as_str()) {
let call = connection.call_method(Some(NM), path, Some(SAVED), "GetSettings", &());
let reply = match shared.timeout(CALL_TIMEOUT, call).await {
Ok(reply) => reply,
Err(error) => {
tracing::info!(%error, path, "couldn't read a saved connection");
continue;
}
};
let body = reply.body();
if body.len() > MAX_REPLY {
tracing::warn!(
path,
bytes = body.len(),
"a saved connection too big; left out"
);
continue;
}
let Ok(settings) = body.deserialize::<HashMap<String, Props>>() else {
tracing::warn!(
path,
"a saved connection's settings that don't parse; left out"
);
continue;
};
let mut saved = SavedConnection::named(&settings);
let Some(left) = room.checked_sub(saved.weight()) else {
left_out += 1;
continue;
};
room = left;
let mut cost = 0;
let json = settings_json(&settings, &mut cost);
match room.checked_sub(cost) {
Some(left) => (room, saved.settings) = (left, json),
None => without_settings += 1,
}
list.push(saved);
}
if without_settings + left_out > 0 {
let what = "saved connections past 1 MiB listed without settings, or left out";
tracing::warn!(without_settings, left_out, "{what}");
}
if self.generation.get() == generation {
set_changed(self.list, list);
}
}
fn signal(self: &Rc<Self>, _: &Connection, _: &Message) {}
fn reads_again(&self, _: &Message) -> bool {
true
}
}