Skip to content

Commit ea5b499

Browse files
committed
fix: reject unexpanded tilde paths in HATCH_DATA_DIR / dirs.env.virtual (PR #460)
1 parent b716320 commit ea5b499

1 file changed

Lines changed: 61 additions & 0 deletions

File tree

crates/pet-hatch/src/lib.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,13 @@ fn get_default_virtual_dir(environment: &dyn Environment) -> Option<PathBuf> {
273273
// `HATCH_DATA_DIR=~/.local/share/hatch` resolves to the user
274274
// home rather than a literal `~` directory.
275275
let expanded = expand_path(PathBuf::from(trimmed));
276+
// If the home directory is unavailable, `expand_path()` returns
277+
// the input verbatim. Don't normalize a leading `~` into a
278+
// literal directory under cwd — bail out so Hatch envs are not
279+
// attributed to a bogus path.
280+
if path_starts_with_tilde(&expanded) {
281+
return None;
282+
}
276283
return Some(norm_case(append_virtual_subdir(expanded)));
277284
}
278285
}
@@ -289,6 +296,14 @@ fn append_virtual_subdir(data_dir: PathBuf) -> PathBuf {
289296
path
290297
}
291298

299+
/// Returns true if `path` still begins with a literal `~`, indicating that
300+
/// `expand_path()` could not resolve the user's home directory (no HOME /
301+
/// USERPROFILE set). Such paths must not be normalized or joined against
302+
/// the workspace root, since `~` was not the user's intended directory.
303+
fn path_starts_with_tilde(path: &Path) -> bool {
304+
path.to_str().is_some_and(|s| s.starts_with('~'))
305+
}
306+
292307
/// Platform default for Hatch's data directory.
293308
///
294309
/// Mirrors `platformdirs.user_data_dir("hatch", appauthor=False)`.
@@ -470,6 +485,12 @@ fn resolve_virtual_paths_against_workspace(workspace: &Path, raw: Vec<String>) -
470485
// "~/.virtualenvs" resolve to the user home rather than being
471486
// joined onto the workspace as a relative path.
472487
let expanded = expand_path(PathBuf::from(trimmed));
488+
// If the home directory is unavailable, `expand_path()` returns
489+
// the input verbatim. Skip such entries rather than joining a
490+
// literal `~` onto the workspace root (e.g. `<workspace>/~/...`).
491+
if path_starts_with_tilde(&expanded) {
492+
continue;
493+
}
473494
let resolved = if expanded.is_absolute() {
474495
expanded
475496
} else {
@@ -1031,6 +1052,46 @@ mod tests {
10311052
assert_eq!(dirs, vec![norm_case(&virtualenvs)]);
10321053
}
10331054

1055+
#[test]
1056+
fn resolve_project_virtual_dirs_skips_unexpanded_tilde() {
1057+
// If HOME / USERPROFILE are unset, `expand_path("~/.virtualenvs")`
1058+
// returns the input verbatim. We must not join `~` onto the
1059+
// workspace root (yielding `<workspace>/~/.virtualenvs`) or pass
1060+
// a tilde-prefixed path through `norm_case()` — both would
1061+
// misclassify unrelated envs.
1062+
let _guard = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
1063+
1064+
let temp = TempDir::new().unwrap();
1065+
let project = temp.path().join("proj");
1066+
fs::create_dir_all(&project).unwrap();
1067+
fs::write(
1068+
project.join("pyproject.toml"),
1069+
b"[tool.hatch.dirs.env]\nvirtual = \"~/.virtualenvs\"\n",
1070+
)
1071+
.unwrap();
1072+
1073+
let prev_home = std::env::var_os("HOME");
1074+
let prev_user_profile = std::env::var_os("USERPROFILE");
1075+
std::env::remove_var("HOME");
1076+
std::env::remove_var("USERPROFILE");
1077+
1078+
let dirs = resolve_project_virtual_dirs(&project);
1079+
1080+
match prev_home {
1081+
Some(v) => std::env::set_var("HOME", v),
1082+
None => std::env::remove_var("HOME"),
1083+
}
1084+
match prev_user_profile {
1085+
Some(v) => std::env::set_var("USERPROFILE", v),
1086+
None => std::env::remove_var("USERPROFILE"),
1087+
}
1088+
1089+
assert!(
1090+
dirs.is_empty(),
1091+
"unexpanded tilde paths must not be claimed: got {dirs:?}"
1092+
);
1093+
}
1094+
10341095
#[test]
10351096
fn configure_caches_workspace_virtual_dirs() {
10361097
// try_from() must not re-read pyproject.toml on every call; configure()

0 commit comments

Comments
 (0)