use std::path::{Path, PathBuf};
use std::sync::Arc;
use kernel::{AxCode, AxError, TimeMs};
use serde::{Deserialize, Serialize};
#[derive(Debug, Default, Serialize, Deserialize)]
struct Table {
#[serde(default, rename = "browser")]
browsers: Vec<Row>,
}
#[derive(Debug, Serialize, Deserialize)]
struct Row {
id: String,
label: String,
public_key: String,
paired_at: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
last_seen: Option<u64>,
}
pub(crate) fn load(path: &Path) -> Result<Vec<wire::PairedBrowser>, AxError> {
let text = match std::fs::read_to_string(path) {
Ok(text) => text,
Err(source) if source.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(source) => return Err(unreadable(path, &source.to_string())),
};
let table: Table = toml::from_str(&text).map_err(|err| unreadable(path, &err.to_string()))?;
table
.browsers
.into_iter()
.map(|row| {
let key = wire::DeviceKey::parse(&row.public_key)
.ok_or_else(|| unreadable(path, &format!("browser {} holds no key", row.id)))?;
Ok(wire::PairedBrowser {
id: key.id(),
label: row.label,
key,
paired_at: TimeMs::new(row.paired_at),
last_seen: row.last_seen.map(TimeMs::new),
})
})
.collect()
}
pub(crate) fn keeping(path: PathBuf) -> wire::KeepBrowsers {
Arc::new(move |browsers: &[wire::PairedBrowser]| {
let table = Table {
browsers: browsers
.iter()
.map(|browser| Row {
id: browser.id.as_str().to_owned(),
label: browser.label.clone(),
public_key: browser.key.hex(),
paired_at: browser.paired_at.value(),
last_seen: browser.last_seen.map(|seen| seen.value()),
})
.collect(),
};
let text = toml::to_string(&table).map_err(|err| unwritten(&path, &err.to_string()))?;
let staged = path.with_extension("toml.staged");
if let Some(dir) = path.parent() {
std::fs::create_dir_all(dir).map_err(|err| unwritten(dir, &err.to_string()))?;
}
std::fs::write(&staged, text).map_err(|err| unwritten(&staged, &err.to_string()))?;
std::fs::rename(&staged, &path).map_err(|err| unwritten(&path, &err.to_string()))
})
}
fn unreadable(path: &Path, why: &str) -> AxError {
AxError::failure(
AxCode::StorageFatal,
"read the paired browsers",
format!("{}: {why}", path.display()),
)
.with_recovery("move the file aside to pair every browser again; it holds public keys only")
}
fn unwritten(path: &Path, why: &str) -> AxError {
AxError::failure(
AxCode::StorageFatal,
"keep the paired browsers",
format!("{}: {why}", path.display()),
)
.with_recovery("check the city directory is writable, then pair again")
}