use std::path::Path;
use ntk_common::Topology;
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
pub struct NtkdConfig {
gsizes: Vec<u32>,
nics: Vec<String>,
port: u16,
#[serde(default)]
andna_key_path: Option<std::path::PathBuf>,
#[serde(default)]
node_key_path: Option<std::path::PathBuf>,
#[serde(default)]
require_auth: bool,
}
impl NtkdConfig {
pub fn load(path: &Path) -> Result<Self, ConfigError> {
let text = std::fs::read_to_string(path).map_err(|source| ConfigError::Io {
path: path.display().to_string(),
source,
})?;
Self::from_str(&text).map_err(|source| ConfigError::Parse {
path: path.display().to_string(),
source: Box::new(source),
})
}
#[allow(
clippy::should_implement_trait,
reason = "contracted method name/signature, not a FromStr impl"
)]
pub fn from_str(text: &str) -> Result<Self, ConfigError> {
let config: Self = toml::from_str(text)?;
config.topology()?;
if config.require_auth && config.node_key_path.is_none() {
return Err(ConfigError::AuthWithoutKey);
}
Ok(config)
}
pub fn topology(&self) -> Result<Topology, ConfigError> {
Topology::new(self.gsizes.iter().copied()).map_err(ConfigError::Topology)
}
pub fn nics(&self) -> &[String] {
&self.nics
}
pub fn port(&self) -> u16 {
self.port
}
pub fn andna_key_path(&self) -> Option<&Path> {
self.andna_key_path.as_deref()
}
pub fn node_key_path(&self) -> Option<&Path> {
self.node_key_path.as_deref()
}
pub fn require_auth(&self) -> bool {
self.require_auth
}
}
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error("failed to read config file {path}: {source}")]
Io {
path: String,
source: std::io::Error,
},
#[error("failed to load config {path}: {source}")]
Parse {
path: String,
source: Box<ConfigError>,
},
#[error("failed to parse config: {0}")]
Toml(#[from] toml::de::Error),
#[error("invalid topology: {0}")]
Topology(#[from] ntk_common::Error),
#[error("require_auth is set but no node_key_path is configured")]
AuthWithoutKey,
}
#[cfg(test)]
mod tests {
use super::*;
const VALID: &str = r#"
gsizes = [4, 2, 2, 2]
nics = ["eth0", "eth1"]
port = 269
"#;
#[test]
fn round_trips_valid_toml() {
let config = NtkdConfig::from_str(VALID).expect("valid config parses");
assert_eq!(config.nics(), ["eth0", "eth1"]);
assert_eq!(config.port(), 269);
assert_eq!(config.topology().expect("valid topology").levels(), 4);
}
#[test]
fn andna_key_path_defaults_to_none_when_absent() {
let config = NtkdConfig::from_str(VALID).expect("valid config parses");
assert_eq!(config.andna_key_path(), None);
}
#[test]
fn andna_key_path_parses_when_present() {
let text = r#"
gsizes = [4, 2, 2, 2]
nics = ["eth0", "eth1"]
port = 269
andna_key_path = "/etc/ntkd/andna.key"
"#;
let config = NtkdConfig::from_str(text).expect("valid config parses");
assert_eq!(
config.andna_key_path(),
Some(Path::new("/etc/ntkd/andna.key"))
);
}
#[test]
fn auth_is_off_and_keyless_by_default() {
let config = NtkdConfig::from_str(VALID).expect("valid config parses");
assert_eq!(config.node_key_path(), None);
assert!(!config.require_auth());
}
#[test]
fn require_auth_without_a_node_key_is_refused() {
let text = r#"
gsizes = [4, 2, 2, 2]
nics = ["eth0", "eth1"]
port = 269
require_auth = true
"#;
assert!(matches!(
NtkdConfig::from_str(text),
Err(ConfigError::AuthWithoutKey)
));
}
#[test]
fn require_auth_with_a_node_key_parses() {
let text = r#"
gsizes = [4, 2, 2, 2]
nics = ["eth0", "eth1"]
port = 269
require_auth = true
node_key_path = "/etc/ntkd/node.key"
"#;
let config = NtkdConfig::from_str(text).expect("valid config parses");
assert!(config.require_auth());
assert_eq!(
config.node_key_path(),
Some(Path::new("/etc/ntkd/node.key"))
);
}
#[test]
fn node_key_and_andna_key_are_independent() {
let text = r#"
gsizes = [4, 2, 2, 2]
nics = ["eth0", "eth1"]
port = 269
node_key_path = "/etc/ntkd/node.key"
andna_key_path = "/etc/ntkd/andna.key"
"#;
let config = NtkdConfig::from_str(text).expect("valid config parses");
assert_ne!(config.node_key_path(), config.andna_key_path());
}
#[test]
fn rejects_zero_gsize_as_config_error_not_panic() {
let text = r#"
gsizes = [4, 0, 2]
nics = ["eth0"]
port = 269
"#;
let err = NtkdConfig::from_str(text).expect_err("zero gsize is invalid");
assert!(matches!(
err,
ConfigError::Topology(ntk_common::Error::ZeroGsize { level: 1 })
));
}
#[test]
fn rejects_malformed_toml() {
let err = NtkdConfig::from_str("not valid toml {{{").expect_err("malformed toml");
assert!(matches!(err, ConfigError::Toml(_)));
}
}