diff --git a/Cargo.lock b/Cargo.lock index e35af947ef..f1c8dbc810 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2102,6 +2102,7 @@ dependencies = [ "mongodb", "mysql_async", "mysql_common", + "nix", "notify", "openssl", "pageant", diff --git a/agents/drivers/duckdb/Cargo.lock b/agents/drivers/duckdb/Cargo.lock index f66e33c3b8..ed514f3981 100644 --- a/agents/drivers/duckdb/Cargo.lock +++ b/agents/drivers/duckdb/Cargo.lock @@ -1257,6 +1257,7 @@ dependencies = [ "minijinja", "mongodb", "mysql_async", + "nix", "notify", "openssl", "pageant", diff --git a/crates/dbx-core/Cargo.toml b/crates/dbx-core/Cargo.toml index 136670e70c..ec4e350875 100644 --- a/crates/dbx-core/Cargo.toml +++ b/crates/dbx-core/Cargo.toml @@ -91,6 +91,9 @@ iana-time-zone = "0.1" [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.61", features = ["Win32_Storage_FileSystem", "Win32_Foundation", "Win32_Security"] } +[target.'cfg(all(unix, not(target_os = "redox")))'.dependencies] +nix = { version = "0.31.3", features = ["user"] } + [dev-dependencies] mysql_common = { git = "https://github.com/t8y2/rust_mysql_common.git", rev = "47740d85a8277167a5717afac785d37b46a36296", default-features = false } tokio = { version = "1", features = ["test-util"] } diff --git a/crates/dbx-core/src/db/file_validator.rs b/crates/dbx-core/src/db/file_validator.rs index 0899ead0d1..771eece5df 100644 --- a/crates/dbx-core/src/db/file_validator.rs +++ b/crates/dbx-core/src/db/file_validator.rs @@ -122,9 +122,8 @@ mod tests { } #[test] - fn test_tilde_without_slash_not_expanded() { - // ~user form is not expanded, treated literally - let result = validate_file_path("~nonexistentuser", is_network_path_test); + fn test_unknown_named_user_path_remains_literal() { + let result = validate_file_path("~dbx-user-that-must-not-exist-7088", is_network_path_test); assert!(result.is_err()); assert!(result.unwrap_err().contains('~')); } diff --git a/crates/dbx-core/src/db/ssh_tunnel.rs b/crates/dbx-core/src/db/ssh_tunnel.rs index d89c6aa202..d13556bdd0 100644 --- a/crates/dbx-core/src/db/ssh_tunnel.rs +++ b/crates/dbx-core/src/db/ssh_tunnel.rs @@ -1913,10 +1913,18 @@ mod tests { assert_eq!(resolve_ssh_agent_socket_path("/tmp/agent.sock"), "/tmp/agent.sock"); } - #[cfg(unix)] + #[cfg(all(unix, not(target_os = "redox")))] #[test] - fn ssh_agent_socket_path_preserves_named_user_path() { - assert_eq!(resolve_ssh_agent_socket_path("~other/.ssh/agent.sock"), "~other/.ssh/agent.sock"); + fn ssh_agent_socket_path_expands_named_user_home() { + let user = nix::unistd::User::from_uid(nix::unistd::getuid()) + .expect("current Unix user lookup should succeed") + .expect("current Unix user should exist in the account database"); + let home = user.dir.into_os_string().into_string().expect("current Unix user home should be valid UTF-8"); + + assert_eq!( + resolve_ssh_agent_socket_path(&format!("~{}/.ssh/agent.sock", user.name)), + format!("{home}/.ssh/agent.sock") + ); } #[test] diff --git a/crates/dbx-core/src/path_utils.rs b/crates/dbx-core/src/path_utils.rs index f5e5f9bdab..a5b40670b7 100644 --- a/crates/dbx-core/src/path_utils.rs +++ b/crates/dbx-core/src/path_utils.rs @@ -1,19 +1,106 @@ pub fn expand_tilde(path: &str) -> String { + expand_tilde_with(path, home_dir, named_user_home) +} + +fn expand_tilde_with( + path: &str, + current_home: impl FnOnce() -> Option, + named_home: impl FnOnce(&str) -> Option, +) -> String { if !path.starts_with('~') { return path.to_string(); } if path.len() == 1 { - return home_dir().unwrap_or_else(|| path.to_string()); + return current_home().unwrap_or_else(|| path.to_string()); } if path.as_bytes().get(1) == Some(&b'/') { - return match home_dir() { + return match current_home() { Some(home) => home + &path[1..], None => path.to_string(), }; } - path.to_string() + + let named_path = &path[1..]; + let (username, suffix) = + named_path.find('/').map_or((named_path, ""), |index| (&named_path[..index], &named_path[index..])); + + named_home(username).map_or_else(|| path.to_string(), |home| home + suffix) } fn home_dir() -> Option { std::env::var("HOME").or_else(|_| std::env::var("USERPROFILE")).ok() } + +#[cfg(all(unix, not(target_os = "redox")))] +fn named_user_home(username: &str) -> Option { + nix::unistd::User::from_name(username).ok().flatten().and_then(|user| user.dir.into_os_string().into_string().ok()) +} + +#[cfg(any(not(unix), target_os = "redox"))] +fn named_user_home(_username: &str) -> Option { + None +} + +#[cfg(test)] +mod tests { + use super::expand_tilde_with; + + fn no_named_user(_username: &str) -> Option { + None + } + + #[test] + fn preserves_paths_without_a_leading_tilde() { + assert_eq!( + expand_tilde_with( + "relative/file", + || panic!("ordinary paths must not resolve the current home"), + |_| panic!("ordinary paths must not resolve a named home") + ), + "relative/file" + ); + assert_eq!( + expand_tilde_with( + "/absolute/file", + || panic!("ordinary paths must not resolve the current home"), + |_| panic!("ordinary paths must not resolve a named home") + ), + "/absolute/file" + ); + } + + #[test] + fn expands_current_user_home_forms() { + assert_eq!(expand_tilde_with("~", || Some("/home/current".into()), no_named_user), "/home/current"); + assert_eq!( + expand_tilde_with( + "~/.ssh/config", + || Some("/home/current".into()), + |_| panic!("current-user paths must not resolve a named home") + ), + "/home/current/.ssh/config" + ); + } + + #[test] + fn preserves_current_user_forms_when_home_is_unavailable() { + assert_eq!(expand_tilde_with("~", || None, no_named_user), "~"); + assert_eq!(expand_tilde_with("~/.ssh/config", || None, no_named_user), "~/.ssh/config"); + } + + #[test] + fn expands_named_user_home_forms() { + let named_home = |username: &str| (username == "alice").then(|| "/Users/alice".to_string()); + + assert_eq!(expand_tilde_with("~alice", || None, named_home), "/Users/alice"); + assert_eq!(expand_tilde_with("~alice/.ssh/agent.sock", || None, named_home), "/Users/alice/.ssh/agent.sock"); + assert_eq!(expand_tilde_with("~alice/", || None, named_home), "/Users/alice/"); + } + + #[test] + fn preserves_unresolved_or_non_posix_named_user_forms() { + assert_eq!(expand_tilde_with("~missing/.ssh/agent.sock", || None, no_named_user), "~missing/.ssh/agent.sock"); + assert_eq!(expand_tilde_with("~alice\\.ssh\\agent.sock", || None, no_named_user), "~alice\\.ssh\\agent.sock"); + assert_eq!(expand_tilde_with("~~/.ssh/agent.sock", || None, no_named_user), "~~/.ssh/agent.sock"); + } +}