fix(core): support named-user tilde paths

This commit is contained in:
Guoyu Su authored and GitHub committed 2026-08-25 23:57:02 +08:00
1 parent 1ed20e424e
commit a9c6c7b5e4
6 files changed
+108 -9

No files matched your search

+3
View File
@@ -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"] }
+2 -3
View File
@@ -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('~'));
}
+11 -3
View File
@@ -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]
+90 -3
View File
@@ -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<String>,
named_home: impl FnOnce(&str) -> Option<String>,
) -> 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<String> {
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<String> {
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<String> {
None
}
#[cfg(test)]
mod tests {
use super::expand_tilde_with;
fn no_named_user(_username: &str) -> Option<String> {
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");
}
}