From faf1b8dbfa2016a594afe8621f68111454d01ec5 Mon Sep 17 00:00:00 2001 From: Dabira Olaoluwa Date: Sun, 16 Aug 2026 22:39:23 -0700 Subject: [PATCH] fix(utils): hard-error in get_config_dir when HOME is unresolvable Previously, get_config_dir() silently fell back to the current working directory when dirs_next::home_dir() returned None, causing the CLI to write auth tokens and config.json into ./.txio/ instead of a stable home directory. This had two real-world risks: - Silent credential loss in ephemeral environments (CI, containers) where the working directory is wiped after the run. - Accidental credential exposure via git if the CLI was run from inside a tracked project directory. Changes: - get_config_dir() now returns Result and fails with a clear, actionable error message if home_dir() returns None or an empty string: "Could not determine home directory (checked $HOME). Set the HOME environment variable and try again." - All 11 callers propagate the Result with ? (save_token, get_token, remove_token, save_config, get_config, list_config, remove_config, save_current_chain, get_current_chain, load_environment). - get_token and get_current_chain signatures change from Option to Result>; all call sites in handlers.rs updated. - Error propagates to main(), which already prints errors to stderr and exits with code 1. - Added test: get_config_dir_fails_when_home_unset. BREAKING CHANGE: any workflow relying on the old silent working-directory fallback (./.txio/) will now receive an explicit error. This is intentional. --- src/cli/handlers.rs | 8 +++---- src/utils/mod.rs | 56 ++++++++++++++++++++++++++++++--------------- 2 files changed, 42 insertions(+), 22 deletions(-) diff --git a/src/cli/handlers.rs b/src/cli/handlers.rs index e281d29..d2e9b0a 100644 --- a/src/cli/handlers.rs +++ b/src/cli/handlers.rs @@ -82,8 +82,8 @@ impl CommandHandler { ui::print_success("Logged out successfully."); } Commands::Status => { - let chain = utils::get_current_chain().unwrap_or_else(|| "sui".to_string()); - let logged_in = utils::get_token().is_some(); + let chain = utils::get_current_chain()?.unwrap_or_else(|| "sui".to_string()); + let logged_in = utils::get_token()?.is_some(); println!("{}", "─── txio Status ───".bold().cyan()); println!( " {} Default chain: {}", @@ -190,7 +190,7 @@ impl CommandHandler { } async fn handle_db_command(action: DbAction) -> Result<()> { - let token = match utils::get_token() { + let token = match utils::get_token()? { Some(token) => token, None => { ui::print_error(&format!( @@ -423,7 +423,7 @@ impl CommandHandler { // attributes the log to the authenticated user itself, so a // failure here (offline, logged out, server unreachable) // must never block returning the RPC result to the user. - if let Some(token) = utils::get_token() { + if let Some(token) = utils::get_token()? { let log_request = RpcLogRequest { method: method.clone(), params: params_val, diff --git a/src/utils/mod.rs b/src/utils/mod.rs index f40fceb..addc527 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -15,7 +15,7 @@ use std::path::{Path, PathBuf}; /// makes it win over the trusted default, while real process env — set before /// either loader runs — always wins over both. pub fn load_environment(explicit_env_file: Option<&Path>) -> Result<()> { - let mut trusted = get_config_dir(); + let mut trusted = get_config_dir()?; trusted.push(".env"); let cwd_env = Path::new(".env"); load_env_files(explicit_env_file, &trusted, cwd_env) @@ -50,8 +50,10 @@ fn should_warn_unloaded_cwd_env(explicit_provided: bool, cwd_env: &Path) -> bool !explicit_provided && cwd_env.exists() } -pub fn get_config_dir() -> PathBuf { - let mut path = dirs_next::home_dir().unwrap_or_else(|| PathBuf::from(".")); +pub fn get_config_dir() -> Result { + let mut path = dirs_next::home_dir() + .filter(|p| !p.as_os_str().is_empty()) + .ok_or_else(|| anyhow!("Could not determine home directory (checked $HOME). Set the HOME environment variable and try again."))?; path.push(".txio"); if !path.exists() { #[cfg(unix)] @@ -67,24 +69,24 @@ pub fn get_config_dir() -> PathBuf { let _ = fs::create_dir_all(&path); } } - path + Ok(path) } pub fn save_current_chain(chain: &str) -> Result<()> { - let mut path = get_config_dir(); + let mut path = get_config_dir()?; path.push("current_chain"); fs::write(path, chain)?; Ok(()) } -pub fn get_current_chain() -> Option { - let mut path = get_config_dir(); +pub fn get_current_chain() -> Result> { + let mut path = get_config_dir()?; path.push("current_chain"); - fs::read_to_string(path).ok().map(|s| s.trim().to_string()) + Ok(fs::read_to_string(path).ok().map(|s| s.trim().to_string())) } pub fn save_token(token: &str) -> Result<()> { - let mut path = get_config_dir(); + let mut path = get_config_dir()?; path.push("token"); #[cfg(unix)] @@ -110,14 +112,14 @@ pub fn save_token(token: &str) -> Result<()> { Ok(()) } -pub fn get_token() -> Option { - let mut path = get_config_dir(); +pub fn get_token() -> Result> { + let mut path = get_config_dir()?; path.push("token"); - fs::read_to_string(path).ok().map(|s| s.trim().to_string()) + Ok(fs::read_to_string(path).ok().map(|s| s.trim().to_string())) } pub fn remove_token() -> Result<()> { - let mut path = get_config_dir(); + let mut path = get_config_dir()?; path.push("token"); if path.exists() { fs::remove_file(path)?; @@ -126,7 +128,7 @@ pub fn remove_token() -> Result<()> { } pub fn save_config(key: &str, value: &str) -> Result<()> { - let mut path = get_config_dir(); + let mut path = get_config_dir()?; path.push("config.json"); let mut map: serde_json::Map = if path.exists() { let content = fs::read_to_string(&path)?; @@ -143,7 +145,7 @@ pub fn save_config(key: &str, value: &str) -> Result<()> { } pub fn get_config(key: &str) -> Result> { - let mut path = get_config_dir(); + let mut path = get_config_dir()?; path.push("config.json"); if !path.exists() { return Ok(None); @@ -155,7 +157,7 @@ pub fn get_config(key: &str) -> Result> { } pub fn list_config() -> Result> { - let mut path = get_config_dir(); + let mut path = get_config_dir()?; path.push("config.json"); if !path.exists() { return Ok(vec![]); @@ -170,7 +172,7 @@ pub fn list_config() -> Result> { } pub fn remove_config(key: &str) -> Result<()> { - let mut path = get_config_dir(); + let mut path = get_config_dir()?; path.push("config.json"); if !path.exists() { return Ok(()); @@ -362,7 +364,7 @@ mod tests { std::env::set_var("HOME", &temp_home); } - let config_dir = get_config_dir(); + let config_dir = get_config_dir().unwrap(); let mode = fs::metadata(&config_dir).unwrap().permissions().mode() & 0o777; assert_eq!(mode, 0o700, "config dir must have mode 0o700"); @@ -449,4 +451,22 @@ mod tests { None => unsafe { std::env::remove_var("HOME") }, } } + + #[test] + fn get_config_dir_fails_when_home_unset() { + let _g = ENV_LOCK.lock().unwrap(); + let old_home = std::env::var_os("HOME"); + unsafe { + std::env::remove_var("HOME"); + } + + let result = get_config_dir(); + assert!(result.is_err(), "get_config_dir should fail when HOME is unset"); + assert!(result.unwrap_err().to_string().contains("Could not determine home directory")); + + match old_home { + Some(value) => unsafe { std::env::set_var("HOME", value) }, + None => unsafe { std::env::remove_var("HOME") }, + } + } }