diff --git a/src/config.rs b/src/config.rs index 93dccbd..e3290dc 100644 --- a/src/config.rs +++ b/src/config.rs @@ -193,6 +193,23 @@ impl Config { Some(c) } + /// A deterministic text dump of every parsed key/value (sorted), used by + /// `echoircd checkconfig` to compare two configs regardless of source format. + pub fn dump(&self) -> String { + let mut keys: Vec<&String> = self.raw.keys().collect(); + keys.sort(); + let mut out = String::new(); + for k in keys { + for v in &self.raw[k] { + out.push_str(k); + out.push_str(" = "); + out.push_str(v); + out.push('\n'); + } + } + out + } + // small helper is defined at module scope (see `yesish`). /// Parse `key = value` lines into `c`; unknown keys and comments are ignored. @@ -585,14 +602,17 @@ fn emit_block(out: &mut String, name: &str, fields: &[(String, String)]) { } } "tls" => { - if let Some(v) = get("backend") { - emit_line(out, "tls_backend", v); - } - if let Some(v) = get("cert") { - emit_line(out, "tls_cert", v); - } - if let Some(v) = get("key") { - emit_line(out, "tls_key", v); + // backend/cert/key map to their tls_ keys; any other field (sni, + // handshake_timeout, …) passes through as tls_, repeatable. + for (f, v) in fields { + let fl = f.to_ascii_lowercase(); + let key = match fl.as_str() { + "backend" => "tls_backend".to_string(), + "cert" => "tls_cert".to_string(), + "key" => "tls_key".to_string(), + other => format!("tls_{other}"), + }; + emit_line(out, &key, v); } } "cloak" => { diff --git a/src/main.rs b/src/main.rs index 1592589..b380808 100644 --- a/src/main.rs +++ b/src/main.rs @@ -132,6 +132,26 @@ fn mkpasswd_cli(cost_arg: Option<&str>) -> i32 { } } +/// `echoircd checkconfig [config]`: parse a config and print a deterministic, +/// sorted dump of every key/value it produces. Two configs (e.g. flat vs block +/// format) that dump identically parse identically. +fn checkconfig_cli(path: &str) -> i32 { + match echoircd::config::Config::try_load(path) { + Some(c) => { + print!("{}", c.dump()); + if c.servername.is_empty() { + eprintln!("echoircd: WARNING — servername is empty"); + return 2; + } + 0 + } + None => { + eprintln!("echoircd: cannot read config {path}"); + 1 + } + } +} + fn main() { let mut args = std::env::args().skip(1); let first = args.next(); @@ -144,6 +164,12 @@ fn main() { if first.as_deref() == Some("mkpasswd") { std::process::exit(mkpasswd_cli(args.next().as_deref())); } + // `echoircd checkconfig [config]` parses a config and prints a sorted dump + // of every key/value (for validating a config or diffing two of them). + if first.as_deref() == Some("checkconfig") { + let cfgpath = args.next().unwrap_or_else(|| "echoircd.conf".to_string()); + std::process::exit(checkconfig_cli(&cfgpath)); + } let path = first.unwrap_or_else(|| "echoircd.conf".to_string()); let cfg = Config::load(&path);