use crate::env::{EnvMode, EnvSetMode, EnvStack, Environment, FALLBACK_PATH};
use crate::expand::expand_tilde;
use crate::flog::{flog, flogf};
use crate::prelude::*;
use crate::wutil::{normalize_path, path_normalize_for_cd, waccess, wdirname, wstat};
use cfg_if::cfg_if;
use errno::{errno, set_errno, Errno};
use fish_widestring::{wcs2osstring, wcs2zstring, HOME_DIRECTORY};
use libc::{EACCES, ENOENT, ENOTDIR, X_OK};
use nix::unistd::AccessFlags;
use std::ffi::OsStr;
use std::io::ErrorKind;
use std::os::unix::prelude::*;
use std::sync::LazyLock;
pub fn path_get_config() -> Option<WString> {
CONFIG_DIRECTORY.path()
}
pub fn path_get_data() -> Option<WString> {
DATA_DIRECTORY.path()
}
pub fn path_get_cache() -> Option<WString> {
CACHE_DIRECTORY.path()
}
#[derive(Clone, Copy, Eq, PartialEq)]
pub enum DirRemoteness {
Unknown,
Local,
Remote,
}
pub fn path_get_data_remoteness() -> DirRemoteness {
DATA_DIRECTORY.remoteness
}
pub fn path_get_config_remoteness() -> DirRemoteness {
CONFIG_DIRECTORY.remoteness
}
pub fn path_emit_config_directory_messages(vars: &EnvStack) {
let data = &*DATA_DIRECTORY;
if let Some(error) = &data.err {
maybe_issue_path_warning(
L!("data"),
wgettext!("can not save history"),
data.used_xdg,
L!("XDG_DATA_HOME"),
&data.path,
error,
vars,
);
}
if data.remoteness == DirRemoteness::Remote {
flog!(path, "data path appears to be on a network volume");
}
let config = &*CONFIG_DIRECTORY;
if let Some(error) = &data.err {
maybe_issue_path_warning(
L!("config"),
wgettext!("can not save universal variables or functions"),
config.used_xdg,
L!("XDG_CONFIG_HOME"),
&config.path,
error,
vars,
);
}
if config.remoteness == DirRemoteness::Remote {
flog!(path, "config path appears to be on a network volume");
}
}
fn maybe_issue_path_warning(
which_dir: &wstr,
custom_error_msg: &wstr,
using_xdg: bool,
xdg_var: &wstr,
path: &wstr,
error: &std::io::Error,
vars: &EnvStack,
) {
let warning_var_name = L!("_FISH_WARNED_").to_owned() + which_dir;
let global_exported_mode = EnvMode::GLOBAL | EnvMode::EXPORT;
if vars.getf(&warning_var_name, global_exported_mode).is_some() {
return;
}
vars.set_one(
&warning_var_name,
EnvSetMode::new_at_early_startup(global_exported_mode),
L!("1").to_owned(),
);
flog!(error, custom_error_msg);
if path.is_empty() {
flog!(
warning_path,
wgettext_fmt!("Unable to locate the %s directory.", which_dir)
);
flog!(
warning_path,
wgettext_fmt!(
"Please set the %s or HOME environment variable before starting fish.",
xdg_var
)
);
} else {
let env_var = if using_xdg { xdg_var } else { L!("HOME") };
flog!(
warning_path,
wgettext_fmt!(
"Unable to locate %s directory derived from $%s: '%s'.",
which_dir,
env_var,
path
)
);
flog!(
warning_path,
wgettext_fmt!("The error was '%s'.", error.to_string())
);
flog!(
warning_path,
wgettext_fmt!(
"Please set $%s to a directory where you have write access.",
env_var
)
);
}
eprintf!("\n");
}
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)
}
}
pub struct GetPathResult {
pub err: Option<Errno>,
pub 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, &FALLBACK_PATH)
}
}
fn path_check_executable(path: &wstr) -> Result<(), std::io::Error> {
if waccess(path, AccessFlags::X_OK).is_err() {
return Err(std::io::Error::last_os_error());
}
let buff = wstat(path)?;
if buff.file_type().is_file() {
Ok(())
} else {
Err(ErrorKind::PermissionDenied.into())
}
}
pub fn path_get_paths(cmd: &wstr, vars: &dyn Environment) -> Vec<WString> {
flogf!(path, "path_get_paths('%s')", cmd);
let mut paths = vec![];
if cmd.contains('/') && path_check_executable(cmd).is_ok() {
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_check_executable(&path).is_ok() {
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());
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;
}
if cmd.contains('\0') {
return noent_res;
}
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(()) => {
return GetPathResult::new(None, proposed_path);
}
Err(err) => {
if err.0 != ENOENT && best.err == Some(Errno(ENOENT)) {
if waccess(wdirname(&proposed_path), AccessFlags::X_OK).is_ok() {
best = GetPathResult::new(Some(err), proposed_path);
}
}
}
}
}
best
}
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_eq!(wd.chars().next_back(), Some('/'));
let paths = path_apply_cdpath(dir, wd, vars);
for a_dir in paths {
if let Ok(md) = wstat(&a_dir) {
if md.is_dir() {
return Some(a_dir);
}
err = ENOTDIR;
}
}
set_errno(Errno(err));
None
}
pub fn path_apply_cdpath(dir: &wstr, wd: &wstr, env_vars: &dyn Environment) -> Vec<WString> {
let mut paths = vec![];
if dir.chars().next() == Some('/') {
paths.push(dir.to_owned());
} else if dir.starts_with(L!("./"))
|| dir.starts_with(L!("../"))
|| [L!("."), L!("..")].contains(&dir)
{
paths.push(path_normalize_for_cd(wd, dir));
} else {
let mut cdpathsv = vec![];
if let Some(cdpaths) = env_vars.get(L!("CDPATH")) {
cdpathsv = cdpaths.as_list().to_vec();
}
cdpathsv.push(L!(".").to_owned());
for path in cdpathsv {
let mut abspath = WString::new();
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
}
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 == ".."
{
return path_get_cdpath(&exp_path, wd, vars);
}
None
}
pub fn path_make_canonical(path: &mut WString) {
let chars: &mut [char] = path.as_char_slice_mut();
let mut written = 0;
let mut prev_was_slash = false;
for read in 0..chars.len() {
let c = chars[read];
let is_slash = c == '/';
if prev_was_slash && is_slash {
continue;
}
chars[written] = c;
written += 1;
prev_was_slash = is_slash;
}
if written > 1 {
path.truncate(written - usize::from(prev_was_slash));
}
}
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;
}
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;
}
let mut idx1 = 0;
let mut idx2 = 0;
while idx1 < len1 && idx2 < len2 {
let c1 = p1[idx1];
let c2 = p2[idx2];
if c1 != c2 {
break;
}
idx1 += 1;
idx2 += 1;
while c1 == '/' && p1.get(idx1) == Some(&'/') {
idx1 += 1;
}
while c2 == '/' && p2.get(idx2) == Some(&'/') {
idx2 += 1;
}
}
idx1 == len1 && idx2 == len2
}
pub fn path_is_valid(path: &wstr, working_directory: &wstr) -> bool {
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('/') {
let mut tmp = working_directory.to_owned();
tmp.push_utfstr(path);
waccess(&tmp, AccessFlags::F_OK).is_ok()
} else {
waccess(path, AccessFlags::F_OK).is_ok()
}
}
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(),
_ => false,
}
}
pub fn path_apply_working_directory(path: &wstr, working_directory: &wstr) -> WString {
if path.is_empty() || working_directory.is_empty() {
return path.to_owned();
}
let prepend_wd = path.char_at(0) != '/' && path.char_at(0) != HOME_DIRECTORY;
if !prepend_wd {
return path.to_owned();
}
let mut path_component = path.to_owned();
if path_component.starts_with("./") {
path_component.replace_range(0..2, L!(""));
}
while path_component.starts_with("/") {
path_component.replace_range(0..1, L!(""));
}
let mut new_path = working_directory.to_owned();
append_path_component(&mut new_path, &path_component);
new_path
}
struct BaseDirectory {
path: WString,
remoteness: DirRemoteness,
err: Option<std::io::Error>,
used_xdg: bool,
}
impl BaseDirectory {
fn path(&self) -> Option<WString> {
self.err.is_none().then(|| self.path.clone())
}
}
#[cfg_attr(test, allow(unused_variables), allow(unreachable_code))]
fn make_base_directory(xdg_var: &wstr, non_xdg_homepath: &wstr) -> BaseDirectory {
#[cfg(test)]
{
use crate::common::BUILD_DIR;
use fish_widestring::osstr2wcstring;
use std::path::PathBuf;
let mut build_dir = PathBuf::from(BUILD_DIR);
build_dir.push("fish-test-home");
let err = std::fs::create_dir_all(&build_dir).err();
return BaseDirectory {
path: osstr2wcstring(build_dir),
remoteness: DirRemoteness::Unknown,
used_xdg: false,
err,
};
}
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");
used_xdg = true;
} else {
if let Some(home) = vars.getf_unless_empty(L!("HOME"), EnvMode::GLOBAL | EnvMode::EXPORT) {
path = home.as_string() + non_xdg_homepath;
}
used_xdg = false;
}
let mut remoteness = DirRemoteness::Unknown;
let err = if path.is_empty() {
Some(std::io::Error::new(
std::io::ErrorKind::NotFound,
"Path is empty",
))
} else if let Err(io_error) = create_dir_all_with_mode(wcs2osstring(&path), 0o700) {
Some(io_error)
} else {
let mut tmp = path.clone();
tmp.push('/');
remoteness = path_remoteness(&tmp);
None
};
BaseDirectory {
path,
remoteness,
err,
used_xdg,
}
}
fn create_dir_all_with_mode<P: AsRef<std::path::Path>>(path: P, mode: u32) -> std::io::Result<()> {
use std::os::unix::fs::DirBuilderExt as _;
std::fs::DirBuilder::new()
.recursive(true)
.mode(mode)
.create(path.as_ref())
}
pub fn path_remoteness(path: &wstr) -> DirRemoteness {
cfg_if! {
if #[cfg(target_os = "illumos")] {
DirRemoteness::Unknown
} else {
let narrow = wcs2zstring(path);
use std::mem::MaybeUninit;
cfg_if! {
if #[cfg(any(target_os = "linux", cygwin))] {
let mut buf = MaybeUninit::uninit();
if unsafe { libc::statfs(narrow.as_ptr(), buf.as_mut_ptr()) } < 0 {
return DirRemoteness::Unknown;
}
let buf = unsafe { buf.assume_init() };
match buf.f_type as usize {
0x5346414F | 0x6B414653 | 0x73757245 | 0x47504653 | 0x564c | 0x6969 | 0x7461636f | 0x61636673 | 0x517B | 0xFE534D42 | 0xFF534D42 | 0x01021997 | 0x19830326 | 0x013111A7 | 0x013111A8 | 0x65735546 | 0xA501FCF5 => DirRemoteness::Remote,
_ => {
DirRemoteness::Unknown
}
}
} else if #[cfg(target_os = "netbsd")] {
let mut buf = MaybeUninit::uninit();
if unsafe { libc::statvfs(narrow.as_ptr(), buf.as_mut_ptr()) } < 0 {
return DirRemoteness::Unknown;
}
let buf = unsafe { buf.assume_init() };
#[allow(clippy::useless_conversion)]
let flags = buf.f_flag as u64;
#[allow(clippy::unnecessary_cast)]
if flags & (libc::MNT_LOCAL as u64) != 0 {
DirRemoteness::Local
} else {
DirRemoteness::Remote
}
} else {
let mut buf = MaybeUninit::uninit();
if unsafe { libc::statfs(narrow.as_ptr(), buf.as_mut_ptr()) } < 0 {
return DirRemoteness::Unknown;
}
let buf = unsafe { buf.assume_init() };
#[allow(clippy::useless_conversion)]
let flags = buf.f_flags as u64;
#[allow(clippy::unnecessary_cast)]
if flags & (libc::MNT_LOCAL as u64) != 0 {
DirRemoteness::Local
} else {
DirRemoteness::Remote
}
}
}
}
}
}
static DATA_DIRECTORY: LazyLock<BaseDirectory> =
LazyLock::new(|| make_base_directory(L!("XDG_DATA_HOME"), L!("/.local/share/fish")));
static CACHE_DIRECTORY: LazyLock<BaseDirectory> =
LazyLock::new(|| make_base_directory(L!("XDG_CACHE_HOME"), L!("/.cache/fish")));
static CONFIG_DIRECTORY: LazyLock<BaseDirectory> =
LazyLock::new(|| make_base_directory(L!("XDG_CONFIG_HOME"), L!("/.config/fish")));
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();
let path_slash = path.char_at(path_len - 1) == '/';
let comp_slash = component.as_char_slice()[0] == '/';
if !path_slash && !comp_slash {
path.push('/');
} else if path_slash && comp_slash {
path.pop();
}
path.push_utfstr(component);
}
}
#[cfg(test)]
mod tests {
use super::{path_apply_working_directory, path_make_canonical, paths_are_equivalent};
use crate::prelude::*;
#[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, "/");
}
#[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!("/"));
path = L!("/home/fishuser/").to_owned();
path_make_canonical(&mut path);
assert_eq!(&path, L!("/home/fishuser"));
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"));
}
}