From a321bbe26b50c5520d9b65289a4689df4bddc346 Mon Sep 17 00:00:00 2001 From: Dane Jensen Date: Tue, 10 Mar 2026 14:15:52 -0700 Subject: [PATCH 01/10] trimmed pr to the essentials. --- Cargo.lock | 10 ++++++++++ crates/tiny/Cargo.toml | 1 + crates/tiny/src/config.rs | 9 ++++++++- crates/tiny/src/main.rs | 2 +- 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c6eda0ac..6f2e771b 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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32824fab5e16e6c4d86dc1ba84489390419a39f97699852b66480bb87d297ed8" +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..f800e1f0 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.1.2" 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 6dbc403c..9573afdb 100644 --- a/crates/tiny/src/config.rs +++ b/crates/tiny/src/config.rs @@ -32,7 +32,7 @@ impl TryFrom> for ClientSASLAuth { Ok(match sasl { SASLAuth::Plain { username, password } => ClientSASLAuth::Plain { username, password }, SASLAuth::External { pem } => ClientSASLAuth::External { - pem: std::fs::read(pem).map_err(|e| format!("Could not read PEM file: {e}"))?, + pem: std::fs::read(expand_path(pem)).map_err(|e| format!("Could not read PEM file: {e}"))?, }, }) } @@ -396,6 +396,13 @@ impl Config { } } +pub(crate) fn expand_path(path: PathBuf) -> PathBuf { + match shellexpand::full(&path.to_string_lossy()) { + Err(e) => panic!("error expanding path: {e:?}"), + Ok(expanded) => PathBuf::from(expanded.as_ref()), + } +} + /// Returns tiny config file path. File may or may not exist. /// /// Places to look: (in priority order) diff --git a/crates/tiny/src/main.rs b/crates/tiny/src/main.rs index f0a28fe7..ffe5e01a 100644 --- a/crates/tiny/src/main.rs +++ b/crates/tiny/src/main.rs @@ -90,7 +90,7 @@ fn run( ) { let debug_log_file = match log_dir.as_ref() { Some(log_dir) => { - let mut log_dir = log_dir.clone(); + let mut log_dir = config::expand_path(log_dir.clone()); log_dir.push(DEBUG_LOG_FILE); log_dir } From 2f30b57540661092d5f2a618a66fe0aae7e88219 Mon Sep 17 00:00:00 2001 From: Dane Jensen Date: Tue, 10 Mar 2026 14:18:13 -0700 Subject: [PATCH 02/10] fixed formatting --- crates/tiny/src/config.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/tiny/src/config.rs b/crates/tiny/src/config.rs index 9573afdb..dbfbad38 100644 --- a/crates/tiny/src/config.rs +++ b/crates/tiny/src/config.rs @@ -32,7 +32,8 @@ impl TryFrom> for ClientSASLAuth { Ok(match sasl { SASLAuth::Plain { username, password } => ClientSASLAuth::Plain { username, password }, SASLAuth::External { pem } => ClientSASLAuth::External { - pem: std::fs::read(expand_path(pem)).map_err(|e| format!("Could not read PEM file: {e}"))?, + pem: std::fs::read(expand_path(pem)) + .map_err(|e| format!("Could not read PEM file: {e}"))?, }, }) } From 52d10546e08b78af19388edbde3b30d3f78ec4ec Mon Sep 17 00:00:00 2001 From: Dane Jensen Date: Wed, 11 Mar 2026 22:42:23 -0700 Subject: [PATCH 03/10] Tightened up error handling from feedback. --- crates/tiny/src/config.rs | 53 +++++++++++++++++++++++++++++++++++---- crates/tiny/src/main.rs | 7 +++++- 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/crates/tiny/src/config.rs b/crates/tiny/src/config.rs index dbfbad38..f7bd9162 100644 --- a/crates/tiny/src/config.rs +++ b/crates/tiny/src/config.rs @@ -32,8 +32,7 @@ impl TryFrom> for ClientSASLAuth { Ok(match sasl { SASLAuth::Plain { username, password } => ClientSASLAuth::Plain { username, password }, SASLAuth::External { pem } => ClientSASLAuth::External { - pem: std::fs::read(expand_path(pem)) - .map_err(|e| format!("Could not read PEM file: {e}"))?, + pem: std::fs::read(pem).map_err(|e| format!("Could not read PEM file: {e}"))?, }, }) } @@ -315,6 +314,47 @@ impl Config { errors } + pub(crate) fn expand_fields(self) -> Option> { + let Config { + servers, + defaults, + log_dir, + } = self; + + let mut servers_: Vec> = Vec::with_capacity(servers.len()); + + for server in servers { + let mut server_ = server.clone(); + + let sasl_auth = match server_.sasl_auth { + None => None, + Some(SASLAuth::Plain { username, password }) => { + Some(SASLAuth::Plain { username, password }) + } + Some(SASLAuth::External { pem }) => Some(SASLAuth::External { + pem: expand_path(pem)?, + }), + }; + + server_.sasl_auth = sasl_auth; + servers_.push(server_); + } + + let log_dir = match log_dir { + Some(dir) => { + let expanded = expand_path(dir)?; + Some(expanded) + } + None => None, + }; + + Some(Config { + servers: servers_, + defaults, + log_dir, + }) + } + /// Runs password commands and updates the config with plain passwords obtained from the /// commands. pub(crate) fn read_passwords(self) -> Option> { @@ -397,10 +437,13 @@ impl Config { } } -pub(crate) fn expand_path(path: PathBuf) -> PathBuf { +pub(crate) fn expand_path(path: PathBuf) -> Option { match shellexpand::full(&path.to_string_lossy()) { - Err(e) => panic!("error expanding path: {e:?}"), - Ok(expanded) => PathBuf::from(expanded.as_ref()), + Err(e) => { + println!("error expanding path: {e:?}"); + None + } + Ok(expanded) => Some(PathBuf::from(expanded.as_ref())), } } diff --git a/crates/tiny/src/main.rs b/crates/tiny/src/main.rs index ffe5e01a..e33494e0 100644 --- a/crates/tiny/src/main.rs +++ b/crates/tiny/src/main.rs @@ -54,6 +54,11 @@ fn main() { exit(1); } + let config = match config.expand_fields() { + None => exit(1), + Some(config) => config, + }; + let config = match config.read_passwords() { None => exit(1), Some(config) => config, @@ -90,7 +95,7 @@ fn run( ) { let debug_log_file = match log_dir.as_ref() { Some(log_dir) => { - let mut log_dir = config::expand_path(log_dir.clone()); + let mut log_dir = log_dir.clone(); log_dir.push(DEBUG_LOG_FILE); log_dir } From 7def5a113fc982cf317b8f7c4b7ae90f01435966 Mon Sep 17 00:00:00 2001 From: Dane Jensen Date: Fri, 13 Mar 2026 14:26:58 -0700 Subject: [PATCH 04/10] Cleaned up implementation from feedback. --- crates/tiny/src/config.rs | 48 ++++++++++++--------------------------- crates/tiny/src/main.rs | 12 ++++++---- 2 files changed, 22 insertions(+), 38 deletions(-) diff --git a/crates/tiny/src/config.rs b/crates/tiny/src/config.rs index f7bd9162..86d79157 100644 --- a/crates/tiny/src/config.rs +++ b/crates/tiny/src/config.rs @@ -1,6 +1,8 @@ use libtiny_client::SASLAuth as ClientSASLAuth; use serde::{Deserialize, Deserializer}; +use shellexpand::LookupError; +use std::env::VarError; use std::fs; use std::fs::File; use std::io::{Read, Write}; @@ -314,45 +316,23 @@ impl Config { errors } - pub(crate) fn expand_fields(self) -> Option> { - let Config { - servers, - defaults, - log_dir, - } = self; - - let mut servers_: Vec> = Vec::with_capacity(servers.len()); - - for server in servers { - let mut server_ = server.clone(); - - let sasl_auth = match server_.sasl_auth { + pub(crate) fn expand_fields(&mut self) -> Result<(), LookupError> { + for server in &mut self.servers { + server.sasl_auth = match &mut server.sasl_auth { None => None, - Some(SASLAuth::Plain { username, password }) => { - Some(SASLAuth::Plain { username, password }) - } + Some(other @ SASLAuth::Plain { .. }) => Some(other.clone()), Some(SASLAuth::External { pem }) => Some(SASLAuth::External { - pem: expand_path(pem)?, + pem: expand_path(pem.to_path_buf())?, }), }; - - server_.sasl_auth = sasl_auth; - servers_.push(server_); } - let log_dir = match log_dir { - Some(dir) => { - let expanded = expand_path(dir)?; - Some(expanded) - } + self.log_dir = match &self.log_dir { None => None, + Some(dir) => Some(expand_path(dir.to_path_buf())?), }; - Some(Config { - servers: servers_, - defaults, - log_dir, - }) + Ok(()) } /// Runs password commands and updates the config with plain passwords obtained from the @@ -437,13 +417,13 @@ impl Config { } } -pub(crate) fn expand_path(path: PathBuf) -> Option { +pub(crate) fn expand_path(path: PathBuf) -> Result> { match shellexpand::full(&path.to_string_lossy()) { Err(e) => { - println!("error expanding path: {e:?}"); - None + println!("error expanding variable {e:?}"); + Err(e) } - Ok(expanded) => Some(PathBuf::from(expanded.as_ref())), + Ok(expanded) => Ok(PathBuf::from(expanded.as_ref())), } } diff --git a/crates/tiny/src/main.rs b/crates/tiny/src/main.rs index e33494e0..20181f92 100644 --- a/crates/tiny/src/main.rs +++ b/crates/tiny/src/main.rs @@ -41,7 +41,7 @@ fn main() { println!("{yaml_err}"); exit(1); } - Ok(config) => { + Ok(mut config) => { let config_errors = config.validate(); if !config_errors.is_empty() { println!( @@ -54,9 +54,13 @@ fn main() { exit(1); } - let config = match config.expand_fields() { - None => exit(1), - Some(config) => config, + match config.expand_fields() { + Err(expand_error) => { + println!("Can't expand variable:"); + println!("{expand_error}"); + exit(1); + } + Ok(_) => (), }; let config = match config.read_passwords() { From da0205d640220978ae46a6d7c2d18eca3025597c Mon Sep 17 00:00:00 2001 From: Dane Jensen Date: Fri, 13 Mar 2026 17:23:48 -0700 Subject: [PATCH 05/10] fixed leftover print statement. --- crates/tiny/src/config.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/tiny/src/config.rs b/crates/tiny/src/config.rs index 86d79157..5ca2f674 100644 --- a/crates/tiny/src/config.rs +++ b/crates/tiny/src/config.rs @@ -419,10 +419,7 @@ impl Config { pub(crate) fn expand_path(path: PathBuf) -> Result> { match shellexpand::full(&path.to_string_lossy()) { - Err(e) => { - println!("error expanding variable {e:?}"); - Err(e) - } + Err(e) => Err(e), Ok(expanded) => Ok(PathBuf::from(expanded.as_ref())), } } From a995b4f140c9021bf56d3e72b927a4470a57f114 Mon Sep 17 00:00:00 2001 From: Dane Jensen Date: Wed, 18 Mar 2026 16:39:31 -0700 Subject: [PATCH 06/10] Privated expand_path and cleaned up .expand_fields() error handling in main.rs --- crates/tiny/src/config.rs | 2 +- crates/tiny/src/main.rs | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/crates/tiny/src/config.rs b/crates/tiny/src/config.rs index 5ca2f674..d37213df 100644 --- a/crates/tiny/src/config.rs +++ b/crates/tiny/src/config.rs @@ -417,7 +417,7 @@ impl Config { } } -pub(crate) fn expand_path(path: PathBuf) -> Result> { +fn expand_path(path: PathBuf) -> Result> { match shellexpand::full(&path.to_string_lossy()) { Err(e) => Err(e), Ok(expanded) => Ok(PathBuf::from(expanded.as_ref())), diff --git a/crates/tiny/src/main.rs b/crates/tiny/src/main.rs index 20181f92..2d31e7a3 100644 --- a/crates/tiny/src/main.rs +++ b/crates/tiny/src/main.rs @@ -54,13 +54,10 @@ fn main() { exit(1); } - match config.expand_fields() { - Err(expand_error) => { - println!("Can't expand variable:"); - println!("{expand_error}"); - exit(1); - } - Ok(_) => (), + if let Err(var_error) = config.expand_fields() { + println!("Config file error: cannot expand variable:"); + println!("- {var_error}"); + exit(1); }; let config = match config.read_passwords() { From 35cc798634f158f09f990aa5bc5e5c3c31146d74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Sinan=20A=C4=9Facan?= Date: Thu, 19 Mar 2026 05:46:42 +0000 Subject: [PATCH 07/10] Pass env and home dir to expansion function, to allow testing --- crates/tiny/src/config.rs | 23 +++++++++++++++-------- crates/tiny/src/main.rs | 5 ++++- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/crates/tiny/src/config.rs b/crates/tiny/src/config.rs index d37213df..afd8e171 100644 --- a/crates/tiny/src/config.rs +++ b/crates/tiny/src/config.rs @@ -316,20 +316,24 @@ impl Config { errors } - pub(crate) fn expand_fields(&mut self) -> Result<(), LookupError> { + pub(crate) fn expand_fields( + &mut self, + home_dir: impl Fn() -> Option, + env_var: impl Fn(&str) -> Result, VarError>, + ) -> Result<(), LookupError> { for server in &mut self.servers { server.sasl_auth = match &mut server.sasl_auth { None => None, Some(other @ SASLAuth::Plain { .. }) => Some(other.clone()), Some(SASLAuth::External { pem }) => Some(SASLAuth::External { - pem: expand_path(pem.to_path_buf())?, + pem: expand_path(pem.to_path_buf(), &home_dir, &env_var)?, }), }; } self.log_dir = match &self.log_dir { None => None, - Some(dir) => Some(expand_path(dir.to_path_buf())?), + Some(dir) => Some(expand_path(dir.to_path_buf(), &home_dir, &env_var)?), }; Ok(()) @@ -417,11 +421,14 @@ impl Config { } } -fn expand_path(path: PathBuf) -> Result> { - match shellexpand::full(&path.to_string_lossy()) { - Err(e) => Err(e), - Ok(expanded) => Ok(PathBuf::from(expanded.as_ref())), - } +fn expand_path( + path: PathBuf, + home_dir: &impl Fn() -> Option, + env_var: &impl Fn(&str) -> Result, VarError>, +) -> Result> { + let path_str = path.to_string_lossy(); + let expanded = shellexpand::full_with_context(&path_str, home_dir, env_var)?; + Ok(PathBuf::from(expanded.as_ref())) } /// Returns tiny config file path. File may or may not exist. diff --git a/crates/tiny/src/main.rs b/crates/tiny/src/main.rs index 2d31e7a3..6021188c 100644 --- a/crates/tiny/src/main.rs +++ b/crates/tiny/src/main.rs @@ -54,7 +54,10 @@ fn main() { exit(1); } - if let Err(var_error) = config.expand_fields() { + if let Err(var_error) = config.expand_fields( + || dirs::home_dir().and_then(|p| p.into_os_string().into_string().ok()), + |s| std::env::var(s).map(Some), + ) { println!("Config file error: cannot expand variable:"); println!("- {var_error}"); exit(1); From 4e3d401bbb335f8dd16021c3f550d4d5e8f4f2f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Sinan=20A=C4=9Facan?= Date: Thu, 19 Mar 2026 05:56:52 +0000 Subject: [PATCH 08/10] Tweak error message, tests --- crates/tiny/src/config.rs | 72 +++++++++++++++++++++++++++++++++++++++ crates/tiny/src/main.rs | 4 +-- 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/crates/tiny/src/config.rs b/crates/tiny/src/config.rs index afd8e171..adbe8475 100644 --- a/crates/tiny/src/config.rs +++ b/crates/tiny/src/config.rs @@ -594,4 +594,76 @@ mod tests { ]) ); } + + #[test] + fn config_shell_expansion() { + let mut config: Config = Config { + servers: vec![Server { + addr: "my_server".to_owned(), + alias: None, + port: 123, + tls: false, + pass: None, + autoconnect: true, + user: None, + realname: "".to_owned(), + nicks: vec!["".to_owned()], + join: vec![], + nickserv_ident: None, + sasl_auth: Some(SASLAuth::External { + pem: "~/a/$SASL/b".into(), + }), + }], + defaults: Defaults { + nicks: vec!["".to_owned()], + realname: "".to_owned(), + join: vec![], + tls: false, + }, + log_dir: Some("~/b/$LOG/c".into()), + }; + config + .expand_fields( + || Some("/home/test".to_string()), + |s| match s { + "SASL" => Ok(Some("sasl_val".to_string())), + "LOG" => Ok(Some("log_val".to_string())), + _ => Err(VarError::NotPresent), + }, + ) + .unwrap(); + + assert_eq!( + config.servers[0].sasl_auth, + Some(SASLAuth::External { + pem: PathBuf::from("/home/test/a/sasl_val/b"), + }) + ); + assert_eq!( + config.log_dir, + Some(PathBuf::from("/home/test/b/log_val/c")) + ); + } + + #[test] + fn config_shell_expansion_var_fail() { + let mut config: Config = Config { + servers: vec![], + defaults: Defaults { + nicks: vec!["nick".to_owned()], + realname: "real".to_owned(), + join: vec![], + tls: false, + }, + log_dir: Some("~/logs/$MISSING/data".into()), + }; + let err = config + .expand_fields( + || Some("/home/test".to_string()), + |_| Err(VarError::NotPresent), + ) + .unwrap_err(); + assert_eq!(err.var_name, "MISSING"); + assert_eq!(err.cause, VarError::NotPresent); + } } diff --git a/crates/tiny/src/main.rs b/crates/tiny/src/main.rs index 6021188c..0c7cfab1 100644 --- a/crates/tiny/src/main.rs +++ b/crates/tiny/src/main.rs @@ -58,8 +58,8 @@ fn main() { || dirs::home_dir().and_then(|p| p.into_os_string().into_string().ok()), |s| std::env::var(s).map(Some), ) { - println!("Config file error: cannot expand variable:"); - println!("- {var_error}"); + println!("Config file error:"); + println!("- Cannot expand variable: {var_error}"); exit(1); }; From c6c8c569e8ba1bfaba61408f5aea3d6c9b5d5c67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Sinan=20A=C4=9Facan?= Date: Thu, 19 Mar 2026 05:58:02 +0000 Subject: [PATCH 09/10] Update default config to clarify --- crates/tiny/config.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/tiny/config.yml b/crates/tiny/config.yml index 8e2b9777..6131ab46 100644 --- a/crates/tiny/config.yml +++ b/crates/tiny/config.yml @@ -49,7 +49,9 @@ servers: # sasl: # username: tiny_user # password: hunter2 - # pem: "/home/user/.config/tiny/oftc.pem" + + # sasl: + # pem: "$HOME/.config/tiny/oftc.pem" # nickserv_ident: hunter2 From 490ce5798d80507f72c96337923e3872d374450a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Sinan=20A=C4=9Facan?= Date: Thu, 19 Mar 2026 06:06:13 +0000 Subject: [PATCH 10/10] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f48a6e0..41b047cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ tiny does not update permissions of existing log files. - Add new server configuration option `autoconnect` to disable automatically connecting to a server on startup. (#443) +- Expand tildes and shell variables in `pem` (SASL authentication) and + `log_dir` config fields. (#192, #463) # 2025/01/01: 0.13.0