use serde::Deserialize;
use std::collections::HashMap;
use std::path::Path;
#[derive(Debug, Clone, Default, Deserialize)]
pub struct Role {
#[serde(default)]
pub display: Option<String>,
#[serde(default)]
pub host: Option<String>,
#[serde(default)]
pub desc: Option<String>,
#[serde(default)]
pub aliases: Vec<String>,
#[serde(default)]
pub status: Option<String>,
#[serde(default)]
pub pubkey: Option<String>,
}
pub fn pubkey<'a>(roster: &'a Roster, id: &str) -> Option<&'a str> {
roster.get(id).and_then(|r| r.pubkey.as_deref())
}
pub(crate) fn classify_pubkey(map: &serde_yaml::Mapping) -> Result<Option<String>, String> {
match map.get("pubkey") {
None => Ok(None),
Some(v) if v.is_null() => Ok(None),
Some(v) => match v.as_str() {
Some(s) => {
let t = s.trim();
Ok((!t.is_empty()).then(|| t.to_string()))
}
None => Err(match v {
serde_yaml::Value::Sequence(_) => "list",
serde_yaml::Value::Mapping(_) => "mapping",
serde_yaml::Value::Number(_) => "number",
serde_yaml::Value::Bool(_) => "boolean",
_ => "non-string value",
}
.to_string()),
},
}
}
pub type Roster = HashMap<String, Role>;
pub fn load(root: &Path) -> Roster {
let mut m = Roster::new();
if let Ok(txt) = std::fs::read_to_string(root.join("roles.toml")) {
if let Ok(val) = txt.parse::<toml::Value>() {
if let Some(roles) = val.get("roles").and_then(toml::Value::as_table) {
for (id, cfg) in roles {
let s = |k: &str| cfg.get(k).and_then(toml::Value::as_str).map(String::from);
m.insert(
id.clone(),
Role {
display: s("display"),
host: s("host"),
desc: s("desc"),
aliases: Vec::new(),
status: s("status"),
pubkey: None,
},
);
}
}
}
}
let dir = root.join("roles");
if dir.is_dir() {
match std::fs::read_dir(&dir) {
Err(e) => eprintln!(
"confer: ⚠ cannot read {} ({e}) — every role's display/host/pubkey is MISSING this load \
(who/whois/verification will be blind).",
dir.display()
),
Ok(entries) => {
for e in entries.flatten() {
let p = e.path();
if p.extension().and_then(|s| s.to_str()) != Some("md") {
continue;
}
let Some(id) = p.file_stem().and_then(|s| s.to_str()) else {
continue;
};
if let Ok(txt) = std::fs::read_to_string(&p) {
match parse_role(&txt) {
Some(role) => {
m.insert(id.to_string(), role);
}
None => eprintln!("confer: skipping malformed role card {}", p.display()),
}
}
}
}
}
}
m
}
fn parse_role(text: &str) -> Option<Role> {
let text = text.strip_prefix('\u{FEFF}').unwrap_or(text);
let mut lines = text.lines();
if lines.next().map(str::trim_end) != Some("---") {
return None;
}
let mut yaml = String::new();
for line in lines {
if line.trim_end() == "---" {
break;
}
yaml.push_str(line);
yaml.push('\n');
}
let mut role: Role = serde_yaml::from_str(&yaml).ok()?;
if let Ok(m) = serde_yaml::from_str::<serde_yaml::Mapping>(&yaml) {
role.pubkey = classify_pubkey(&m).ok().flatten();
} else {
role.pubkey = None;
}
Some(role)
}
pub fn display<'a>(roster: &'a Roster, id: &'a str) -> &'a str {
roster
.get(id)
.and_then(|r| r.display.as_deref())
.unwrap_or(id)
}
pub fn host<'a>(roster: &'a Roster, id: &str) -> Option<&'a str> {
roster.get(id).and_then(|r| r.host.as_deref())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_role_never_pins_a_nonstring_pubkey() {
assert_eq!(parse_role("---\ndisplay: v\npubkey: true\n---\n").unwrap().pubkey, None);
assert_eq!(parse_role("---\npubkey: 12345\n---\n").unwrap().pubkey, None);
assert_eq!(parse_role("---\npubkey: null\n---\n").unwrap().pubkey, None);
assert_eq!(
parse_role("---\npubkey: yes\n---\n").unwrap().pubkey.as_deref(),
Some("yes")
);
assert_eq!(
parse_role("---\npubkey: ssh-ed25519 AAAA x\n---\n").unwrap().pubkey.as_deref(),
Some("ssh-ed25519 AAAA x")
);
assert_eq!(
parse_role("\u{FEFF}---\npubkey: ssh-ed25519 AAAA x\n---\n").unwrap().pubkey.as_deref(),
Some("ssh-ed25519 AAAA x")
);
}
}