1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
use std::path::{Path, PathBuf};
/// Guards critical binaries by ensuring their paths are preserved during `PATH` operations
pub struct PathGuard {
protected_binaries: Vec<String>,
}
impl Default for PathGuard {
fn default() -> Self {
Self {
protected_binaries: vec![
// whi itself and common integrations
"whi".to_string(),
"zoxide".to_string(),
// Critical system commands used by shell integrations
"seq".to_string(), // Fish integration (command lookup)
"uname".to_string(), // Fish prompt functions
"stat".to_string(), // Both shells (file metadata)
"command".to_string(), // Both shells (command checking)
],
}
}
}
impl PathGuard {
/// Create guard with custom protected binaries
#[must_use]
pub fn new(binaries: &[&str]) -> Self {
Self {
protected_binaries: binaries.iter().map(|s| (*s).to_string()).collect(),
}
}
/// Ensure protected binary paths from `original_path` are preserved in `new_path`
///
/// Silently appends missing protected paths to the end of `new_path`
#[must_use]
pub fn ensure_protected_paths(&self, original_path: &str, new_path: String) -> String {
let protected_dirs = self.detect_protected_paths(original_path);
if protected_dirs.is_empty() {
return new_path;
}
let new_entries: Vec<&str> = new_path.split(':').filter(|s| !s.is_empty()).collect();
let mut result = new_path.clone();
for dir in protected_dirs {
let dir_str = dir.to_string_lossy();
// Check if this directory is already in new_path
if !new_entries.iter().any(|&entry| entry == dir_str.as_ref()) {
// Append at the end to minimize disruption
if !result.is_empty() && !result.ends_with(':') {
result.push(':');
}
result.push_str(&dir_str);
}
}
result
}
/// Find directories containing protected binaries in current `PATH`
///
/// Silently ignores binaries that are not found - no crashes if binary doesn't exist
fn detect_protected_paths(&self, current_path: &str) -> Vec<PathBuf> {
use std::collections::HashSet;
let protected_dirs: HashSet<PathBuf> = self
.protected_binaries
.iter()
.filter_map(|binary| Self::find_binary_dir(current_path, binary))
.collect();
protected_dirs.into_iter().collect()
}
/// Find the winning (first) directory for a binary in `PATH`
fn find_binary_dir(path_str: &str, binary_name: &str) -> Option<PathBuf> {
for dir in path_str.split(':') {
if dir.is_empty() {
continue;
}
let dir_path = PathBuf::from(dir);
let exe_path = dir_path.join(binary_name);
if Self::is_executable(&exe_path) {
return Some(dir_path);
}
}
None
}
/// Check if a file is executable
#[cfg(unix)]
fn is_executable(path: &Path) -> bool {
use std::fs;
use std::os::unix::fs::PermissionsExt;
if let Ok(metadata) = fs::metadata(path)
&& metadata.is_file()
{
let permissions = metadata.permissions();
let mode = permissions.mode();
return (mode & 0o111) != 0;
}
false
}
#[cfg(not(unix))]
fn is_executable(path: &Path) -> bool {
// On non-Unix, just check if file exists
path.is_file()
}
}