From 51e533b6e8ed075647dcc3de6a45640ce10b18da Mon Sep 17 00:00:00 2001 From: onurogut Date: Sun, 15 Feb 2026 21:58:59 +0300 Subject: [PATCH] Expand ~ and $VAR in config paths (log_dir, sasl pem) Use shellexpand to resolve tilde and environment variables in log_dir and SASL external PEM paths during config parsing. Add shellexpand as a new dependency for tilde (~) and environment variable ($VAR) expansion in file paths. Add tests for path expansion, log_dir and sasl pem integration. Fixes #192, fixes #429 --- Cargo.lock | 10 ++++ crates/tiny/Cargo.toml | 1 + crates/tiny/src/config.rs | 107 +++++++++++++++++++++++++++++++++++++- 3 files changed, 117 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index c6eda0ac..1a4d99bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -867,6 +867,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" +[[package]] +name = "shellexpand" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb" +dependencies = [ + "dirs", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1062,6 +1071,7 @@ dependencies = [ "serde", "serde_yaml", "shell-words", + "shellexpand", "term_input", "termbox_simple", "time 0.1.45", diff --git a/crates/tiny/Cargo.toml b/crates/tiny/Cargo.toml index 70095dc5..4f3d0bde 100644 --- a/crates/tiny/Cargo.toml +++ b/crates/tiny/Cargo.toml @@ -27,6 +27,7 @@ log = "0.4" serde = { version = "1.0.196", features = ["derive"] } serde_yaml = "0.8" shell-words = "1.1.0" +shellexpand = "3" time = "0.1" tokio = { version = "1.36", default-features = false, features = [] } tokio-stream = { version = "0.1", features = [] } diff --git a/crates/tiny/src/config.rs b/crates/tiny/src/config.rs index b045717b..eb7d853c 100644 --- a/crates/tiny/src/config.rs +++ b/crates/tiny/src/config.rs @@ -422,6 +422,17 @@ pub(crate) fn get_config_path() -> PathBuf { } } +fn expand_path(path: &Path) -> PathBuf { + let s = path.to_string_lossy(); + match shellexpand::full(&s) { + Ok(expanded) => PathBuf::from(expanded.as_ref()), + Err(err) => { + println!("Failed to expand path {path:?}: {err}"); + path.to_owned() + } + } +} + pub(crate) fn parse_config(config_path: &Path) -> Result, serde_yaml::Error> { let contents = { let mut str = String::new(); @@ -430,7 +441,18 @@ pub(crate) fn parse_config(config_path: &Path) -> Result, serd str }; - serde_yaml::from_str(&contents) + let mut config: Config = serde_yaml::from_str(&contents)?; + + if let Some(log_dir) = &mut config.log_dir { + *log_dir = expand_path(log_dir); + } + for server in &mut config.servers { + if let Some(SASLAuth::External { pem }) = &mut server.sasl_auth { + *pem = expand_path(pem); + } + } + + Ok(config) } pub(crate) fn generate_default_config(config_path: &Path) { @@ -524,6 +546,89 @@ mod tests { ); } + #[test] + fn parse_config_expands_log_dir() { + let home = std::env::var("HOME").unwrap(); + let yaml = "\ +servers: + - addr: irc.test.com + port: 6697 + tls: true + realname: test + nicks: [test] + join: [] +defaults: + nicks: [test] + realname: test +log_dir: ~/test_logs +"; + let dir = std::env::temp_dir().join("tiny_test_parse_config"); + let _ = fs::create_dir_all(&dir); + let config_path = dir.join("config.yml"); + fs::write(&config_path, yaml).unwrap(); + + let config = parse_config(&config_path).unwrap(); + assert_eq!( + config.log_dir.unwrap(), + PathBuf::from(format!("{home}/test_logs")) + ); + + let _ = fs::remove_dir_all(&dir); + } + + #[test] + fn parse_config_expands_sasl_pem() { + let home = std::env::var("HOME").unwrap(); + let yaml = "\ +servers: + - addr: irc.test.com + port: 6697 + tls: true + realname: test + nicks: [test] + join: [] + sasl: + pem: ~/certs/my.pem +defaults: + nicks: [test] + realname: test +"; + let dir = std::env::temp_dir().join("tiny_test_parse_config_sasl"); + let _ = fs::create_dir_all(&dir); + let config_path = dir.join("config.yml"); + fs::write(&config_path, yaml).unwrap(); + + let config = parse_config(&config_path).unwrap(); + match &config.servers[0].sasl_auth { + Some(SASLAuth::External { pem }) => { + assert_eq!(*pem, PathBuf::from(format!("{home}/certs/my.pem"))); + } + other => panic!("Expected SASLAuth::External, got {other:?}"), + } + + let _ = fs::remove_dir_all(&dir); + } + + #[test] + fn expand_path_tilde() { + let home = std::env::var("HOME").unwrap(); + let expanded = expand_path(Path::new("~/foo")); + assert_eq!(expanded, PathBuf::from(format!("{home}/foo"))); + } + + #[test] + fn expand_path_env_var() { + let home = std::env::var("HOME").unwrap(); + let expanded = expand_path(Path::new("$HOME/foo")); + assert_eq!(expanded, PathBuf::from(format!("{home}/foo"))); + } + + #[test] + fn expand_path_no_expansion() { + let path = Path::new("/absolute/path/no/vars"); + assert_eq!(expand_path(path), path); + } + #[test] fn parse_password_field() { let field = "command: my pass cmd";