use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
use crate::error;
pub(crate) use crate::sys::stubs::fs::MetadataExt;
static PATHEXT_EXTENSIONS: LazyLock<Vec<String>> = LazyLock::new(|| {
std::env::var("PATHEXT")
.unwrap_or_else(|_| ".COM;.EXE;.BAT;.CMD".to_string())
.split(';')
.filter(|s| !s.is_empty())
.map(|s| s.to_ascii_lowercase())
.collect()
});
fn pathext_entry_stem(entry: &str) -> &str {
entry.strip_prefix('.').unwrap_or(entry)
}
fn has_executable_extension(path: &Path) -> bool {
path.extension().is_some_and(|ext| {
PATHEXT_EXTENSIONS
.iter()
.any(|e| ext.eq_ignore_ascii_case(pathext_entry_stem(e)))
})
}
fn is_executable_file(path: &Path) -> bool {
has_executable_extension(path) && path.is_file()
}
pub fn resolve_executable(path: PathBuf) -> Option<PathBuf> {
if is_executable_file(&path) {
return Some(path);
}
for ext in PATHEXT_EXTENSIONS.iter() {
let mut name = path.as_os_str().to_owned();
name.push(ext);
let candidate = PathBuf::from(name);
if candidate.is_file() {
return Some(candidate);
}
}
None
}
impl crate::sys::fs::PathExt for Path {
fn readable(&self) -> bool {
self.exists()
}
fn writable(&self) -> bool {
self.metadata().is_ok_and(|m| !m.permissions().readonly())
}
fn executable(&self) -> bool {
if is_executable_file(self) {
return true;
}
PATHEXT_EXTENSIONS.iter().any(|ext| {
let mut name = self.as_os_str().to_owned();
name.push(ext);
Self::new(&name).is_file()
})
}
fn exists_and_is_block_device(&self) -> bool {
false
}
fn exists_and_is_char_device(&self) -> bool {
false
}
fn exists_and_is_fifo(&self) -> bool {
false
}
fn exists_and_is_socket(&self) -> bool {
false
}
fn exists_and_is_setgid(&self) -> bool {
false
}
fn exists_and_is_setuid(&self) -> bool {
false
}
fn exists_and_is_sticky_bit(&self) -> bool {
false
}
fn get_device_and_inode(&self) -> Result<(u64, u64), crate::error::Error> {
Err(error::ErrorKind::NotSupportedOnThisPlatform("get_device_and_inode").into())
}
}
pub fn split_paths<T: AsRef<OsStr> + ?Sized>(s: &T) -> std::env::SplitPaths<'_> {
std::env::split_paths(s)
}
pub fn open_null_file() -> Result<std::fs::File, error::Error> {
let f = std::fs::File::options()
.read(true)
.write(true)
.open("NUL")?;
Ok(f)
}
pub fn try_open_special_file(path: &Path) -> Option<Result<std::fs::File, std::io::Error>> {
if path.ends_with("dev/null") && path.is_absolute() {
Some(open_null_file().map_err(std::io::Error::other))
} else {
None
}
}
pub(crate) fn get_default_executable_search_paths() -> Vec<PathBuf> {
default_system_paths()
}
pub fn get_default_standard_utils_paths() -> Vec<PathBuf> {
default_system_paths()
}
fn default_system_paths() -> Vec<PathBuf> {
let mut paths = Vec::new();
if let Ok(sysroot) = std::env::var("SystemRoot") {
paths.push(PathBuf::from(&sysroot).join("system32"));
paths.push(PathBuf::from(&sysroot));
paths.push(PathBuf::from(&sysroot).join("System32").join("Wbem"));
paths.push(
PathBuf::from(&sysroot)
.join("System32")
.join("WindowsPowerShell")
.join("v1.0"),
);
}
if let Ok(userprofile) = std::env::var("USERPROFILE") {
paths.push(
PathBuf::from(userprofile)
.join("AppData")
.join("Local")
.join("Microsoft")
.join("WindowsApps"),
);
}
paths
}
pub const fn get_system_profile_path() -> Option<&'static Path> {
None
}
pub const fn get_system_rc_path() -> Option<&'static Path> {
None
}
pub const fn default_case_insensitive_path_expansion() -> bool {
true
}
const PATH_SEPARATORS: [char; 2] = ['/', '\\'];
pub fn contains_path_separator(s: &str) -> bool {
s.contains(PATH_SEPARATORS)
}
pub fn ends_with_path_separator(s: &str) -> bool {
s.ends_with(PATH_SEPARATORS)
}
pub fn strip_path_separator_suffix(s: &str) -> &str {
s.strip_suffix(PATH_SEPARATORS).unwrap_or(s)
}
pub fn rfind_path_separator(s: &str) -> Option<usize> {
s.rfind(PATH_SEPARATORS)
}
pub fn split_path_for_pattern(s: &str) -> impl Iterator<Item = &str> {
s.split(PATH_SEPARATORS)
}
pub fn pattern_path_root(first_component: &str) -> Option<PathBuf> {
if first_component.is_empty() {
Some(PathBuf::from("/"))
} else if first_component.len() == 2
&& first_component.as_bytes()[0].is_ascii_alphabetic()
&& first_component.as_bytes()[1] == b':'
{
let mut root = String::with_capacity(3);
root.push_str(first_component);
root.push('/');
Some(PathBuf::from(root))
} else {
None
}
}
pub fn push_path_for_pattern(path: &mut PathBuf, component: &str) {
let bytes = path.as_os_str().as_encoded_bytes();
let needs_sep = !bytes.is_empty() && !matches!(bytes.last(), Some(b'/' | b'\\'));
let buf = path.as_mut_os_string();
if needs_sep {
buf.push("/");
}
buf.push(component);
}
pub fn normalize_path_separators(s: &str) -> std::borrow::Cow<'_, str> {
if s.contains('\\') {
std::borrow::Cow::Owned(s.replace('\\', "/"))
} else {
std::borrow::Cow::Borrowed(s)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn path_separator_helpers_both_slashes() {
assert!(contains_path_separator("foo/bar"));
assert!(contains_path_separator(r"foo\bar"));
assert!(contains_path_separator(r"mixed/and\back"));
assert!(!contains_path_separator("foobar"));
assert!(ends_with_path_separator("foo/"));
assert!(ends_with_path_separator(r"foo\"));
assert!(!ends_with_path_separator("foo"));
assert_eq!(strip_path_separator_suffix("foo/"), "foo");
assert_eq!(strip_path_separator_suffix(r"foo\"), "foo");
assert_eq!(strip_path_separator_suffix("foo"), "foo");
assert_eq!(rfind_path_separator("a/b/c"), Some(3));
assert_eq!(rfind_path_separator(r"a\b\c"), Some(3));
assert_eq!(rfind_path_separator(r"a/b\c"), Some(3));
assert_eq!(rfind_path_separator("abc"), None);
}
#[test]
fn split_path_for_pattern_both_slashes() {
let parts: Vec<_> = split_path_for_pattern("a/b/c").collect();
assert_eq!(parts, vec!["a", "b", "c"]);
let parts: Vec<_> = split_path_for_pattern(r"a\b\c").collect();
assert_eq!(parts, vec!["a", "b", "c"]);
let parts: Vec<_> = split_path_for_pattern(r"a/b\c").collect();
assert_eq!(parts, vec!["a", "b", "c"]);
let parts: Vec<_> = split_path_for_pattern("/a/b").collect();
assert_eq!(parts, vec!["", "a", "b"]);
}
#[test]
fn pattern_path_root_leading_separator() {
assert_eq!(pattern_path_root(""), Some(PathBuf::from("/")));
}
#[test]
fn pattern_path_root_drive_letters() {
assert_eq!(pattern_path_root("c:"), Some(PathBuf::from("c:/")));
assert_eq!(pattern_path_root("C:"), Some(PathBuf::from("C:/")));
assert_eq!(pattern_path_root("Z:"), Some(PathBuf::from("Z:/")));
}
#[test]
fn pattern_path_root_rejects_non_drive_two_char_prefix() {
assert_eq!(pattern_path_root("1:"), None);
assert_eq!(pattern_path_root("cd"), None);
assert_eq!(pattern_path_root("c:\\"), None);
assert_eq!(pattern_path_root("foo"), None);
}
#[test]
fn push_path_for_pattern_appends_with_forward_slash() {
let mut p = PathBuf::from(r"C:\Users\reuben");
push_path_for_pattern(&mut p, "foo");
assert_eq!(p, PathBuf::from(r"C:\Users\reuben/foo"));
}
#[test]
fn push_path_for_pattern_no_double_separator() {
let mut p = PathBuf::from("C:/Users/reuben/");
push_path_for_pattern(&mut p, "foo");
assert_eq!(p, PathBuf::from("C:/Users/reuben/foo"));
let mut p = PathBuf::from(r"C:\Users\reuben\");
push_path_for_pattern(&mut p, "foo");
assert_eq!(p, PathBuf::from(r"C:\Users\reuben\foo"));
}
#[test]
fn push_path_for_pattern_onto_drive_root() {
let mut p = PathBuf::from("c:/");
push_path_for_pattern(&mut p, "foo");
assert_eq!(p, PathBuf::from("c:/foo"));
}
#[test]
fn push_path_for_pattern_onto_empty() {
let mut p = PathBuf::new();
push_path_for_pattern(&mut p, "foo");
assert_eq!(p, PathBuf::from("foo"));
}
#[test]
fn normalize_path_separators_converts_backslashes() {
use std::borrow::Cow;
assert!(matches!(
normalize_path_separators("c:/foo/bar"),
Cow::Borrowed("c:/foo/bar")
));
let normalized = normalize_path_separators(r"c:\foo\bar");
assert_eq!(normalized.as_ref(), "c:/foo/bar");
let normalized = normalize_path_separators(r"c:\foo/bar");
assert_eq!(normalized.as_ref(), "c:/foo/bar");
}
#[test]
fn default_case_insensitive_is_true() {
assert!(default_case_insensitive_path_expansion());
}
#[test]
fn has_executable_extension_is_case_insensitive() {
assert!(has_executable_extension(Path::new("foo.exe")));
assert!(has_executable_extension(Path::new("foo.EXE")));
assert!(has_executable_extension(Path::new("foo.Cmd")));
assert!(!has_executable_extension(Path::new("foo.txt")));
assert!(!has_executable_extension(Path::new("foo")));
}
#[test]
fn pathext_entry_stem_strips_dot() {
assert_eq!(pathext_entry_stem(".exe"), "exe");
assert_eq!(pathext_entry_stem(".cmd"), "cmd");
assert_eq!(pathext_entry_stem("exe"), "exe");
assert_eq!(pathext_entry_stem(""), "");
}
#[test]
fn resolve_executable_for_nonexistent_returns_none() {
let path = PathBuf::from(r"C:\__brush_test_definitely_missing__");
assert!(resolve_executable(path).is_none());
}
}