mirror of
https://github.com/fish-shell/fish-shell
synced 2025-01-14 05:53:59 +00:00
Port path.h
This commit is contained in:
parent
629cbe0115
commit
ec176dc07e
5 changed files with 770 additions and 33 deletions
|
@ -54,6 +54,7 @@ corrosion_set_env_vars(${fish_rust_target}
|
|||
"FISH_BUILD_DIR=${CMAKE_BINARY_DIR}"
|
||||
"FISH_AUTOCXX_GEN_DIR=${fish_autocxx_gen_dir}"
|
||||
"FISH_RUST_TARGET_DIR=${rust_target_dir}"
|
||||
"PREFIX=${CMAKE_INSTALL_PREFIX}"
|
||||
)
|
||||
|
||||
target_include_directories(${fish_rust_target} INTERFACE
|
||||
|
|
|
@ -1,6 +1,23 @@
|
|||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <term.h>
|
||||
|
||||
size_t C_MB_CUR_MAX() { return MB_CUR_MAX; }
|
||||
|
||||
int has_cur_term() { return cur_term != NULL; }
|
||||
|
||||
uint64_t C_ST_LOCAL() {
|
||||
#if defined(ST_LOCAL)
|
||||
return ST_LOCAL;
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
uint64_t C_MNT_LOCAL() {
|
||||
#if defined(MNT_LOCAL)
|
||||
return MNT_LOCAL;
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
|
|
@ -7,7 +7,19 @@ pub fn cur_term() -> bool {
|
|||
unsafe { has_cur_term() }
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub fn ST_LOCAL() -> u64 {
|
||||
unsafe { C_ST_LOCAL() }
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub fn MNT_LOCAL() -> u64 {
|
||||
unsafe { C_MNT_LOCAL() }
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
fn C_MB_CUR_MAX() -> usize;
|
||||
fn has_cur_term() -> bool;
|
||||
fn C_ST_LOCAL() -> u64;
|
||||
fn C_MNT_LOCAL() -> u64;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use crate::common::{char_offset, EXPAND_RESERVED_BASE, EXPAND_RESERVED_END};
|
||||
use crate::env::Environment;
|
||||
use crate::operation_context::OperationContext;
|
||||
use crate::parse_constants::ParseErrorList;
|
||||
use crate::wchar::{wstr, WString};
|
||||
|
@ -140,3 +141,14 @@ pub fn expand_to_command_and_args(
|
|||
) -> ExpandResult {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Perform tilde expansion and nothing else on the specified string, which is modified in place.
|
||||
///
|
||||
/// \param input the string to tilde expand
|
||||
pub fn expand_tilde(input: &mut WString, _vars: &dyn Environment) {
|
||||
if input.chars().next() == Some('~') {
|
||||
input.replace_range(0..1, wstr::from_char_slice(&[HOME_DIRECTORY]));
|
||||
todo!();
|
||||
// expand_home_directory(input, vars);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,529 @@
|
|||
use crate::{
|
||||
expand::HOME_DIRECTORY,
|
||||
wchar::{wstr, WExt, WString, L},
|
||||
//! 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::wcs2zstring;
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
use crate::compat::{MNT_LOCAL, ST_LOCAL};
|
||||
use crate::env::{EnvMode, EnvStack, Environment};
|
||||
use crate::expand::{expand_tilde, HOME_DIRECTORY};
|
||||
use crate::flog::{FLOG, FLOGF};
|
||||
use crate::wchar::{wstr, WExt, WString, L};
|
||||
use crate::wutil::{
|
||||
normalize_path, path_normalize_for_cd, waccess, wdirname, wgettext, wgettext_fmt, wmkdir, wstat,
|
||||
};
|
||||
use errno::{errno, set_errno, Errno};
|
||||
use libc::{EACCES, EAGAIN, ENOENT, ENOTDIR, F_OK, X_OK};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::ffi::OsStr;
|
||||
use std::io::Write;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::os::unix::prelude::MetadataExt;
|
||||
use widestring_suffix::widestrs;
|
||||
|
||||
/// 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
|
||||
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 and 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_data() -> Option<WString> {
|
||||
let dir = get_data_directory();
|
||||
if dir.success() {
|
||||
Some(dir.path.to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[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.
|
||||
/// 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.
|
||||
#[widestrs]
|
||||
pub fn path_emit_config_directory_messages(vars: &mut EnvStack) {
|
||||
let data = get_data_directory();
|
||||
if !data.success() {
|
||||
maybe_issue_path_warning(
|
||||
"data"L,
|
||||
&wgettext!("can not save history"),
|
||||
data.used_xdg,
|
||||
"XDG_DATA_HOME"L,
|
||||
&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(
|
||||
"config"L,
|
||||
&wgettext!("can not save universal variables or functions"),
|
||||
config.used_xdg,
|
||||
"XDG_CONFIG_HOME"L,
|
||||
&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.
|
||||
#[widestrs]
|
||||
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: &mut EnvStack,
|
||||
) {
|
||||
let warning_var_name = "_FISH_WARNED_"L.to_owned() + which_dir;
|
||||
if vars
|
||||
.getf(&warning_var_name, EnvMode::GLOBAL | EnvMode::EXPORT)
|
||||
.is_some()
|
||||
{
|
||||
return;
|
||||
}
|
||||
vars.set_one(
|
||||
&warning_var_name,
|
||||
EnvMode::GLOBAL | EnvMode::EXPORT,
|
||||
"1"L.to_owned(),
|
||||
);
|
||||
|
||||
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 { "HOME"L };
|
||||
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
|
||||
)
|
||||
);
|
||||
}
|
||||
let _ = std::io::stdout().write(&[b'\n']);
|
||||
}
|
||||
|
||||
/// Finds the path of an executable named \p cmd, by looking in $PATH taken from \p vars.
|
||||
/// \returns the path if found, none if not.
|
||||
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.
|
||||
#[widestrs]
|
||||
static DEFAULT_PATH: Lazy<[WString; 3]> = Lazy::new(|| {
|
||||
[
|
||||
"/bin"L.to_owned(),
|
||||
"/usr/bin"L.to_owned(),
|
||||
// TODO This should use env!. The fallback is only to appease "cargo test" for now.
|
||||
WString::from_str(option_env!("PREFIX").unwrap_or("/usr/local")) + "/bin"L,
|
||||
]
|
||||
});
|
||||
|
||||
/// Finds the path of an executable named \p cmd, by looking in $PATH taken from \p vars.
|
||||
/// 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 {
|
||||
err: Option<Errno>,
|
||||
path: WString,
|
||||
}
|
||||
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_is_executable(path: &wstr) -> bool {
|
||||
let narrow = wcs2zstring(path);
|
||||
if unsafe { libc::access(narrow.as_ptr(), X_OK) } != 0 {
|
||||
return false;
|
||||
}
|
||||
let narrow: Vec<u8> = narrow.into();
|
||||
let Ok(md) = std::fs::metadata(OsStr::from_bytes(&narrow)) else { return false; };
|
||||
md.is_file()
|
||||
}
|
||||
|
||||
/// 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_is_executable(cmd) {
|
||||
paths.push(cmd.to_owned());
|
||||
return paths;
|
||||
}
|
||||
|
||||
let Some(path_var) = vars.get(L!("PATH")) else { return paths; };
|
||||
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_is_executable(&path) {
|
||||
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.
|
||||
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());
|
||||
}
|
||||
|
||||
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.clone()), X_OK) == 0 {
|
||||
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.
|
||||
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().last() == Some('/'));
|
||||
let paths = path_apply_cdpath(dir, wd, vars);
|
||||
|
||||
for a_dir in paths {
|
||||
if let Some(md) = wstat(&a_dir) {
|
||||
if md.is_dir() {
|
||||
return Some(a_dir);
|
||||
}
|
||||
err = ENOTDIR;
|
||||
}
|
||||
}
|
||||
|
||||
set_errno(Errno(err));
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns the given directory with all CDPATH components applied.
|
||||
#[widestrs]
|
||||
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) {
|
||||
// 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("CDPATH"L) {
|
||||
cdpathsv = cdpaths.as_list().to_vec();
|
||||
}
|
||||
// Always append $PWD
|
||||
cdpathsv.push("."L.to_owned());
|
||||
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.
|
||||
#[widestrs]
|
||||
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 == ".."L
|
||||
{
|
||||
// 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
|
||||
}
|
||||
|
||||
#[widestrs]
|
||||
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) {
|
||||
true
|
||||
} else if [".."L, "../"L].contains(&path) {
|
||||
!working_directory.is_empty() && working_directory != "/"L
|
||||
} 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)) {
|
||||
(Some(s1), Some(s2)) => s1.ino() == s2.ino() && s1.dev() == s2.dev(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 ~).
|
||||
|
@ -36,6 +558,171 @@ pub fn path_apply_working_directory(path: &wstr, working_directory: &wstr) -> WS
|
|||
new_path
|
||||
}
|
||||
|
||||
/// 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 \p xdg_var is
|
||||
/// set, use that directory; otherwise use the path \p non_xdg_homepath rooted in $HOME. \return the
|
||||
/// result; see the base_directory_t fields.
|
||||
#[widestrs]
|
||||
fn make_base_directory(xdg_var: &wstr, non_xdg_homepath: &wstr) -> BaseDirectory {
|
||||
// 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() + "/fish"L;
|
||||
used_xdg = true;
|
||||
} else {
|
||||
if let Some(home) = vars.getf_unless_empty("HOME"L, EnvMode::GLOBAL | EnvMode::EXPORT) {
|
||||
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.
|
||||
fn create_directory(d: &wstr) -> bool {
|
||||
let mut md;
|
||||
loop {
|
||||
md = wstat(d);
|
||||
if md.is_none() && errno().0 != EAGAIN {
|
||||
break;
|
||||
}
|
||||
}
|
||||
match md {
|
||||
Some(md) => {
|
||||
if md.is_dir() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if errno().0 == ENOENT {
|
||||
let dir = wdirname(d.to_owned());
|
||||
if create_directory(&dir) && wmkdir(d, 0o700) == 0 {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// \return whether the given path is on a remote filesystem.
|
||||
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;
|
||||
}
|
||||
return if buf.f_flag & st_local != 0 {
|
||||
DirRemoteness::local
|
||||
} else {
|
||||
DirRemoteness::remote
|
||||
};
|
||||
}
|
||||
let mnt_local = MNT_LOCAL();
|
||||
if mnt_local != 0 {
|
||||
let mut buf: libc::statfs = unsafe { std::mem::zeroed() };
|
||||
if unsafe { libc::statfs(narrow.as_ptr(), &mut buf) } < 0 {
|
||||
return DirRemoteness::unknown;
|
||||
}
|
||||
return if u64::from(buf.f_flags) & mnt_local != 0 {
|
||||
DirRemoteness::local
|
||||
} else {
|
||||
DirRemoteness::remote
|
||||
};
|
||||
}
|
||||
DirRemoteness::unknown
|
||||
}
|
||||
}
|
||||
|
||||
#[widestrs]
|
||||
fn get_data_directory() -> &'static BaseDirectory {
|
||||
static DIR: Lazy<BaseDirectory> =
|
||||
Lazy::new(|| make_base_directory("XDG_DATA_HOME"L, "/.local/share/fish"L));
|
||||
&*DIR
|
||||
}
|
||||
|
||||
#[widestrs]
|
||||
fn get_config_directory() -> &'static BaseDirectory {
|
||||
static DIR: Lazy<BaseDirectory> =
|
||||
Lazy::new(|| make_base_directory("XDG_CONFIG_HOME"L, "/.config/fish"L));
|
||||
&*DIR
|
||||
}
|
||||
|
||||
/// Appends a path component, with a / if necessary.
|
||||
pub fn append_path_component(path: &mut WString, component: &wstr) {
|
||||
if path.is_empty() || component.is_empty() {
|
||||
path.push_utfstr(component);
|
||||
|
@ -54,36 +741,6 @@ pub fn append_path_component(path: &mut WString, component: &wstr) {
|
|||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_path_make_canonical() {
|
||||
let mut path = L!("//foo//////bar/").to_owned();
|
||||
|
@ -94,3 +751,41 @@ fn test_path_make_canonical() {
|
|||
path_make_canonical(&mut path);
|
||||
assert_eq!(path, "/");
|
||||
}
|
||||
|
||||
#[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"));
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue