From ec176dc07e74586dfb049f135478f12eed3ba914 Mon Sep 17 00:00:00 2001 From: Johannes Altmanninger Date: Sun, 9 Apr 2023 14:33:20 +0200 Subject: [PATCH] Port path.h --- cmake/Rust.cmake | 1 + fish-rust/src/compat.c | 17 + fish-rust/src/compat.rs | 12 + fish-rust/src/expand.rs | 12 + fish-rust/src/path.rs | 761 ++++++++++++++++++++++++++++++++++++++-- 5 files changed, 770 insertions(+), 33 deletions(-) diff --git a/cmake/Rust.cmake b/cmake/Rust.cmake index 43df4413d..6b7170a26 100644 --- a/cmake/Rust.cmake +++ b/cmake/Rust.cmake @@ -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 diff --git a/fish-rust/src/compat.c b/fish-rust/src/compat.c index 1fabccf18..8f94525c0 100644 --- a/fish-rust/src/compat.c +++ b/fish-rust/src/compat.c @@ -1,6 +1,23 @@ +#include #include #include 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 +} diff --git a/fish-rust/src/compat.rs b/fish-rust/src/compat.rs index c1b04b282..21d886bb2 100644 --- a/fish-rust/src/compat.rs +++ b/fish-rust/src/compat.rs @@ -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; } diff --git a/fish-rust/src/expand.rs b/fish-rust/src/expand.rs index efbfddad6..894d41c27 100644 --- a/fish-rust/src/expand.rs +++ b/fish-rust/src/expand.rs @@ -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); + } +} diff --git a/fish-rust/src/path.rs b/fish-rust/src/path.rs index 886a0bb91..96c64ad78 100644 --- a/fish-rust/src/path.rs +++ b/fish-rust/src/path.rs @@ -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 { + 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 { + 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 { + 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, + path: WString, +} +impl GetPathResult { + fn new(err: Option, 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 = 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 { + 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>(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 = 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 { + 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 { + 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 { + 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 = + Lazy::new(|| make_base_directory("XDG_DATA_HOME"L, "/.local/share/fish"L)); + &*DIR +} + +#[widestrs] +fn get_config_directory() -> &'static BaseDirectory { + static DIR: Lazy = + 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")); +}