use std::{borrow::Cow, env, ffi::{OsStr, OsString}, future::Future, io, path::{Component, Path, PathBuf}}; use yazi_shared::url::Url; use crate::{CWD, services}; #[inline] pub fn clean_url(url: &Url) -> Url { Url::from(clean_path(url)) } #[inline] pub fn clean_path(path: impl AsRef) -> PathBuf { _clean_path(path.as_ref()) } fn _clean_path(path: &Path) -> PathBuf { let mut out = vec![]; for c in path.components() { match c { Component::CurDir => {} Component::ParentDir => match out.last() { Some(Component::RootDir) => {} Some(Component::Normal(_)) => _ = out.pop(), None | Some(Component::CurDir) | Some(Component::ParentDir) | Some(Component::Prefix(_)) => out.push(c), }, c => out.push(c), } } if out.is_empty() { PathBuf::from(".") } else { out.iter().collect() } } #[inline] pub fn expand_path(p: impl AsRef) -> PathBuf { _expand_path(p.as_ref()) } fn _expand_path(p: &Path) -> PathBuf { // ${HOME} or $HOME #[cfg(unix)] let re = regex::bytes::Regex::new(r"\$(?:\{([^}]+)\}|([a-zA-Z\d_]+))").unwrap(); // %USERPROFILE% #[cfg(windows)] let re = regex::bytes::Regex::new(r"%([^%]+)%").unwrap(); let b = re.replace_all(p.as_os_str().as_encoded_bytes(), |caps: ®ex::bytes::Captures| { let name = caps.get(2).or_else(|| caps.get(1)).unwrap(); str::from_utf8(name.as_bytes()) .ok() .and_then(env::var_os) .map_or_else(|| caps.get(0).unwrap().as_bytes().to_owned(), |s| s.into_encoded_bytes()) }); // Windows paths that only have a drive letter but no root, e.g. "D:" #[cfg(windows)] if b.len() == 2 { if b[1] == b':' && b[0].is_ascii_alphabetic() { return PathBuf::from(format!("{}:\\", b[0].to_ascii_uppercase() as char)); } } let p = unsafe { Path::new(OsStr::from_encoded_bytes_unchecked(b.as_ref())) }; if let Ok(rest) = p.strip_prefix("~") { clean_path(dirs::home_dir().unwrap_or_default().join(rest)) } else if p.is_absolute() { clean_path(p) } else { clean_path(CWD.load().join(p)) } } pub fn skip_path(p: &Path, u: usize) -> &Path { let mut it = p.components(); for _ in 0..u { if it.next().is_none() { return Path::new(""); } } it.as_path() } pub async fn unique_name(u: Url, append: F) -> io::Result where F: Future, { match services::symlink_metadata(&u).await { Ok(_) => _unique_name(u, append.await).await, Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(u), Err(e) => Err(e), } } async fn _unique_name(mut url: Url, append: bool) -> io::Result { let Some(stem) = url.file_stem().map(|s| s.to_owned()) else { return Err(io::Error::new(io::ErrorKind::InvalidInput, "empty file stem")); }; let dot_ext = url.extension().map_or_else(OsString::new, |e| { let mut s = OsString::with_capacity(e.len() + 1); s.push("."); s.push(e); s }); let mut i = 1u64; let mut name = OsString::with_capacity(stem.len() + dot_ext.len() + 5); loop { name.clear(); name.push(&stem); if append { name.push(&dot_ext); name.push("_"); name.push(i.to_string()); } else { name.push("_"); name.push(i.to_string()); name.push(&dot_ext); } url.set_name(&name); match services::symlink_metadata(&url).await { Ok(_) => i += 1, Err(e) if e.kind() == io::ErrorKind::NotFound => break, Err(e) => return Err(e), } } Ok(url) } // Parameters // * `path`: The absolute path(contains no `/./`) to get relative path. // * `root`: The absolute path(contains no `/./`) to be compared. // // Return // * Unix: The relative format to `root` of `path`. // * Windows: The relative format to `root` of `path`; or `path` itself when // `path` and `root` are both under different disk drives. pub fn path_relative_to<'a>(path: &'a Path, root: &Path) -> Cow<'a, Path> { assert!(path.is_absolute()); assert!(root.is_absolute()); let mut p_comps = path.components(); let mut r_comps = root.components(); // 1. Ensure that the two paths have the same prefix. // 2. Strips any common prefix the two paths do have. // // NOTE: // Prefixes are platform dependent, // but different prefixes would for example indicate paths for different drives // on Windows. let (p_head, r_head) = loop { use std::path::Component::*; match (p_comps.next(), r_comps.next()) { (Some(RootDir), Some(RootDir)) => (), (Some(Prefix(a)), Some(Prefix(b))) if a == b => (), (Some(Prefix(_) | RootDir), _) | (_, Some(Prefix(_) | RootDir)) => { return Cow::from(path); } (None, None) => break (None, None), (a, b) if a != b => break (a, b), _ => (), } }; let p_comps = p_head.into_iter().chain(p_comps); let walk_up = r_head.into_iter().chain(r_comps).map(|_| Component::ParentDir); let mut buf = PathBuf::new(); buf.extend(walk_up); buf.extend(p_comps); Cow::from(buf) } #[cfg(windows)] pub fn backslash_to_slash(p: &Path) -> Cow<'_, Path> { let bytes = p.as_os_str().as_encoded_bytes(); // Fast path to skip if there are no backslashes let skip_len = bytes.iter().take_while(|&&b| b != b'\\').count(); if skip_len >= bytes.len() { return Cow::Borrowed(p); } let (skip, rest) = bytes.split_at(skip_len); let mut out = Vec::new(); out.try_reserve_exact(bytes.len()).unwrap_or_else(|_| panic!()); out.extend(skip); for &b in rest { out.push(if b == b'\\' { b'/' } else { b }); } Cow::Owned(PathBuf::from(unsafe { OsString::from_encoded_bytes_unchecked(out) })) } #[cfg(test)] mod tests { use std::{borrow::Cow, path::Path}; use super::path_relative_to; #[cfg(unix)] #[test] fn test_path_relative_to() { fn assert(path: &str, root: &str, res: &str) { assert_eq!(path_relative_to(Path::new(path), Path::new(root)), Cow::Borrowed(Path::new(res))); } assert("/a/b", "/a/b/c", "../"); assert("/a/b/c", "/a/b", "c"); assert("/a/b/c", "/a/b/d", "../c"); assert("/a", "/a/b/c", "../../"); assert("/a/a/b", "/a/b/b", "../../a/b"); } #[cfg(windows)] #[test] fn test_path_relative_to() { fn assert(path: &str, root: &str, res: &str) { assert_eq!(path_relative_to(Path::new(path), Path::new(root)), Cow::Borrowed(Path::new(res))); } assert("C:\\a\\b", "C:\\a\\b\\c", "..\\"); assert("C:\\a\\b\\c", "C:\\a\\b", "c"); assert("C:\\a\\b\\c", "C:\\a\\b\\d", "..\\c"); assert("C:\\a", "C:\\a\\b\\c", "..\\..\\"); assert("C:\\a\\a\\b", "C:\\a\\b\\b", "..\\..\\a\\b"); } }