fish-shell/src/path.rs

855 lines
28 KiB
Rust
Raw Normal View History

2023-04-09 20:33:20 +08:00
//! Directory utilities. This library contains functions for locating configuration directories,
//! for testing if a command with a given name can be found in the PATH, and various other
//! path-related issues.
use crate::common::{is_windows_subsystem_for_linux as is_wsl, wcs2zstring, WSL};
2023-04-09 20:33:20 +08:00
use crate::env::{EnvMode, EnvStack, Environment};
use crate::expand::{expand_tilde, HOME_DIRECTORY};
use crate::flog::{FLOG, FLOGF};
#[cfg(not(target_os = "linux"))]
use crate::libc::{MNT_LOCAL, ST_LOCAL};
2023-08-09 06:16:04 +08:00
use crate::wchar::prelude::*;
use crate::wutil::{normalize_path, path_normalize_for_cd, waccess, wdirname, wmkdir, wstat};
2023-04-09 20:33:20 +08:00
use errno::{errno, set_errno, Errno};
use libc::{EACCES, ENOENT, ENOTDIR, F_OK, X_OK};
2023-04-09 20:33:20 +08:00
use once_cell::sync::Lazy;
use std::ffi::OsStr;
2023-12-05 15:44:51 +08:00
use std::io::ErrorKind;
use std::os::unix::prelude::*;
2023-04-09 20:33:20 +08:00
/// Returns the user configuration directory for fish. If the directory or one of its parents
/// doesn't exist, they are first created.
///
/// \param path The directory as an out param
/// Return whether the directory was returned successfully
2023-04-09 20:33:20 +08:00
pub fn path_get_config() -> Option<WString> {
let dir = get_config_directory();
if dir.success() {
Some(dir.path.to_owned())
} else {
None
}
}
/// Returns the user data directory for fish. If the directory or one of its parents doesn't exist,
/// they are first created.
///
/// Volatile files presumed to be local to the machine, such as the fish_history will be stored in this directory.
2023-04-09 20:33:20 +08:00
///
/// \param path The directory as an out param
/// Return whether the directory was returned successfully
2023-04-09 20:33:20 +08:00
pub fn path_get_data() -> Option<WString> {
let dir = get_data_directory();
if dir.success() {
Some(dir.path.to_owned())
} else {
None
}
}
/// Returns the user cache directory for fish. If the directory or one of its parents doesn't exist,
/// they are first created.
///
/// Volatile files presumed to be local to the machine such as all the
/// generated_completions, will be stored in this directory.
///
/// \param path The directory as an out param
/// Return whether the directory was returned successfully
pub fn path_get_cache() -> Option<WString> {
let dir = get_cache_directory();
if dir.success() {
Some(dir.path.to_owned())
} else {
None
}
}
2023-04-09 20:33:20 +08:00
#[derive(Clone, Copy, Eq, PartialEq)]
pub enum DirRemoteness {
/// directory status is unknown
unknown,
/// directory is known local
local,
/// directory is known remote
remote,
}
/// Return the remoteness of the fish data directory.
2023-04-09 20:33:20 +08:00
/// This will be remote for filesystems like NFS, SMB, etc.
pub fn path_get_data_remoteness() -> DirRemoteness {
get_data_directory().remoteness
}
/// Like path_get_data_remoteness but for the config directory.
pub fn path_get_config_remoteness() -> DirRemoteness {
get_config_directory().remoteness
}
/// Emit any errors if config directories are missing.
/// Use the given environment stack to ensure this only occurs once.
pub fn path_emit_config_directory_messages(vars: &EnvStack) {
2023-04-09 20:33:20 +08:00
let data = get_data_directory();
if !data.success() {
maybe_issue_path_warning(
L!("data"),
wgettext!("can not save history"),
2023-04-09 20:33:20 +08:00
data.used_xdg,
L!("XDG_DATA_HOME"),
2023-04-09 20:33:20 +08:00
&data.path,
data.err,
vars,
);
}
if data.remoteness == DirRemoteness::remote {
FLOG!(path, "data path appears to be on a network volume");
}
let config = get_config_directory();
if !config.success() {
maybe_issue_path_warning(
L!("config"),
wgettext!("can not save universal variables or functions"),
2023-04-09 20:33:20 +08:00
config.used_xdg,
L!("XDG_CONFIG_HOME"),
2023-04-09 20:33:20 +08:00
&config.path,
config.err,
vars,
);
}
if config.remoteness == DirRemoteness::remote {
FLOG!(path, "config path appears to be on a network volume");
}
}
/// We separate this from path_create() for two reasons. First it's only caused if there is a
/// problem, and thus is not central to the behavior of that function. Second, we only want to issue
/// the message once. If the current shell starts a new fish shell (e.g., by running `fish -c` from
/// a function) we don't want that subshell to issue the same warnings.
fn maybe_issue_path_warning(
which_dir: &wstr,
custom_error_msg: &wstr,
using_xdg: bool,
xdg_var: &wstr,
path: &wstr,
saved_errno: libc::c_int,
vars: &EnvStack,
2023-04-09 20:33:20 +08:00
) {
let warning_var_name = L!("_FISH_WARNED_").to_owned() + which_dir;
2023-04-09 20:33:20 +08:00
if vars
.getf(&warning_var_name, EnvMode::GLOBAL | EnvMode::EXPORT)
.is_some()
{
return;
}
vars.set_one(
&warning_var_name,
EnvMode::GLOBAL | EnvMode::EXPORT,
L!("1").to_owned(),
2023-04-09 20:33:20 +08:00
);
FLOG!(error, custom_error_msg);
if path.is_empty() {
FLOG!(
warning_path,
wgettext_fmt!("Unable to locate the %ls directory.", which_dir)
);
FLOG!(
warning_path,
wgettext_fmt!(
"Please set the %ls or HOME environment variable before starting fish.",
xdg_var
)
);
} else {
let env_var = if using_xdg { xdg_var } else { L!("HOME") };
2023-04-09 20:33:20 +08:00
FLOG!(
warning_path,
wgettext_fmt!(
"Unable to locate %ls directory derived from $%ls: '%ls'.",
which_dir,
env_var,
path
)
);
FLOG!(
warning_path,
wgettext_fmt!("The error was '%s'.", Errno(saved_errno).to_string())
);
FLOG!(
warning_path,
wgettext_fmt!(
"Please set $%ls to a directory where you have write access.",
env_var
)
);
}
2023-12-05 15:44:51 +08:00
printf!("\n");
2023-04-09 20:33:20 +08:00
}
/// Finds the path of an executable named `cmd`, by looking in $PATH taken from `vars`.
/// Returns the path if found, none if not.
2023-04-09 20:33:20 +08:00
pub fn path_get_path(cmd: &wstr, vars: &dyn Environment) -> Option<WString> {
let result = path_try_get_path(cmd, vars);
if result.err.is_some() {
None
} else {
Some(result.path)
}
}
// PREFIX is defined at build time.
pub static DEFAULT_PATH: Lazy<[WString; 3]> = Lazy::new(|| {
2023-04-09 20:33:20 +08:00
[
WString::from_str(env!("PREFIX")) + L!("/bin"),
L!("/usr/bin").to_owned(),
L!("/bin").to_owned(),
2023-04-09 20:33:20 +08:00
]
});
/// Finds the path of an executable named `cmd`, by looking in $PATH taken from `vars`.
2023-04-09 20:33:20 +08:00
/// On success, err will be 0 and the path is returned.
/// On failure, we return the "best path" with err set appropriately.
/// For example, if we find a non-executable file, we will return its path and EACCESS.
/// If no candidate path is found, path will be empty and err will be set to ENOENT.
/// Possible err values are taken from access().
pub struct GetPathResult {
pub err: Option<Errno>,
pub path: WString,
2023-04-09 20:33:20 +08:00
}
impl GetPathResult {
fn new(err: Option<Errno>, path: WString) -> Self {
Self { err, path }
}
}
pub fn path_try_get_path(cmd: &wstr, vars: &dyn Environment) -> GetPathResult {
if let Some(path) = vars.get(L!("PATH")) {
path_get_path_core(cmd, path.as_list())
} else {
path_get_path_core(cmd, &*DEFAULT_PATH)
}
}
fn path_check_executable(path: &wstr) -> Result<(), std::io::Error> {
if waccess(path, X_OK) != 0 {
return Err(std::io::Error::last_os_error());
}
let buff = wstat(path)?;
if buff.file_type().is_file() {
Ok(())
} else {
Err(ErrorKind::PermissionDenied.into())
2023-04-09 20:33:20 +08:00
}
}
/// Return all the paths that match the given command.
pub fn path_get_paths(cmd: &wstr, vars: &dyn Environment) -> Vec<WString> {
FLOGF!(path, "path_get_paths('%ls')", cmd);
let mut paths = vec![];
// If the command has a slash, it must be an absolute or relative path and thus we don't bother
// looking for matching commands in the PATH var.
if cmd.contains('/') && path_check_executable(cmd).is_ok() {
2023-04-09 20:33:20 +08:00
paths.push(cmd.to_owned());
return paths;
}
let Some(path_var) = vars.get(L!("PATH")) else {
return paths;
};
2023-04-09 20:33:20 +08:00
for path in path_var.as_list() {
if path.is_empty() {
continue;
}
let mut path = path.clone();
append_path_component(&mut path, cmd);
if path_check_executable(&path).is_ok() {
2023-04-09 20:33:20 +08:00
paths.push(path);
}
}
paths
}
fn path_get_path_core<S: AsRef<wstr>>(cmd: &wstr, pathsv: &[S]) -> GetPathResult {
let noent_res = GetPathResult::new(Some(Errno(ENOENT)), WString::new());
// Test if the given path can be executed.
// Return 0 on success, an errno value on failure.
2023-04-09 20:33:20 +08:00
let test_path = |path: &wstr| -> Result<(), Errno> {
let narrow = wcs2zstring(path);
if unsafe { libc::access(narrow.as_ptr(), X_OK) } != 0 {
return Err(errno());
}
let narrow: Vec<u8> = narrow.into();
let Ok(md) = std::fs::metadata(OsStr::from_bytes(&narrow)) else {
return Err(errno());
};
if md.is_file() {
Ok(())
} else {
Err(Errno(EACCES))
}
};
if cmd.is_empty() {
return noent_res;
}
// Commands cannot contain NUL byte.
if cmd.contains('\0') {
return noent_res;
}
// If the command has a slash, it must be an absolute or relative path and thus we don't bother
// looking for a matching command.
if cmd.contains('/') {
return GetPathResult::new(test_path(cmd).err(), cmd.to_owned());
}
Work around $PATH issues under WSL (#10506) A common complaint has been the massive amount of directories Windows appends to $PATH slowing down fish when it attempts to find a non-existent binary (which it does a lot more often than someone not in the know might think). The typical workaround suggested is to trim unneeded entries from $PATH, but this a) has considerable friction, b) breaks resolution of Windows binaries (you can no longer use `clip.exe`, `cmd.exe`, etc). This patch introduces a two-PATH workaround. If the cmd we are executing does not contain a period (i.e. has no extension) it by definition cannot be a Windows executable. In this case, we skip searching for it in any of the auto-mounted, auto-PATH-appended directories like `/mnt/c/Windows/` or `/mnt/c/Program Files`, but we *do* include those directories if what we're searching for could be a Windows executable. (For now, instead of hard-coding a list of known Windows executable extensions like .bat, .cmd, .exe, etc, we just depend on the presence of an extension at all). e.g. this is what starting up fish prints with logging enabled (that has been removed): bypassing 100 dirs for lookup of kill bypassing 100 dirs for lookup of zoxide bypassing 100 dirs for lookup of zoxide bypassing 100 dirs for lookup of fd not bypassing dirs for lookup of open.exe not bypassing dirs for lookup of git.exe This has resulted in a massive speedup of common fish functions, especially anywhere we internally use or perform the equivalent of `if command -q foo`. Note that the `is_windows_subsystem_for_linux()` check will need to be patched to extend this workaround to WSLv2, but I'll do that separately. Under WSL: * Benchmark `external_cmds` improves by 10% * Benchmark `load_completions` improves by an incredible 77%
2024-05-20 23:29:32 +08:00
// WSLv1/WSLv2 tack on the entire Windows PATH to the end of the PATH environment variable, and
// accessing these paths from WSL binaries is pathalogically slow. We also don't expect to find
// any "normal" nix binaries under these paths, so we can skip them unless we are executing bins
// with Windows-ish names. We try to keep paths manually added to $fish_user_paths by only
// chopping off entries after the last "normal" PATH entry.
let pathsv = if is_wsl(WSL::Any) && !cmd.contains('.') {
Work around $PATH issues under WSL (#10506) A common complaint has been the massive amount of directories Windows appends to $PATH slowing down fish when it attempts to find a non-existent binary (which it does a lot more often than someone not in the know might think). The typical workaround suggested is to trim unneeded entries from $PATH, but this a) has considerable friction, b) breaks resolution of Windows binaries (you can no longer use `clip.exe`, `cmd.exe`, etc). This patch introduces a two-PATH workaround. If the cmd we are executing does not contain a period (i.e. has no extension) it by definition cannot be a Windows executable. In this case, we skip searching for it in any of the auto-mounted, auto-PATH-appended directories like `/mnt/c/Windows/` or `/mnt/c/Program Files`, but we *do* include those directories if what we're searching for could be a Windows executable. (For now, instead of hard-coding a list of known Windows executable extensions like .bat, .cmd, .exe, etc, we just depend on the presence of an extension at all). e.g. this is what starting up fish prints with logging enabled (that has been removed): bypassing 100 dirs for lookup of kill bypassing 100 dirs for lookup of zoxide bypassing 100 dirs for lookup of zoxide bypassing 100 dirs for lookup of fd not bypassing dirs for lookup of open.exe not bypassing dirs for lookup of git.exe This has resulted in a massive speedup of common fish functions, especially anywhere we internally use or perform the equivalent of `if command -q foo`. Note that the `is_windows_subsystem_for_linux()` check will need to be patched to extend this workaround to WSLv2, but I'll do that separately. Under WSL: * Benchmark `external_cmds` improves by 10% * Benchmark `load_completions` improves by an incredible 77%
2024-05-20 23:29:32 +08:00
let win_path_count = pathsv
.iter()
.rev()
.take_while(|p| {
let p = p.as_ref();
p.starts_with("/mnt/")
&& p.chars()
.nth("/mnt/x".len())
Work around $PATH issues under WSL (#10506) A common complaint has been the massive amount of directories Windows appends to $PATH slowing down fish when it attempts to find a non-existent binary (which it does a lot more often than someone not in the know might think). The typical workaround suggested is to trim unneeded entries from $PATH, but this a) has considerable friction, b) breaks resolution of Windows binaries (you can no longer use `clip.exe`, `cmd.exe`, etc). This patch introduces a two-PATH workaround. If the cmd we are executing does not contain a period (i.e. has no extension) it by definition cannot be a Windows executable. In this case, we skip searching for it in any of the auto-mounted, auto-PATH-appended directories like `/mnt/c/Windows/` or `/mnt/c/Program Files`, but we *do* include those directories if what we're searching for could be a Windows executable. (For now, instead of hard-coding a list of known Windows executable extensions like .bat, .cmd, .exe, etc, we just depend on the presence of an extension at all). e.g. this is what starting up fish prints with logging enabled (that has been removed): bypassing 100 dirs for lookup of kill bypassing 100 dirs for lookup of zoxide bypassing 100 dirs for lookup of zoxide bypassing 100 dirs for lookup of fd not bypassing dirs for lookup of open.exe not bypassing dirs for lookup of git.exe This has resulted in a massive speedup of common fish functions, especially anywhere we internally use or perform the equivalent of `if command -q foo`. Note that the `is_windows_subsystem_for_linux()` check will need to be patched to extend this workaround to WSLv2, but I'll do that separately. Under WSL: * Benchmark `external_cmds` improves by 10% * Benchmark `load_completions` improves by an incredible 77%
2024-05-20 23:29:32 +08:00
.map(|c| c == '/')
.unwrap_or(false)
})
.count();
&pathsv[..pathsv.len() - win_path_count]
} else {
pathsv
Work around $PATH issues under WSL (#10506) A common complaint has been the massive amount of directories Windows appends to $PATH slowing down fish when it attempts to find a non-existent binary (which it does a lot more often than someone not in the know might think). The typical workaround suggested is to trim unneeded entries from $PATH, but this a) has considerable friction, b) breaks resolution of Windows binaries (you can no longer use `clip.exe`, `cmd.exe`, etc). This patch introduces a two-PATH workaround. If the cmd we are executing does not contain a period (i.e. has no extension) it by definition cannot be a Windows executable. In this case, we skip searching for it in any of the auto-mounted, auto-PATH-appended directories like `/mnt/c/Windows/` or `/mnt/c/Program Files`, but we *do* include those directories if what we're searching for could be a Windows executable. (For now, instead of hard-coding a list of known Windows executable extensions like .bat, .cmd, .exe, etc, we just depend on the presence of an extension at all). e.g. this is what starting up fish prints with logging enabled (that has been removed): bypassing 100 dirs for lookup of kill bypassing 100 dirs for lookup of zoxide bypassing 100 dirs for lookup of zoxide bypassing 100 dirs for lookup of fd not bypassing dirs for lookup of open.exe not bypassing dirs for lookup of git.exe This has resulted in a massive speedup of common fish functions, especially anywhere we internally use or perform the equivalent of `if command -q foo`. Note that the `is_windows_subsystem_for_linux()` check will need to be patched to extend this workaround to WSLv2, but I'll do that separately. Under WSL: * Benchmark `external_cmds` improves by 10% * Benchmark `load_completions` improves by an incredible 77%
2024-05-20 23:29:32 +08:00
};
2023-04-09 20:33:20 +08:00
let mut best = noent_res;
for next_path in pathsv {
let next_path: &wstr = next_path.as_ref();
if next_path.is_empty() {
continue;
}
let mut proposed_path = next_path.to_owned();
append_path_component(&mut proposed_path, cmd);
match test_path(&proposed_path) {
Ok(()) => {
// We found one.
return GetPathResult::new(None, proposed_path);
}
Err(err) => {
if err.0 != ENOENT && best.err == Some(Errno(ENOENT)) {
// Keep the first *interesting* error and path around.
// ENOENT isn't interesting because not having a file is the normal case.
// Ignore if the parent directory is already inaccessible.
if waccess(wdirname(&proposed_path), X_OK) == 0 {
2023-04-09 20:33:20 +08:00
best = GetPathResult::new(Some(err), proposed_path);
}
}
}
}
}
best
}
/// Returns the full path of the specified directory, using the CDPATH variable as a list of base
/// directories for relative paths.
///
/// If no valid path is found, false is returned and errno is set to ENOTDIR if at least one such
/// path was found, but it did not point to a directory, or ENOENT if no file of the specified
/// name was found.
///
/// \param dir The name of the directory.
/// \param wd The working directory. The working directory must end with a slash.
/// \param vars The environment variables to use (for the CDPATH variable)
/// Return the command, or none() if it could not be found.
2023-04-09 20:33:20 +08:00
pub fn path_get_cdpath(dir: &wstr, wd: &wstr, vars: &dyn Environment) -> Option<WString> {
let mut err = ENOENT;
if dir.is_empty() {
return None;
}
assert!(wd.chars().next_back() == Some('/'));
2023-04-09 20:33:20 +08:00
let paths = path_apply_cdpath(dir, wd, vars);
for a_dir in paths {
if let Ok(md) = wstat(&a_dir) {
2023-04-09 20:33:20 +08:00
if md.is_dir() {
return Some(a_dir);
}
err = ENOTDIR;
}
}
set_errno(Errno(err));
None
}
/// Returns the given directory with all CDPATH components applied.
pub fn path_apply_cdpath(dir: &wstr, wd: &wstr, env_vars: &dyn Environment) -> Vec<WString> {
let mut paths = vec![];
if dir.chars().next() == Some('/') {
// Absolute path.
paths.push(dir.to_owned());
} else if dir.starts_with(L!("./"))
|| dir.starts_with(L!("../"))
|| [L!("."), L!("..")].contains(&dir)
{
2023-04-09 20:33:20 +08:00
// Path is relative to the working directory.
paths.push(path_normalize_for_cd(wd, dir));
} else {
// Respect CDPATH.
let mut cdpathsv = vec![];
if let Some(cdpaths) = env_vars.get(L!("CDPATH")) {
2023-04-09 20:33:20 +08:00
cdpathsv = cdpaths.as_list().to_vec();
}
// Always append $PWD
cdpathsv.push(L!(".").to_owned());
2023-04-09 20:33:20 +08:00
for path in cdpathsv {
let mut abspath = WString::new();
// We want to return an absolute path (see issue 6220)
if ![Some('/'), Some('~')].contains(&path.chars().next()) {
abspath = wd.to_owned();
abspath.push('/');
}
abspath.push_utfstr(&path);
expand_tilde(&mut abspath, env_vars);
if abspath.is_empty() {
continue;
}
abspath = normalize_path(&abspath, true);
let mut whole_path = abspath;
append_path_component(&mut whole_path, dir);
paths.push(whole_path);
}
}
paths
}
/// Returns the path resolved as an implicit cd command, or none() if none. This requires it to
/// start with one of the allowed prefixes (., .., ~) and resolve to a directory.
pub fn path_as_implicit_cd(path: &wstr, wd: &wstr, vars: &dyn Environment) -> Option<WString> {
let mut exp_path = path.to_owned();
expand_tilde(&mut exp_path, vars);
if exp_path.starts_with(L!("/"))
|| exp_path.starts_with(L!("./"))
|| exp_path.starts_with(L!("../"))
|| exp_path.ends_with(L!("/"))
|| exp_path == ".."
2023-04-09 20:33:20 +08:00
{
// These paths can be implicit cd, so see if you cd to the path. Note that a single period
// cannot (that's used for sourcing files anyways).
return path_get_cdpath(&exp_path, wd, vars);
}
None
}
/// Remove double slashes and trailing slashes from a path, e.g. transform foo//bar/ into foo/bar.
/// The string is modified in-place.
pub fn path_make_canonical(path: &mut WString) {
let chars: &mut [char] = path.as_char_slice_mut();
// Ignore trailing slashes, unless it's the first character.
let mut len = chars.len();
while len > 1 && chars[len - 1] == '/' {
len -= 1;
}
// Turn runs of slashes into a single slash.
let mut trailing = 0;
let mut prev_was_slash = false;
for leading in 0..len {
let c = chars[leading];
let is_slash = c == '/';
if !prev_was_slash || !is_slash {
// This is either the first slash in a run, or not a slash at all.
chars[trailing] = c;
trailing += 1;
}
prev_was_slash = is_slash;
}
assert!(trailing <= len);
if trailing < len {
path.truncate(trailing);
}
}
/// Check if two paths are equivalent, which means to ignore runs of multiple slashes (or trailing
/// slashes).
pub fn paths_are_equivalent(p1: &wstr, p2: &wstr) -> bool {
let p1 = p1.as_char_slice();
let p2 = p2.as_char_slice();
if p1 == p2 {
return true;
}
// Ignore trailing slashes after the first character.
let mut len1 = p1.len();
let mut len2 = p2.len();
while len1 > 1 && p1[len1 - 1] == '/' {
len1 -= 1
}
while len2 > 1 && p2[len2 - 1] == '/' {
len2 -= 1
}
// Start walking
let mut idx1 = 0;
let mut idx2 = 0;
while idx1 < len1 && idx2 < len2 {
let c1 = p1[idx1];
let c2 = p2[idx2];
// If the characters are different, the strings are not equivalent.
if c1 != c2 {
break;
}
idx1 += 1;
idx2 += 1;
// If the character was a slash, walk forwards until we hit the end of the string, or a
// non-slash. Note the first condition is invariant within the loop.
while c1 == '/' && p1.get(idx1) == Some(&'/') {
idx1 += 1;
}
while c2 == '/' && p2.get(idx2) == Some(&'/') {
idx2 += 1;
}
}
// We matched if we consumed all of the characters in both strings.
idx1 == len1 && idx2 == len2
}
pub fn path_is_valid(path: &wstr, working_directory: &wstr) -> bool {
// Some special paths are always valid.
if path.is_empty() {
false
} else if [L!("."), L!("./")].contains(&path) {
2023-04-09 20:33:20 +08:00
true
} else if [L!(".."), L!("../")].contains(&path) {
!working_directory.is_empty() && working_directory != L!("/")
2023-04-09 20:33:20 +08:00
} else if path.chars().next() != Some('/') {
// Prepend the working directory. Note that we know path is not empty here.
let mut tmp = working_directory.to_owned();
tmp.push_utfstr(path);
waccess(&tmp, F_OK) == 0
} else {
// Simple check.
waccess(path, F_OK) == 0
}
}
/// Returns whether the two paths refer to the same file.
pub fn paths_are_same_file(path1: &wstr, path2: &wstr) -> bool {
if paths_are_equivalent(path1, path2) {
return true;
}
match (wstat(path1), wstat(path2)) {
(Ok(s1), Ok(s2)) => s1.ino() == s2.ino() && s1.dev() == s2.dev(),
2023-04-09 20:33:20 +08:00
_ => false,
}
}
2023-03-06 10:38:41 +08:00
/// If the given path looks like it's relative to the working directory, then prepend that working
/// directory. This operates on unescaped paths only (so a ~ means a literal ~).
pub fn path_apply_working_directory(path: &wstr, working_directory: &wstr) -> WString {
if path.is_empty() || working_directory.is_empty() {
return path.to_owned();
}
// We're going to make sure that if we want to prepend the wd, that the string has no leading
// "/".
let prepend_wd = path.char_at(0) != '/' && path.char_at(0) != HOME_DIRECTORY;
2023-03-06 10:38:41 +08:00
if !prepend_wd {
// No need to prepend the wd, so just return the path we were given.
return path.to_owned();
}
// Remove up to one "./".
let mut path_component = path.to_owned();
if path_component.starts_with("./") {
path_component.replace_range(0..2, L!(""));
}
// Removing leading /s.
while path_component.starts_with("/") {
path_component.replace_range(0..1, L!(""));
}
// Construct and return a new path.
let mut new_path = working_directory.to_owned();
append_path_component(&mut new_path, &path_component);
new_path
}
2023-04-09 20:33:20 +08:00
/// The following type wraps up a user's "base" directories, corresponding (conceptually if not
/// actually) to XDG spec.
struct BaseDirectory {
/// the path where we attempted to create the directory.
path: WString,
/// whether the dir is remote
remoteness: DirRemoteness,
/// the error code if creating the directory failed, or 0 on success.
err: libc::c_int,
/// whether an XDG variable was used in resolving the directory.
used_xdg: bool,
}
impl BaseDirectory {
fn success(&self) -> bool {
self.err == 0
}
}
/// Attempt to get a base directory, creating it if necessary. If a variable named `xdg_var` is
/// set, use that directory; otherwise use the path `non_xdg_homepath` rooted in $HOME. Return the
2023-04-09 20:33:20 +08:00
/// result; see the base_directory_t fields.
#[cfg_attr(test, allow(unused_variables), allow(unreachable_code))]
2023-04-09 20:33:20 +08:00
fn make_base_directory(xdg_var: &wstr, non_xdg_homepath: &wstr) -> BaseDirectory {
#[cfg(test)]
// If running under `cargo test`, contain ourselves to the build directory and do not try to use
// the actual $HOME or $XDG_XXX directories. This prevents the tests from failing and/or stops
// the tests polluting the user's actual $HOME if a sandbox environment has not been set up.
{
use crate::common::str2wcstring;
use std::path::PathBuf;
let mut build_dir = PathBuf::from(env!("FISH_BUILD_DIR"));
build_dir.push("fish_root");
let err = match std::fs::create_dir_all(&build_dir) {
Ok(_) => 0,
Err(e) => e
.raw_os_error()
.expect("Failed to create fish base directory, but it wasn't an OS error!"),
};
return BaseDirectory {
path: str2wcstring(build_dir.as_os_str().as_bytes()),
remoteness: DirRemoteness::unknown,
used_xdg: false,
err,
};
}
2023-04-09 20:33:20 +08:00
// The vars we fetch must be exported. Allowing them to be universal doesn't make sense and
// allowing that creates a lock inversion that deadlocks the shell since we're called before
// uvars are available.
let vars = EnvStack::globals();
let mut path = WString::new();
let used_xdg;
if let Some(xdg_dir) = vars.getf_unless_empty(xdg_var, EnvMode::GLOBAL | EnvMode::EXPORT) {
path = xdg_dir.as_string() + L!("/fish");
2023-04-09 20:33:20 +08:00
used_xdg = true;
} else {
if let Some(home) = vars.getf_unless_empty(L!("HOME"), EnvMode::GLOBAL | EnvMode::EXPORT) {
2023-04-09 20:33:20 +08:00
path = home.as_string() + non_xdg_homepath;
}
used_xdg = false;
}
set_errno(Errno(0));
let err;
let mut remoteness = DirRemoteness::unknown;
if path.is_empty() {
err = ENOENT;
} else if !create_directory(&path) {
err = errno().0;
} else {
err = 0;
// Need to append a trailing slash to check the contents of the directory, not its parent.
let mut tmp = path.clone();
tmp.push('/');
remoteness = path_remoteness(&tmp);
}
BaseDirectory {
path,
remoteness,
err,
used_xdg,
}
}
/// Make sure the specified directory exists. If needed, try to create it and any currently not
/// existing parent directories, like mkdir -p,.
///
/// Return 0 if, at the time of function return the directory exists, -1 otherwise.
2023-04-09 20:33:20 +08:00
fn create_directory(d: &wstr) -> bool {
let md = loop {
match wstat(d) {
Err(md) if md.kind() == ErrorKind::Interrupted => continue,
md => break md,
2023-04-09 20:33:20 +08:00
}
};
2023-04-09 20:33:20 +08:00
match md {
Ok(md) if md.is_dir() => true,
Err(e) if e.kind() == ErrorKind::NotFound => {
let dir: &wstr = wdirname(d);
return create_directory(dir) && wmkdir(d, 0o700) == 0;
2023-04-09 20:33:20 +08:00
}
_ => false,
2023-04-09 20:33:20 +08:00
}
}
/// Return whether the given path is on a remote filesystem.
2023-04-09 20:33:20 +08:00
fn path_remoteness(path: &wstr) -> DirRemoteness {
let narrow = wcs2zstring(path);
#[cfg(target_os = "linux")]
{
let mut buf: libc::statfs = unsafe { std::mem::zeroed() };
if unsafe { libc::statfs(narrow.as_ptr(), &mut buf) } < 0 {
return DirRemoteness::unknown;
}
// Linux has constants for these like NFS_SUPER_MAGIC, SMB_SUPER_MAGIC, CIFS_MAGIC_NUMBER but
// these are in varying headers. Simply hard code them.
// NOTE: The cast is necessary for 32-bit systems because of the 4-byte CIFS_MAGIC_NUMBER
match usize::try_from(buf.f_type).unwrap() {
0x6969 | // NFS_SUPER_MAGIC
0x517B | // SMB_SUPER_MAGIC
0xFE534D42 | // SMB2_MAGIC_NUMBER - not in the manpage
0xFF534D42 // CIFS_MAGIC_NUMBER
=> DirRemoteness::remote,
_ => {
// Other FSes are assumed local.
DirRemoteness::local
}
}
}
#[cfg(not(target_os = "linux"))]
{
let st_local = ST_LOCAL();
if st_local != 0 {
// ST_LOCAL is a flag to statvfs, which is itself standardized.
// In practice the only system to use this path is NetBSD.
let mut buf: libc::statvfs = unsafe { std::mem::zeroed() };
if unsafe { libc::statvfs(narrow.as_ptr(), &mut buf) } < 0 {
return DirRemoteness::unknown;
}
// statvfs::f_flag is `unsigned long`, which is 4-bytes on most 32-bit targets.
#[cfg_attr(target_pointer_width = "64", allow(clippy::useless_conversion))]
return if u64::from(buf.f_flag) & st_local != 0 {
2023-04-09 20:33:20 +08:00
DirRemoteness::local
} else {
DirRemoteness::remote
};
}
let mnt_local = MNT_LOCAL();
if mnt_local != 0 {
let mut buf: libc::statvfs = unsafe { std::mem::zeroed() };
if unsafe { libc::statvfs(narrow.as_ptr(), &mut buf) } < 0 {
2023-04-09 20:33:20 +08:00
return DirRemoteness::unknown;
}
// statfs::f_flag is hard-coded as 64-bits on 32/64-bit FreeBSD but it's a (4-byte)
// long on 32-bit NetBSD.. and always 4-bytes on macOS (even on 64-bit builds).
#[allow(clippy::useless_conversion)]
return if u64::from(buf.f_flag) & mnt_local != 0 {
2023-04-09 20:33:20 +08:00
DirRemoteness::local
} else {
DirRemoteness::remote
};
}
DirRemoteness::unknown
}
}
fn get_data_directory() -> &'static BaseDirectory {
static DIR: Lazy<BaseDirectory> =
Lazy::new(|| make_base_directory(L!("XDG_DATA_HOME"), L!("/.local/share/fish")));
&DIR
2023-04-09 20:33:20 +08:00
}
fn get_cache_directory() -> &'static BaseDirectory {
static DIR: Lazy<BaseDirectory> =
Lazy::new(|| make_base_directory(L!("XDG_CACHE_HOME"), L!("/.cache/fish")));
&DIR
}
2023-04-09 20:33:20 +08:00
fn get_config_directory() -> &'static BaseDirectory {
static DIR: Lazy<BaseDirectory> =
Lazy::new(|| make_base_directory(L!("XDG_CONFIG_HOME"), L!("/.config/fish")));
&DIR
2023-04-09 20:33:20 +08:00
}
/// Appends a path component, with a / if necessary.
2023-03-06 10:38:41 +08:00
pub fn append_path_component(path: &mut WString, component: &wstr) {
if path.is_empty() || component.is_empty() {
path.push_utfstr(component);
} else {
let path_len = path.len();
2023-03-13 10:07:00 +08:00
let path_slash = path.char_at(path_len - 1) == '/';
2023-03-06 10:38:41 +08:00
let comp_slash = component.as_char_slice()[0] == '/';
if !path_slash && !comp_slash {
// Need a slash
path.push('/');
} else if path_slash && comp_slash {
// Too many slashes.
path.pop();
}
path.push_utfstr(component);
}
}
2023-04-10 09:13:01 +08:00
#[test]
fn test_path_make_canonical() {
let mut path = L!("//foo//////bar/").to_owned();
path_make_canonical(&mut path);
assert_eq!(path, "/foo/bar");
path = L!("/").to_owned();
path_make_canonical(&mut path);
assert_eq!(path, "/");
}
2023-04-09 20:33:20 +08:00
#[test]
fn test_path() {
let mut path = L!("//foo//////bar/").to_owned();
path_make_canonical(&mut path);
assert_eq!(&path, L!("/foo/bar"));
path = L!("/").to_owned();
path_make_canonical(&mut path);
assert_eq!(&path, L!("/"));
assert!(!paths_are_equivalent(L!("/foo/bar/baz"), L!("foo/bar/baz")));
assert!(paths_are_equivalent(
L!("///foo///bar/baz"),
L!("/foo/bar////baz//")
));
assert!(paths_are_equivalent(L!("/foo/bar/baz"), L!("/foo/bar/baz")));
assert!(paths_are_equivalent(L!("/"), L!("/")));
assert_eq!(
path_apply_working_directory(L!("abc"), L!("/def/")),
L!("/def/abc")
);
assert_eq!(
path_apply_working_directory(L!("abc/"), L!("/def/")),
L!("/def/abc/")
);
assert_eq!(
path_apply_working_directory(L!("/abc/"), L!("/def/")),
L!("/abc/")
);
assert_eq!(
path_apply_working_directory(L!("/abc"), L!("/def/")),
L!("/abc")
);
assert!(path_apply_working_directory(L!(""), L!("/def/")).is_empty());
assert_eq!(path_apply_working_directory(L!("abc"), L!("")), L!("abc"));
}