Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/tiny/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [] }
Expand Down
107 changes: 106 additions & 1 deletion crates/tiny/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Copy link
Copy Markdown
Owner

@osa1 osa1 Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When expand_path fails to expand a variable we can't continue, we should show the error to the user and stop.

parse_config should return all errors, including IO errors (which we don't return currently, but we should) + path expansion errors. The call site (main) already deals with errors (prints them and exits).

}
}
}

pub(crate) fn parse_config(config_path: &Path) -> Result<Config<PassOrCmd>, serde_yaml::Error> {
let contents = {
let mut str = String::new();
Expand All @@ -430,7 +441,18 @@ pub(crate) fn parse_config(config_path: &Path) -> Result<Config<PassOrCmd>, serd
str
};

serde_yaml::from_str(&contents)
let mut config: Config<PassOrCmd> = 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) {
Expand Down Expand Up @@ -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);
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't have to create a file to test config file parsing and shell expansion. Refactor the code to separate the IO parts (the let contents = { ... } part in parse_config) from parsing parts, then test the parsing function.

}

#[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";
Expand Down
Loading