Skip to content

Commit 2ac76d5

Browse files
authored
fix: tighten Poetry cache path matching (#417)
Summary: - Match Poetry cache paths by ordered path components instead of raw substrings. - Preserve support for Linux/macOS cache layouts and the Windows `pypoetry/Cache/virtualenvs` layout. - Add tests for exact components, ordering, mixed case, substring false positives, and invalid Poetry environment names. Validation: - cargo test -p pet-poetry - cargo fmt --all - cargo clippy --all -- -D warnings Fixes #398
1 parent cb0fe69 commit 2ac76d5

1 file changed

Lines changed: 118 additions & 6 deletions

File tree

crates/pet-poetry/src/lib.rs

Lines changed: 118 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,11 @@ fn is_poetry_cache_environment(path: &Path) -> bool {
4545
// - Linux: ~/.cache/pypoetry/virtualenvs/
4646
// - macOS: ~/Library/Caches/pypoetry/virtualenvs/
4747
// - Windows: %LOCALAPPDATA%\pypoetry\Cache\virtualenvs\
48-
let path_str = path.to_str().unwrap_or_default();
49-
50-
// Check if path contains typical Poetry cache directory structure
51-
if path_str.contains("pypoetry") && path_str.contains("virtualenvs") {
48+
if has_poetry_cache_components(path) {
5249
// Further validate by checking if the directory name matches Poetry's naming pattern
53-
// Pattern: {name}-{8-char-hash}-py or just .venv
50+
// Pattern: {name}-{8-char-hash}-py{version}
5451
if let Some(dir_name) = path.file_name().and_then(|n| n.to_str()) {
55-
// Check for Poetry's hash-based naming: name-XXXXXXXX-py
52+
// Check for Poetry's hash-based naming: name-XXXXXXXX-py<major>.<minor>
5653
// The hash is 8 characters of base64url encoding
5754
if POETRY_ENV_NAME_PATTERN.is_match(dir_name) {
5855
return true;
@@ -63,6 +60,27 @@ fn is_poetry_cache_environment(path: &Path) -> bool {
6360
false
6461
}
6562

63+
fn has_poetry_cache_components(path: &Path) -> bool {
64+
let mut found_pypoetry = false;
65+
66+
for component in path.components() {
67+
let Some(component) = component.as_os_str().to_str() else {
68+
return false;
69+
};
70+
71+
if component.eq_ignore_ascii_case("pypoetry") {
72+
found_pypoetry = true;
73+
continue;
74+
}
75+
76+
if found_pypoetry && component.eq_ignore_ascii_case("virtualenvs") {
77+
return true;
78+
}
79+
}
80+
81+
false
82+
}
83+
6684
/// Check if a .venv directory is an in-project Poetry environment
6785
/// This is for the case when virtualenvs.in-project = true is set.
6886
/// We check if the parent directory has Poetry configuration files.
@@ -359,6 +377,100 @@ mod tests {
359377
use super::*;
360378
use pet_core::os_environment::EnvironmentApi;
361379

380+
fn path_from_components(components: &[&str]) -> PathBuf {
381+
let mut path = PathBuf::new();
382+
for component in components {
383+
path.push(component);
384+
}
385+
path
386+
}
387+
388+
#[test]
389+
fn test_poetry_cache_environment_requires_exact_cache_components() {
390+
let path = path_from_components(&[
391+
"home",
392+
"user",
393+
".cache",
394+
"pypoetry",
395+
"virtualenvs",
396+
"project-1a2b3c4d-py3.11",
397+
]);
398+
399+
assert!(is_poetry_cache_environment(&path));
400+
}
401+
402+
#[test]
403+
fn test_poetry_cache_environment_allows_windows_cache_component() {
404+
let path = path_from_components(&[
405+
"Users",
406+
"user",
407+
"AppData",
408+
"Local",
409+
"pypoetry",
410+
"Cache",
411+
"virtualenvs",
412+
"project-1a2b3c4d-py3.11",
413+
]);
414+
415+
assert!(is_poetry_cache_environment(&path));
416+
}
417+
418+
#[test]
419+
fn test_poetry_cache_environment_rejects_substring_cache_components() {
420+
let path = path_from_components(&[
421+
"Users",
422+
"pypoetry_user",
423+
"virtualenvs_backup",
424+
"project-1a2b3c4d-py3.11",
425+
]);
426+
427+
assert!(!is_poetry_cache_environment(&path));
428+
}
429+
430+
#[test]
431+
fn test_poetry_cache_environment_requires_ordered_cache_components() {
432+
let path = path_from_components(&[
433+
"home",
434+
"user",
435+
".cache",
436+
"virtualenvs",
437+
"pypoetry",
438+
"project-1a2b3c4d-py3.11",
439+
]);
440+
441+
assert!(!is_poetry_cache_environment(&path));
442+
}
443+
444+
#[test]
445+
fn test_poetry_cache_environment_allows_mixed_case_cache_components() {
446+
let path = path_from_components(&[
447+
"Users",
448+
"user",
449+
"AppData",
450+
"Local",
451+
"PyPoetry",
452+
"Cache",
453+
"VirtualEnvs",
454+
"project-1a2b3c4d-py3.11",
455+
]);
456+
457+
assert!(is_poetry_cache_environment(&path));
458+
}
459+
460+
#[test]
461+
fn test_poetry_cache_environment_requires_poetry_env_name() {
462+
let path = path_from_components(&[
463+
"home",
464+
"user",
465+
".cache",
466+
"pypoetry",
467+
"virtualenvs",
468+
"not-a-poetry-env",
469+
]);
470+
471+
assert!(!is_poetry_cache_environment(&path));
472+
}
473+
362474
#[test]
363475
fn test_sync_search_result_from_replaces_cached_result() {
364476
let environment = EnvironmentApi::new();

0 commit comments

Comments
 (0)