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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
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) {
if metadata.is_file() {
let permissions = metadata.permissions();
let mode = permissions.mode();
// Check if executable bit is set (user, group, or other)
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()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_guard_preserves_missing_binary() {
let original = "/usr/local/bin:/home/user/.cargo/bin:/usr/bin";
let modified = "/usr/local/bin:/usr/bin"; // cargo bin removed
// Create a test directory structure (would need actual files for real test)
// For now, test the logic with empty result
let guard = PathGuard::new(&["nonexistent"]);
let result = guard.ensure_protected_paths(original, modified.to_string());
// Should return modified as-is since binary doesn't exist
assert_eq!(result, modified);
}
#[test]
fn test_guard_skips_already_present() {
let original = "/usr/local/bin:/home/user/.cargo/bin";
let modified = "/usr/local/bin:/home/user/.cargo/bin";
let guard = PathGuard::new(&["test"]);
let result = guard.ensure_protected_paths(original, modified.to_string());
// Should not duplicate
assert_eq!(result, modified);
}
#[test]
fn test_guard_appends_to_empty_path() {
// Use nonexistent binaries so detect_protected_paths returns empty
let guard = PathGuard::new(&["nonexistent_binary_xyz123"]);
// If new_path is empty and we had protected paths, they'd be appended
// (though in practice this shouldn't happen)
let result = guard.ensure_protected_paths("/usr/bin", "".to_string());
// Without real files, detect_protected_paths returns empty, so result is ""
assert_eq!(result, "");
}
#[test]
fn test_guard_handles_missing_binary_gracefully() {
// Test that guard doesn't crash when protected binary doesn't exist
let guard = PathGuard::new(&["nonexistent_binary_xyz123"]);
let original = "/usr/local/bin:/usr/bin";
let modified = "/usr/local/bin";
// Should not panic, just return modified path as-is
let result = guard.ensure_protected_paths(original, modified.to_string());
assert_eq!(result, modified);
}
#[test]
fn test_guard_ignores_uninstalled_binaries() {
// Test with mix of real and fake binaries
let guard = PathGuard::new(&["sh", "fake_binary_that_does_not_exist"]);
// sh exists in /bin on most Unix systems
let original = "/bin:/usr/bin";
let modified = "/usr/bin";
// Should preserve /bin (for sh) but not crash on fake binary
let result = guard.ensure_protected_paths(original, modified.to_string());
// Result should contain /usr/bin, and may contain /bin if sh was found there
assert!(result.contains("/usr/bin"));
}
}