diff --git a/yazi-actor/src/cmp/trigger.rs b/yazi-actor/src/cmp/trigger.rs index f987920c..17f997b7 100644 --- a/yazi-actor/src/cmp/trigger.rs +++ b/yazi-actor/src/cmp/trigger.rs @@ -1,7 +1,7 @@ use std::{ffi::OsString, mem, path::MAIN_SEPARATOR_STR}; use anyhow::Result; -use yazi_fs::{CWD, expand_url, provider}; +use yazi_fs::{CWD, path::expand_url, provider}; use yazi_macro::{act, render, succ}; use yazi_parser::cmp::{CmpItem, ShowOpt, TriggerOpt}; use yazi_proxy::CmpProxy; @@ -67,7 +67,7 @@ impl Actor for Trigger { impl Trigger { fn split_url(s: &str) -> Option<(Url, UrnBuf)> { - let (scheme, path, _) = Url::parse(s.as_bytes()).ok()?; + let (scheme, path, ..) = Url::parse(s.as_bytes()).ok()?; if !scheme.is_virtual() && path.as_os_str() == "~" { return None; // We don't autocomplete a `~`, but `~/` diff --git a/yazi-actor/src/lives/file.rs b/yazi-actor/src/lives/file.rs index e6483417..c736ff26 100644 --- a/yazi-actor/src/lives/file.rs +++ b/yazi-actor/src/lives/file.rs @@ -81,14 +81,14 @@ impl UserData for File { })? }); methods.add_method("prefix", |lua, me, ()| { - if !me.url.has_base() { + if !me.url.has_trail() { return Ok(None); } let Some(path) = me.url.as_path() else { return Ok(None); }; - let mut comp = path.strip_prefix(me.url.loc.base()).unwrap_or(path).components(); + let mut comp = path.strip_prefix(me.url.loc.trail()).unwrap_or(path).components(); comp.next_back(); Some(lua.create_string(comp.as_path().as_os_str().as_encoded_bytes())).transpose() }); diff --git a/yazi-actor/src/mgr/bulk_rename.rs b/yazi-actor/src/mgr/bulk_rename.rs index 76a84bc6..0214252f 100644 --- a/yazi-actor/src/mgr/bulk_rename.rs +++ b/yazi-actor/src/mgr/bulk_rename.rs @@ -6,7 +6,7 @@ use scopeguard::defer; use tokio::io::AsyncWriteExt; use yazi_config::{YAZI, opener::OpenerRule}; use yazi_dds::Pubsub; -use yazi_fs::{File, FilesOp, max_common_root, maybe_exists, paths_to_same_file, provider::{self, local::{Gate, Local}}, skip_url}; +use yazi_fs::{File, FilesOp, max_common_root, maybe_exists, path::skip_url, paths_to_same_file, provider::{self, local::{Gate, Local}}}; use yazi_macro::{err, succ}; use yazi_parser::VoidOpt; use yazi_proxy::{AppProxy, HIDER, TasksProxy, WATCHER}; diff --git a/yazi-actor/src/mgr/cd.rs b/yazi-actor/src/mgr/cd.rs index c94fb35b..b9ed52f2 100644 --- a/yazi-actor/src/mgr/cd.rs +++ b/yazi-actor/src/mgr/cd.rs @@ -5,7 +5,7 @@ use tokio::pin; use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; use yazi_config::popup::InputCfg; use yazi_dds::Pubsub; -use yazi_fs::{File, FilesOp, expand_url}; +use yazi_fs::{File, FilesOp, path::expand_url}; use yazi_macro::{act, err, render, succ}; use yazi_parser::mgr::CdOpt; use yazi_proxy::{CmpProxy, InputProxy, MgrProxy}; @@ -49,9 +49,7 @@ impl Actor for Cd { // Current let rep = tab.history.remove_or(&opt.target); let rep = mem::replace(&mut tab.current, rep); - if rep.url.is_regular() { - tab.history.insert(rep.url.to_owned(), rep); - } + tab.history.insert(rep.url.to_owned(), rep); // Parent if let Some(parent) = opt.target.parent_url() { diff --git a/yazi-actor/src/mgr/follow.rs b/yazi-actor/src/mgr/follow.rs index 82b6f9ae..38e3854b 100644 --- a/yazi-actor/src/mgr/follow.rs +++ b/yazi-actor/src/mgr/follow.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use yazi_fs::clean_url; +use yazi_fs::path::clean_url; use yazi_macro::{act, succ}; use yazi_parser::VoidOpt; use yazi_shared::event::Data; @@ -20,7 +20,7 @@ impl Actor for Follow { if link_to.is_absolute() { act!(mgr:reveal, cx, link_to.to_owned()) } else if let Some(p) = file.url.parent_url() { - act!(mgr:reveal, cx, clean_url(p.join(link_to)).into_owned()) + act!(mgr:reveal, cx, clean_url(p.join(link_to))) } else { succ!() } diff --git a/yazi-boot/src/boot.rs b/yazi-boot/src/boot.rs index a41380d1..e0b852a1 100644 --- a/yazi-boot/src/boot.rs +++ b/yazi-boot/src/boot.rs @@ -2,7 +2,7 @@ use std::{borrow::Cow, collections::HashSet, path::PathBuf}; use futures::executor::block_on; use serde::Serialize; -use yazi_fs::{CWD, Xdg, expand_url, provider}; +use yazi_fs::{CWD, Xdg, path::expand_url, provider}; use yazi_shared::url::{Url, UrnBuf}; #[derive(Debug, Default, Serialize)] diff --git a/yazi-config/src/pattern.rs b/yazi-config/src/pattern.rs index 395754bf..bdc07dc6 100644 --- a/yazi-config/src/pattern.rs +++ b/yazi-config/src/pattern.rs @@ -34,7 +34,7 @@ impl Pattern { #[cfg(windows)] let path = if self.sep_lit { - yazi_fs::backslash_to_slash(&url.loc) + yazi_fs::path::backslash_to_slash(&url.loc) } else { std::borrow::Cow::Borrowed(url.loc.as_path()) }; diff --git a/yazi-config/src/preview/preview.rs b/yazi-config/src/preview/preview.rs index e842e245..2061df6f 100644 --- a/yazi-config/src/preview/preview.rs +++ b/yazi-config/src/preview/preview.rs @@ -3,7 +3,7 @@ use std::{borrow::Cow, path::PathBuf}; use anyhow::{Context, Result, bail}; use serde::{Deserialize, Serialize}; use yazi_codegen::DeserializeOver2; -use yazi_fs::{Xdg, expand_path}; +use yazi_fs::{Xdg, path::expand_path}; use yazi_shared::{SStr, timestamp_us}; use super::PreviewWrap; diff --git a/yazi-config/src/theme/theme.rs b/yazi-config/src/theme/theme.rs index b5e31a77..3566f5aa 100644 --- a/yazi-config/src/theme/theme.rs +++ b/yazi-config/src/theme/theme.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use anyhow::{Context, Result, bail}; use serde::Deserialize; use yazi_codegen::{DeserializeOver1, DeserializeOver2}; -use yazi_fs::{Xdg, expand_path, ok_or_not_found}; +use yazi_fs::{Xdg, ok_or_not_found, path::expand_path}; use super::{Filetype, Flavor, Icon}; use crate::Style; diff --git a/yazi-fs/src/file.rs b/yazi-fs/src/file.rs index 958e4f4f..f4afdefd 100644 --- a/yazi-fs/src/file.rs +++ b/yazi-fs/src/file.rs @@ -55,6 +55,9 @@ impl File { #[inline] pub fn url_owned(&self) -> Url { self.url.to_owned() } + #[inline] + pub fn uri(&self) -> &Urn { self.url.uri() } + #[inline] pub fn urn(&self) -> &Urn { self.url.urn() } diff --git a/yazi-fs/src/lib.rs b/yazi-fs/src/lib.rs index 771d61e1..a31a472a 100644 --- a/yazi-fs/src/lib.rs +++ b/yazi-fs/src/lib.rs @@ -1,8 +1,8 @@ #![allow(clippy::if_same_then_else, clippy::option_map_unit_fn)] -yazi_macro::mod_pub!(cha mounts provider); +yazi_macro::mod_pub!(cha mounts provider path); -yazi_macro::mod_flat!(calculator cwd file files filter fns op path sorter sorting stage xdg); +yazi_macro::mod_flat!(calculator cwd file files filter fns op sorter sorting stage xdg); pub fn init() { CWD.init(<_>::default()); diff --git a/yazi-fs/src/path/clean.rs b/yazi-fs/src/path/clean.rs new file mode 100644 index 00000000..cdeed84f --- /dev/null +++ b/yazi-fs/src/path/clean.rs @@ -0,0 +1,81 @@ +use std::{borrow::Cow, path::{Path, PathBuf}}; + +use yazi_shared::url::{Loc, Url}; + +pub fn clean_url<'a>(url: impl Into>) -> Url { + let cow = url.into(); + let (path, uri, urn) = clean_path_impl(&cow.loc, cow.loc.base().count(), cow.loc.trail().count()); + + let loc = Loc::with(path, uri, urn).expect("Failed to create Loc from cleaned path"); + match cow { + Cow::Borrowed(u) => Url { loc, scheme: u.scheme.clone() }, + Cow::Owned(u) => Url { loc, scheme: u.scheme }, + } +} + +fn clean_path_impl(path: &Path, base: usize, trail: usize) -> (PathBuf, usize, usize) { + use std::path::Component::*; + + let mut out = vec![]; + let mut uri_count = 0; + let mut urn_count = 0; + + macro_rules! push { + ($i:ident, $c:ident) => {{ + out.push($c); + if $i >= base { + uri_count += 1; + } + if $i >= trail { + urn_count += 1; + } + }}; + } + + for (i, c) in path.components().enumerate() { + match c { + CurDir => {} + ParentDir => match out.last() { + Some(RootDir) => {} + Some(Normal(_)) => _ = out.pop(), + None | Some(CurDir) | Some(ParentDir) | Some(Prefix(_)) => push!(i, c), + }, + c => push!(i, c), + } + } + + (if out.is_empty() { PathBuf::from(".") } else { out.iter().collect() }, uri_count, urn_count) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_clean_url() -> anyhow::Result<()> { + let cases = [ + // CurDir + ("archive://:3//./tmp/test.zip/foo/bar", "archive://:3//tmp/test.zip/foo/bar"), + ("archive://:3//tmp/./test.zip/foo/bar", "archive://:3//tmp/test.zip/foo/bar"), + ("archive://:3//tmp/./test.zip/./foo/bar", "archive://:3//tmp/test.zip/foo/bar"), + ("archive://:3//tmp/./test.zip/./foo/./bar/.", "archive://:3//tmp/test.zip/foo/bar"), + // ParentDir + ("archive://:3:2//../../tmp/test.zip/foo/bar", "archive://:3:2//tmp/test.zip/foo/bar"), + ("archive://:3:2//tmp/../../test.zip/foo/bar", "archive://:3:2//test.zip/foo/bar"), + ("archive://:4:2//tmp/test.zip/../../foo/bar", "archive://:2:2//foo/bar"), + ("archive://:5:2//tmp/test.zip/../../foo/bar", "archive://:3:2//foo/bar"), + ("archive://:4:4//tmp/test.zip/foo/bar/../../", "archive://:1:1//tmp/test.zip"), + ("archive://:5:4//tmp/test.zip/foo/bar/../../", "archive://:2:1//tmp/test.zip"), + ("archive://:4:4//tmp/test.zip/foo/bar/../../../", "archive:////tmp"), + ]; + + for (input, expected) in cases { + let input: Url = input.parse()?; + #[cfg(unix)] + assert_eq!(format!("{:?}", clean_url(input)), expected); + #[cfg(windows)] + assert_eq!(format!("{:?}", clean_url(input)).replace(r"\", "/"), expected.replace(r"\", "/")); + } + Ok(()) + } +} diff --git a/yazi-fs/src/path/expand.rs b/yazi-fs/src/path/expand.rs new file mode 100644 index 00000000..cbf6f668 --- /dev/null +++ b/yazi-fs/src/path/expand.rs @@ -0,0 +1,170 @@ +use std::{borrow::Cow, ffi::{OsStr, OsString}, path::{Path, PathBuf}}; + +use yazi_shared::url::{Loc, Url}; + +use crate::CWD; + +#[inline] +pub fn expand_url<'a>(url: impl Into>) -> Cow<'a, Url> { + let cow = url.into(); + match expand_url_impl(&cow) { + Cow::Borrowed(_) => cow, + Cow::Owned(url) => url.into(), + } +} + +// FIXME: VFS +#[inline] +pub fn expand_path(p: impl AsRef) -> PathBuf { + expand_url(Url::from(p.as_ref())).into_owned().loc.into_path() +} + +fn expand_url_impl(url: &Url) -> Cow<'_, Url> { + let (o_base, o_rest, o_urn) = url.loc.triple(); + + let n_base = expand_variables(o_base); + let n_rest = expand_variables(o_rest); + let n_urn = expand_variables(o_urn); + + let rest_diff = n_rest.components().count() as isize - o_rest.components().count() as isize; + let urn_diff = n_urn.components().count() as isize - o_urn.components().count() as isize; + + let uri_count = url.uri().count() as isize; + let urn_count = url.urn().count() as isize; + + let loc = Loc::with( + PathBuf::from_iter([n_base, n_rest, n_urn]), + (uri_count + rest_diff + urn_diff) as usize, + (urn_count + urn_diff) as usize, + ) + .expect("Failed to create Loc from expanded path"); + + let url = Url { loc, scheme: url.scheme.clone() }; + match absolute_url(&url) { + Cow::Borrowed(_) => url.into(), + Cow::Owned(u) => u.into(), + } +} + +fn expand_variables(p: &Path) -> Cow<'_, Path> { + // ${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 = p.as_os_str().as_encoded_bytes(); + let b = re.replace_all(b, |caps: ®ex::bytes::Captures| { + let name = caps.get(2).or_else(|| caps.get(1)).unwrap(); + str::from_utf8(name.as_bytes()) + .ok() + .and_then(std::env::var_os) + .map_or_else(|| caps.get(0).unwrap().as_bytes().to_owned(), |s| s.into_encoded_bytes()) + }); + + unsafe { + match b { + Cow::Borrowed(b) => Path::new(OsStr::from_encoded_bytes_unchecked(b)).into(), + Cow::Owned(b) => PathBuf::from(OsString::from_encoded_bytes_unchecked(b)).into(), + } + } +} + +fn absolute_url(url: &Url) -> Cow<'_, Url> { + let b = url.loc.as_os_str().as_encoded_bytes(); + let local = !url.scheme.is_virtual(); + + if cfg!(windows) && local && b.len() == 2 && b[1] == b':' && b[0].is_ascii_alphabetic() { + let loc = Loc::with( + format!(r"{}:\", b[0].to_ascii_uppercase() as char).into(), + if url.has_base() { 0 } else { 2 }, + if url.has_trail() { 0 } else { 2 }, + ) + .expect("Failed to create Loc from drive letter"); + Url { loc, scheme: url.scheme.clone() }.into() + } else if local + && let Ok(rest) = url.loc.strip_prefix("~/") + && let Some(home) = dirs::home_dir() + && home.is_absolute() + { + let add = home.components().count() - 1; // Home root ("~") has offset by the absolute root ("/") + let loc = Loc::with( + home.join(rest), + url.uri().count() + if url.has_base() { 0 } else { add }, + url.urn().count() + if url.has_trail() { 0 } else { add }, + ) + .expect("Failed to create Loc from home directory"); + Url { loc, scheme: url.scheme.clone() }.into() + } else if !url.is_absolute() { + let cwd = CWD.load(); + let loc = Loc::with(cwd.loc.join(&url.loc), url.uri().count(), url.urn().count()) + .expect("Failed to create Loc from relative path"); + Url { loc, scheme: cwd.scheme.clone() }.into() + } else { + url.into() + } +} + +#[cfg(test)] +mod tests { + use anyhow::Result; + + use super::*; + + #[cfg(unix)] + #[test] + fn test_expand_url() -> Result<()> { + unsafe { + std::env::set_var("FOO", "foo"); + std::env::set_var("BAR_BAZ", "bar/baz"); + std::env::set_var("BAR/BAZ", "bar_baz"); + std::env::set_var("EM/PT/Y", ""); + } + + let cases = [ + // Zero extra component expanded + ("archive:////tmp/test.zip/$FOO/bar", "archive:////tmp/test.zip/foo/bar"), + ("archive://:1//tmp/test.zip/$FOO/bar", "archive://:1//tmp/test.zip/foo/bar"), + ("archive://:2//tmp/test.zip/bar/$FOO", "archive://:2//tmp/test.zip/bar/foo"), + ("archive://:3//tmp/test.zip/$FOO/bar", "archive://:3//tmp/test.zip/foo/bar"), + ("archive://:3:1//tmp/test.zip/bar/$FOO", "archive://:3:1//tmp/test.zip/bar/foo"), + ("archive://:3:2//tmp/test.zip/$FOO/bar", "archive://:3:2//tmp/test.zip/foo/bar"), + ("archive://:3:3//tmp/test.zip/bar/$FOO", "archive://:3:3//tmp/test.zip/bar/foo"), + // +1 component + ("archive:////tmp/test.zip/$BAR_BAZ", "archive:////tmp/test.zip/bar/baz"), + ("archive://:1//tmp/test.zip/$BAR_BAZ", "archive://:2//tmp/test.zip/bar/baz"), + ("archive://:2//$BAR_BAZ/tmp/test.zip", "archive://:2//bar/baz/tmp/test.zip"), + ("archive://:2:1//tmp/test.zip/$BAR_BAZ", "archive://:3:2//tmp/test.zip/bar/baz"), + ("archive://:2:2//tmp/$BAR_BAZ/test.zip", "archive://:3:3//tmp/bar/baz/test.zip"), + ("archive://:2:2//$BAR_BAZ/tmp/test.zip", "archive://:2:2//bar/baz/tmp/test.zip"), + // -1 component + ("archive:////tmp/test.zip/${BAR/BAZ}", "archive:////tmp/test.zip/bar_baz"), + ("archive://:1//tmp/test.zip/${BAR/BAZ}", "archive://:1//tmp/test.zip/${BAR/BAZ}"), + ("archive://:1//tmp/${BAR/BAZ}/test.zip", "archive://:1//tmp/bar_baz/test.zip"), + ("archive://:2//tmp/test.zip/${BAR/BAZ}", "archive://:1//tmp/test.zip/bar_baz"), + ("archive://:2//tmp/${BAR/BAZ}/test.zip", "archive://:2//tmp/${BAR/BAZ}/test.zip"), + ("archive://:2:1//tmp/test.zip/${BAR/BAZ}", "archive://:2:1//tmp/test.zip/${BAR/BAZ}"), + ("archive://:2:1//tmp/${BAR/BAZ}/test.zip", "archive://:2:1//tmp/${BAR/BAZ}/test.zip"), + ("archive://:2:1//${BAR/BAZ}/tmp/test.zip", "archive://:2:1//bar_baz/tmp/test.zip"), + ("archive://:3:2//tmp/test.zip/${BAR/BAZ}", "archive://:2:1//tmp/test.zip/bar_baz"), + ("archive://:3:2//tmp/${BAR/BAZ}/test.zip", "archive://:3:2//tmp/${BAR/BAZ}/test.zip"), + ("archive://:3:3//tmp/test.zip/${BAR/BAZ}", "archive://:2:2//tmp/test.zip/bar_baz"), + ("archive://:3:3//tmp/${BAR/BAZ}/test.zip", "archive://:2:2//tmp/bar_baz/test.zip"), + // Zeros all components + ("archive:////${EM/PT/Y}", "archive:////"), + ("archive://:1//${EM/PT/Y}", "archive://:1//${EM/PT/Y}"), + ("archive://:2//${EM/PT/Y}", "archive://:2//${EM/PT/Y}"), + ("archive://:3//${EM/PT/Y}", "archive:////"), + ("archive://:4//${EM/PT/Y}", "archive://:1//"), + ]; + + for (input, expected) in cases { + let u: Url = input.parse()?; + assert_eq!(format!("{:?}", expand_url(u)), expected); + } + + Ok(()) + } +} diff --git a/yazi-fs/src/path/mod.rs b/yazi-fs/src/path/mod.rs new file mode 100644 index 00000000..09b2a3f5 --- /dev/null +++ b/yazi-fs/src/path/mod.rs @@ -0,0 +1 @@ +yazi_macro::mod_flat!(clean expand path); diff --git a/yazi-fs/src/path.rs b/yazi-fs/src/path/path.rs similarity index 63% rename from yazi-fs/src/path.rs rename to yazi-fs/src/path/path.rs index 5c19a29b..b994e635 100644 --- a/yazi-fs/src/path.rs +++ b/yazi-fs/src/path/path.rs @@ -1,96 +1,9 @@ -use std::{borrow::Cow, env, ffi::{OsStr, OsString}, future::Future, io, path::{Path, PathBuf}}; +use std::{borrow::Cow, ffi::{OsStr, OsString}, future::Future, io, path::PathBuf}; use anyhow::{Result, bail}; use yazi_shared::url::{Loc, Url}; -use crate::{CWD, provider}; - -pub fn clean_url<'a>(url: impl Into>) -> Cow<'a, Url> { - let url = url.into(); - let path = clean_path(&url.loc); - - if path.as_os_str() == url.loc.as_os_str() { - url - } else { - url.with(Loc::with_lossy(&clean_path(url.loc.base()), path)).into() - } -} - -fn clean_path(path: &Path) -> PathBuf { - use std::path::Component::*; - - let mut out = vec![]; - for c in path.components() { - match c { - CurDir => {} - ParentDir => match out.last() { - Some(RootDir) => {} - Some(Normal(_)) => _ = out.pop(), - None | Some(CurDir) | Some(ParentDir) | Some(Prefix(_)) => out.push(c), - }, - c => out.push(c), - } - } - - if out.is_empty() { PathBuf::from(".") } else { out.iter().collect() } -} - -// FIXME: VFS -#[inline] -pub fn expand_path(p: impl AsRef) -> PathBuf { - expand_url(Url::from(p.as_ref())).into_owned().loc.into_path() -} - -#[inline] -pub fn expand_url<'a>(url: impl Into>) -> Cow<'a, Url> { - let cow: Cow<'a, Url> = url.into(); - match _expand_url(&cow) { - Cow::Borrowed(_) => cow, - Cow::Owned(url) => url.into(), - } -} - -fn _expand_url(url: &Url) -> Cow<'_, Url> { - // ${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 = url.loc.as_os_str().as_encoded_bytes(); - let local = !url.scheme.is_virtual(); - - // Windows paths that only have a drive letter but no root, e.g. "D:" - #[cfg(windows)] - if local && b.len() == 2 && b[1] == b':' && b[0].is_ascii_alphabetic() { - return url.with(format!(r"{}:\", b[0].to_ascii_uppercase() as char)).into(); - } - - let b = re.replace_all(b, |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()) - }); - - let path: Cow<_> = unsafe { - match b { - Cow::Borrowed(b) => Path::new(OsStr::from_encoded_bytes_unchecked(b)).into(), - Cow::Owned(b) => PathBuf::from(OsString::from_encoded_bytes_unchecked(b)).into(), - } - }; - - if let Some(rest) = path.strip_prefix("~").ok().filter(|_| local) { - url.with(clean_path(&dirs::home_dir().unwrap_or_default().join(rest))).into() - } else if path.is_absolute() { - url.with(clean_path(&path)).into() - } else { - clean_url(CWD.load().join(path)) - } -} +use crate::provider; pub fn skip_url(url: &Url, n: usize) -> Cow<'_, OsStr> { let mut it = url.components(); @@ -164,7 +77,7 @@ pub fn url_relative_to<'a>(from: &Url, to: &'a Url) -> Result> { } if from.covariant(to) { - return Ok(to.with(Path::new(".")).into()); + return Ok(Url { loc: Loc::zeroed("."), scheme: to.scheme.clone() }.into()); } let (mut f_it, mut t_it) = (from.components(), to.components()); @@ -186,11 +99,11 @@ pub fn url_relative_to<'a>(from: &Url, to: &'a Url) -> Result> { let rest = t_head.into_iter().chain(t_it); let buf: PathBuf = dots.chain(rest).collect(); - Ok(to.with(buf).into()) + Ok(Url { loc: Loc::zeroed(buf), scheme: to.scheme.clone() }.into()) } #[cfg(windows)] -pub fn backslash_to_slash(p: &Path) -> Cow<'_, Path> { +pub fn backslash_to_slash(p: &std::path::Path) -> Cow<'_, std::path::Path> { let bytes = p.as_os_str().as_encoded_bytes(); // Fast path to skip if there are no backslashes diff --git a/yazi-fs/src/xdg.rs b/yazi-fs/src/xdg.rs index 4e287e27..e95541be 100644 --- a/yazi-fs/src/xdg.rs +++ b/yazi-fs/src/xdg.rs @@ -1,6 +1,6 @@ use std::{env, path::PathBuf}; -use crate::expand_path; +use crate::path::expand_path; pub struct Xdg; diff --git a/yazi-parser/src/mgr/cd.rs b/yazi-parser/src/mgr/cd.rs index 860ea78b..766e7f4c 100644 --- a/yazi-parser/src/mgr/cd.rs +++ b/yazi-parser/src/mgr/cd.rs @@ -1,7 +1,7 @@ use std::borrow::Cow; use mlua::{ExternalError, FromLua, IntoLua, Lua, Value}; -use yazi_fs::expand_url; +use yazi_fs::path::expand_url; use yazi_shared::{event::CmdCow, url::Url}; #[derive(Debug)] diff --git a/yazi-parser/src/mgr/copy.rs b/yazi-parser/src/mgr/copy.rs index 84c9c7a3..a47b083e 100644 --- a/yazi-parser/src/mgr/copy.rs +++ b/yazi-parser/src/mgr/copy.rs @@ -48,7 +48,7 @@ impl CopySeparator { pub fn transform + ?Sized>(self, p: &T) -> Cow<'_, OsStr> { #[cfg(windows)] if self == Self::Unix { - return match yazi_fs::backslash_to_slash(p.as_ref()) { + return match yazi_fs::path::backslash_to_slash(p.as_ref()) { Cow::Owned(p) => Cow::Owned(p.into_os_string()), Cow::Borrowed(p) => Cow::Borrowed(p.as_os_str()), }; diff --git a/yazi-parser/src/mgr/reveal.rs b/yazi-parser/src/mgr/reveal.rs index 13f464ff..7dd3dcb6 100644 --- a/yazi-parser/src/mgr/reveal.rs +++ b/yazi-parser/src/mgr/reveal.rs @@ -1,7 +1,7 @@ use std::borrow::Cow; use mlua::{ExternalError, FromLua, IntoLua, Lua, Value}; -use yazi_fs::expand_url; +use yazi_fs::path::expand_url; use yazi_shared::{event::CmdCow, url::Url}; use crate::mgr::CdSource; diff --git a/yazi-parser/src/mgr/tab_create.rs b/yazi-parser/src/mgr/tab_create.rs index aa977cb6..92065266 100644 --- a/yazi-parser/src/mgr/tab_create.rs +++ b/yazi-parser/src/mgr/tab_create.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; use mlua::{ExternalError, FromLua, IntoLua, Lua, Value}; use yazi_boot::BOOT; -use yazi_fs::expand_url; +use yazi_fs::path::expand_url; use yazi_shared::{event::CmdCow, url::Url}; #[derive(Debug)] diff --git a/yazi-plugin/src/fs/fs.rs b/yazi-plugin/src/fs/fs.rs index 83ceb6f6..fcc1a2ab 100644 --- a/yazi-plugin/src/fs/fs.rs +++ b/yazi-plugin/src/fs/fs.rs @@ -155,7 +155,7 @@ fn calc_size(lua: &Lua) -> mlua::Result { fn expand_url(lua: &Lua) -> mlua::Result { lua.create_function(|lua, value: Value| { - use yazi_fs::expand_url; + use yazi_fs::path::expand_url; match &value { Value::String(s) => Url::new(expand_url(Url::try_from(s.as_bytes().as_ref())?)).into_lua(lua), Value::UserData(ud) => match expand_url(&*ud.borrow::()?) { @@ -169,7 +169,7 @@ fn expand_url(lua: &Lua) -> mlua::Result { fn unique_name(lua: &Lua) -> mlua::Result { lua.create_async_function(|lua, url: UrlRef| async move { - match yazi_fs::unique_name(url.clone(), async { false }).await { + match yazi_fs::path::unique_name(url.clone(), async { false }).await { Ok(u) => Url::new(u).into_lua_multi(&lua), Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua), } diff --git a/yazi-scheduler/src/file/file.rs b/yazi-scheduler/src/file/file.rs index 68b41235..c2868335 100644 --- a/yazi-scheduler/src/file/file.rs +++ b/yazi-scheduler/src/file/file.rs @@ -4,7 +4,7 @@ use anyhow::{Result, anyhow}; use tokio::{io::{self, ErrorKind::{AlreadyExists, NotFound}}, sync::mpsc}; use tracing::warn; use yazi_config::YAZI; -use yazi_fs::{SizeCalculator, cha::Cha, copy_with_progress, maybe_exists, ok_or_not_found, provider::{self, DirEntry}, skip_url, url_relative_to}; +use yazi_fs::{SizeCalculator, cha::Cha, copy_with_progress, maybe_exists, ok_or_not_found, path::{skip_url, url_relative_to}, provider::{self, DirEntry}}; use yazi_shared::{Id, url::Url}; use super::{FileIn, FileInDelete, FileInHardlink, FileInLink, FileInPaste, FileInTrash}; diff --git a/yazi-scheduler/src/scheduler.rs b/yazi-scheduler/src/scheduler.rs index 18589bb1..c1b0404b 100644 --- a/yazi-scheduler/src/scheduler.rs +++ b/yazi-scheduler/src/scheduler.rs @@ -6,7 +6,7 @@ use parking_lot::Mutex; use tokio::{select, sync::mpsc::{self, UnboundedReceiver}, task::JoinHandle}; use yazi_config::{YAZI, plugin::{Fetcher, Preloader}}; use yazi_dds::Pump; -use yazi_fs::{must_be_dir, provider, remove_dir_clean, unique_name}; +use yazi_fs::{must_be_dir, path::unique_name, provider, remove_dir_clean}; use yazi_parser::{app::PluginOpt, tasks::ProcessExecOpt}; use yazi_proxy::MgrProxy; use yazi_shared::{Id, Throttle, url::Url}; diff --git a/yazi-shared/src/url/component.rs b/yazi-shared/src/url/component.rs index 26f6e0c2..be7a5bf3 100644 --- a/yazi-shared/src/url/component.rs +++ b/yazi-shared/src/url/component.rs @@ -150,11 +150,11 @@ mod tests { #[test] fn test_collect() { let search: Url = "search://keyword//root/projects/yazi".parse().unwrap(); - assert_eq!(search.loc.urn().as_os_str(), OsStr::new("")); + assert_eq!(search.loc.uri().as_os_str(), OsStr::new("")); assert_eq!(search.scheme, Scheme::Search("keyword".to_owned())); let item = search.join("main.rs"); - assert_eq!(item.loc.urn().as_os_str(), OsStr::new("main.rs")); + assert_eq!(item.loc.uri().as_os_str(), OsStr::new("main.rs")); assert_eq!(item.scheme, Scheme::Search("keyword".to_owned())); let u: Url = item.components().take(4).collect(); diff --git a/yazi-shared/src/url/encode.rs b/yazi-shared/src/url/encode.rs index e1ffcbba..1c1f63d3 100644 --- a/yazi-shared/src/url/encode.rs +++ b/yazi-shared/src/url/encode.rs @@ -25,18 +25,21 @@ impl<'a> Encode<'a> { #[inline] fn urn(loc: &'a Loc) -> impl Display { - struct D(usize); + struct D(usize, usize); impl Display for D { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if self.0 != 0 { - write!(f, ":{}", self.0)?; + let (uri, urn) = (self.0, self.1); + match (uri != 0, urn != 0) { + (true, true) => write!(f, ":{uri}:{urn}"), + (true, false) => write!(f, ":{uri}"), + (false, true) => write!(f, "::{urn}"), + (false, false) => Ok(()), } - Ok(()) } } - D(loc.urn().components().count()) + D(loc.uri().count(), loc.urn().count()) } } diff --git a/yazi-shared/src/url/loc.rs b/yazi-shared/src/url/loc.rs index 8b8e1d97..e781ac22 100644 --- a/yazi-shared/src/url/loc.rs +++ b/yazi-shared/src/url/loc.rs @@ -7,6 +7,7 @@ use crate::url::{Urn, UrnBuf}; #[derive(Clone, Default)] pub struct Loc { inner: PathBuf, + uri: usize, urn: usize, } @@ -40,7 +41,11 @@ impl Hash for Loc { impl Debug for Loc { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.debug_struct("Loc").field("path", &self.inner).field("urn", &self.urn()).finish() + f.debug_struct("Loc") + .field("path", &self.inner) + .field("uri", &self.uri()) + .field("urn", &self.urn()) + .finish() } } @@ -55,8 +60,8 @@ impl From for Loc { impl From for Loc { fn from(path: PathBuf) -> Self { let Some(name) = path.file_name() else { - let urn = path.as_os_str().len(); - return Self { inner: path, urn }; + let uri = path.as_os_str().len(); + return Self { inner: path, uri, urn: 0 }; }; let name_len = name.len(); @@ -71,6 +76,7 @@ impl From for Loc { bytes.truncate(prefix_len + name_len); Self { inner: PathBuf::from(unsafe { OsString::from_encoded_bytes_unchecked(bytes) }), + uri: name_len, urn: name_len, } } @@ -85,31 +91,66 @@ impl> From<&T> for Loc { } impl Loc { - pub fn zeroed(path: PathBuf) -> Self { - let mut loc = Self::from(path); - loc.urn = 0; + pub fn new(path: impl Into, base: &Path, trail: &Path) -> Self { + let mut loc = Self::from(path.into()); + loc.uri = + loc.inner.strip_prefix(base).expect("Loc must start with the given base").as_os_str().len(); + loc.urn = + loc.inner.strip_prefix(trail).expect("Loc must start with the given trail").as_os_str().len(); loc } - pub fn with(urn: usize, path: PathBuf) -> Result { - let mut loc = Self::from(path); - let mut it = loc.inner.components(); - for _ in 0..urn { - if it.next_back().is_none() { - bail!("URN exceeds its entire URL"); - } + pub fn with(path: PathBuf, uri: usize, urn: usize) -> Result { + if urn > uri { + bail!("URN cannot be longer than URI"); } - loc.urn = loc.strip_prefix(it).unwrap().as_os_str().len(); + let mut loc = Self::from(path); + if uri == 0 { + (loc.uri, loc.urn) = (0, 0); + return Ok(loc); + } else if urn == 0 { + loc.urn = 0; + } + + let mut it = loc.inner.components(); + for i in 1..=uri { + if it.next_back().is_none() { + bail!("URI exceeds the entire URL"); + } + if i == urn { + loc.urn = loc.strip_prefix(it.clone()).unwrap().as_os_str().len(); + } + if i == uri { + loc.uri = loc.strip_prefix(it).unwrap().as_os_str().len(); + break; + } + } Ok(loc) } - pub fn with_lossy(base: &Path, path: PathBuf) -> Self { - let mut loc = Self::from(path); - loc.urn = loc.inner.strip_prefix(base).unwrap_or(&loc.inner).as_os_str().len(); + pub fn zeroed(path: impl Into) -> Self { + let mut loc = Self::from(path.into()); + (loc.uri, loc.urn) = (0, 0); loc } + pub fn floated(path: impl Into, base: &Path) -> Self { + let mut loc = Self::from(path.into()); + loc.uri = + loc.inner.strip_prefix(base).expect("Loc must start with the given base").as_os_str().len(); + loc + } + + #[inline] + pub fn uri(&self) -> &Urn { + Urn::new(unsafe { + OsStr::from_encoded_bytes_unchecked( + self.bytes().get_unchecked(self.bytes().len() - self.uri..), + ) + }) + } + #[inline] pub fn urn(&self) -> &Urn { Urn::new(unsafe { @@ -132,16 +173,30 @@ impl Loc { } if old.len() > new.len() { - self.urn -= old.len() - new.len(); + let n = old.len() - new.len(); + (self.uri, self.urn) = (self.uri - n, self.urn - n); } else { - self.urn += new.len() - old.len(); + let n = new.len() - old.len(); + (self.uri, self.urn) = (self.uri + n, self.urn + n); } self.inner.set_file_name(new); } #[inline] - pub fn base(&self) -> &Path { - Path::new(unsafe { + pub fn base(&self) -> &Urn { + Urn::new(unsafe { + OsStr::from_encoded_bytes_unchecked( + self.bytes().get_unchecked(..self.bytes().len() - self.uri), + ) + }) + } + + #[inline] + pub fn has_base(&self) -> bool { self.bytes().len() != self.uri } + + #[inline] + pub fn trail(&self) -> &Urn { + Urn::new(unsafe { OsStr::from_encoded_bytes_unchecked( self.bytes().get_unchecked(..self.bytes().len() - self.urn), ) @@ -149,15 +204,15 @@ impl Loc { } #[inline] - pub fn has_base(&self) -> bool { self.bytes().len() != self.urn } + pub fn has_trail(&self) -> bool { self.bytes().len() != self.urn } #[inline] pub fn rebase(&self, parent: &Path) -> Self { - debug_assert!(self.urn == self.name().len()); + debug_assert!(self.uri == self.name().len()); let path = parent.join(self.name()); debug_assert!(path.file_name().is_some_and(|s| s.len() == self.name().len())); - Self { inner: path, urn: self.urn } + Self { inner: path, uri: self.uri, urn: self.uri } } #[inline] @@ -166,6 +221,23 @@ impl Loc { #[inline] pub fn into_path(self) -> PathBuf { self.inner } + #[inline] + pub fn triple(&self) -> (&Path, &Path, &Path) { + let len = self.bytes().len(); + + let base = ..len - self.uri; + let rest = len - self.uri..len - self.urn; + let urn = len - self.urn..; + + unsafe { + ( + Path::new(OsStr::from_encoded_bytes_unchecked(self.bytes().get_unchecked(base))), + Path::new(OsStr::from_encoded_bytes_unchecked(self.bytes().get_unchecked(rest))), + Path::new(OsStr::from_encoded_bytes_unchecked(self.bytes().get_unchecked(urn))), + ) + } + } + #[inline] fn bytes(&self) -> &[u8] { self.inner.as_os_str().as_encoded_bytes() } } @@ -177,75 +249,91 @@ mod tests { #[test] fn test_new() { let loc: Loc = Path::new("/").into(); - assert_eq!(loc.urn(), Urn::new("/")); + assert_eq!(loc.uri().as_os_str(), OsStr::new("/")); + assert_eq!(loc.urn().as_os_str(), OsStr::new("")); assert_eq!(loc.name(), OsStr::new("")); - assert_eq!(loc.base(), Path::new("")); + assert_eq!(loc.base().as_os_str(), OsStr::new("")); + assert_eq!(loc.trail().as_os_str(), OsStr::new("/")); let loc: Loc = Path::new("/root").into(); - assert_eq!(loc.urn(), Urn::new("root")); + assert_eq!(loc.uri().as_os_str(), OsStr::new("root")); + assert_eq!(loc.urn().as_os_str(), OsStr::new("root")); assert_eq!(loc.name(), OsStr::new("root")); - assert_eq!(loc.base(), Path::new("/")); + assert_eq!(loc.base().as_os_str(), OsStr::new("/")); + assert_eq!(loc.trail().as_os_str(), OsStr::new("/")); let loc: Loc = Path::new("/root/code/foo/").into(); - assert_eq!(loc.urn(), Urn::new("foo")); + assert_eq!(loc.uri().as_os_str(), OsStr::new("foo")); + assert_eq!(loc.urn().as_os_str(), OsStr::new("foo")); assert_eq!(loc.name(), OsStr::new("foo")); - assert_eq!(loc.base(), Path::new("/root/code/")); + assert_eq!(loc.base().as_os_str(), OsStr::new("/root/code/")); + assert_eq!(loc.trail().as_os_str(), OsStr::new("/root/code/")); } #[test] fn test_with() -> Result<()> { - let loc = Loc::with(0, "/".into())?; + let loc = Loc::with("/".into(), 0, 0)?; + assert_eq!(loc.uri().as_os_str(), OsStr::new("")); assert_eq!(loc.urn().as_os_str(), OsStr::new("")); assert_eq!(loc.name(), OsStr::new("")); assert_eq!(loc.base().as_os_str(), OsStr::new("/")); + assert_eq!(loc.trail().as_os_str(), OsStr::new("/")); - let loc = Loc::with(1, "/root/code/".into())?; + let loc = Loc::with("/root/code/".into(), 1, 1)?; + assert_eq!(loc.uri().as_os_str(), OsStr::new("code")); assert_eq!(loc.urn().as_os_str(), OsStr::new("code")); assert_eq!(loc.name(), OsStr::new("code")); assert_eq!(loc.base().as_os_str(), OsStr::new("/root/")); + assert_eq!(loc.trail().as_os_str(), OsStr::new("/root/")); - let loc = Loc::with(2, "/root/code/foo//".into())?; + let loc = Loc::with("/root/code/foo//".into(), 2, 1)?; + assert_eq!(loc.uri().as_os_str(), OsStr::new("code/foo")); + assert_eq!(loc.urn().as_os_str(), OsStr::new("foo")); + assert_eq!(loc.name(), OsStr::new("foo")); + assert_eq!(loc.base().as_os_str(), OsStr::new("/root/")); + assert_eq!(loc.trail().as_os_str(), OsStr::new("/root/code/")); + + let loc = Loc::with("/root/code/foo//".into(), 2, 2)?; + assert_eq!(loc.uri().as_os_str(), OsStr::new("code/foo")); assert_eq!(loc.urn().as_os_str(), OsStr::new("code/foo")); assert_eq!(loc.name(), OsStr::new("foo")); assert_eq!(loc.base().as_os_str(), OsStr::new("/root/")); + assert_eq!(loc.trail().as_os_str(), OsStr::new("/root/")); + + let loc = Loc::with("/root/code/foo//bar/".into(), 2, 2)?; + assert_eq!(loc.uri().as_os_str(), OsStr::new("foo//bar")); + assert_eq!(loc.urn().as_os_str(), OsStr::new("foo//bar")); + assert_eq!(loc.name(), OsStr::new("bar")); + assert_eq!(loc.base().as_os_str(), OsStr::new("/root/code/")); + assert_eq!(loc.trail().as_os_str(), OsStr::new("/root/code/")); + + let loc = Loc::with("/root/code/foo//bar/".into(), 3, 2)?; + assert_eq!(loc.uri().as_os_str(), OsStr::new("code/foo//bar")); + assert_eq!(loc.urn().as_os_str(), OsStr::new("foo//bar")); + assert_eq!(loc.name(), OsStr::new("bar")); + assert_eq!(loc.base().as_os_str(), OsStr::new("/root/")); + assert_eq!(loc.trail().as_os_str(), OsStr::new("/root/code/")); Ok(()) } #[test] - fn test_with_lossy() { - let loc = Loc::with_lossy(Path::new("/"), "/".into()); - assert_eq!(loc.urn().as_os_str(), OsStr::new("")); - assert_eq!(loc.name(), OsStr::new("")); - assert_eq!(loc.base().as_os_str(), OsStr::new("/")); - - let loc = Loc::with_lossy(Path::new("/root/"), "/root/code/".into()); - assert_eq!(loc.urn().as_os_str(), OsStr::new("code")); - assert_eq!(loc.name(), OsStr::new("code")); - assert_eq!(loc.base().as_os_str(), OsStr::new("/root/")); - - let loc = Loc::with_lossy(Path::new("/root//"), "/root/code/foo//".into()); - assert_eq!(loc.urn().as_os_str(), OsStr::new("code/foo")); - assert_eq!(loc.name(), OsStr::new("foo")); - assert_eq!(loc.base().as_os_str(), OsStr::new("/root/")); - } - - #[test] - fn test_set_name() { + fn test_set_name() -> Result<()> { const S: char = std::path::MAIN_SEPARATOR; - let mut loc = Loc::with_lossy(Path::new("/root"), "/root/code/foo/".into()); - assert_eq!(loc.urn().as_os_str(), OsStr::new("code/foo")); + let mut loc = Loc::with("/root/code/foo/".into(), 2, 1)?; + assert_eq!(loc.uri().as_os_str(), OsStr::new("code/foo")); assert_eq!(loc.name(), OsStr::new("foo")); assert_eq!(loc.base().as_os_str(), OsStr::new("/root/")); loc.set_name("bar.txt"); - assert_eq!(loc.urn().as_os_str(), OsString::from(format!("code{S}bar.txt"))); + assert_eq!(loc.uri().as_os_str(), OsString::from(format!("code{S}bar.txt"))); assert_eq!(loc.name(), OsStr::new("bar.txt")); assert_eq!(loc.base().as_os_str(), OsStr::new("/root/")); loc.set_name("baz"); - assert_eq!(loc.urn().as_os_str(), OsString::from(format!("code{S}baz"))); + assert_eq!(loc.uri().as_os_str(), OsString::from(format!("code{S}baz"))); assert_eq!(loc.name(), OsStr::new("baz")); assert_eq!(loc.base().as_os_str(), OsStr::new("/root/")); + Ok(()) } } diff --git a/yazi-shared/src/url/scheme.rs b/yazi-shared/src/url/scheme.rs index fdd021ce..bc33eb27 100644 --- a/yazi-shared/src/url/scheme.rs +++ b/yazi-shared/src/url/scheme.rs @@ -36,7 +36,10 @@ impl Scheme { } } - pub(super) fn parse(bytes: &[u8], skip: &mut usize) -> Result<(Self, bool, Option)> { + pub(super) fn parse( + bytes: &[u8], + skip: &mut usize, + ) -> Result<(Self, bool, Option<(usize, usize)>)> { let Some((mut protocol, rest)) = bytes.split_by_seq(b"://") else { return Ok((Self::Regular, false, None)); }; @@ -53,16 +56,16 @@ impl Scheme { let (scheme, port) = match protocol { b"regular" => (Self::Regular, None), b"search" => { - let (domain, port) = Self::decode_param(rest, skip)?; - (Self::Search(domain), Some(port)) + let (domain, uri, urn) = Self::decode_param(rest, skip)?; + (Self::Search(domain), Some((uri, urn))) } b"archive" => { - let (domain, port) = Self::decode_param(rest, skip)?; - (Self::Archive(domain), Some(port)) + let (domain, uri, urn) = Self::decode_param(rest, skip)?; + (Self::Archive(domain), Some((uri, urn))) } b"sftp" => { - let (domain, port) = Self::decode_param(rest, skip)?; - (Self::Sftp(domain), Some(port)) + let (domain, uri, urn) = Self::decode_param(rest, skip)?; + (Self::Sftp(domain), Some((uri, urn))) } _ => bail!("Could not parse protocol from URL: {}", String::from_utf8_lossy(bytes)), }; @@ -94,25 +97,62 @@ impl Scheme { } } - fn decode_param(bytes: &[u8], skip: &mut usize) -> Result<(String, usize)> { + fn decode_param(bytes: &[u8], skip: &mut usize) -> Result<(String, usize, usize)> { let mut len = bytes.iter().copied().take_while(|&b| b != b'/').count(); let slash = bytes.get(len).is_some_and(|&b| b == b'/'); *skip += len + slash as usize; - let port = Self::decode_port(&bytes[..len], &mut len)?; + let (uri, urn) = Self::decode_port(&bytes[..len], &mut len)?; let domain = match Cow::from(percent_decode(&bytes[..len])) { Cow::Borrowed(b) => str::from_utf8(b)?.to_owned(), Cow::Owned(b) => String::from_utf8(b)?, }; - Ok((domain, port)) + Ok((domain, uri, urn)) } - fn decode_port(bytes: &[u8], skip: &mut usize) -> Result { - let Some(idx) = bytes.iter().rposition(|&b| b == b':') else { return Ok(0) }; - let len = bytes.len() - idx; + fn decode_port(bytes: &[u8], skip: &mut usize) -> anyhow::Result<(usize, usize)> { + let Some(a_idx) = bytes.iter().rposition(|&b| b == b':') else { return Ok((0, 0)) }; + let a_len = bytes.len() - a_idx; + *skip -= a_len; + let a = if a_len == 1 { 0 } else { str::from_utf8(&bytes[a_idx + 1..])?.parse()? }; - *skip -= len; - Ok(if len == 1 { 0 } else { str::from_utf8(&bytes[idx + 1..])?.parse()? }) + let Some(b_idx) = bytes[..a_idx].iter().rposition(|&b| b == b':') else { return Ok((a, 0)) }; + let b_len = bytes[..a_idx].len() - b_idx; + *skip -= b_len; + let b = if b_len == 1 { 0 } else { str::from_utf8(&bytes[b_idx + 1..a_idx])?.parse()? }; + + Ok((b, a)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_decode_port() -> Result<()> { + fn assert(s: &str, uri: usize, urn: usize, len: usize) -> Result<()> { + let mut n = usize::MAX; + let port = Scheme::decode_port(s.as_bytes(), &mut n)?; + assert_eq!((port.0, port.1, usize::MAX - n), (uri, urn, len)); + Ok(()) + } + + // Zeros + assert("", 0, 0, 0)?; + assert(":", 0, 0, 1)?; + assert("::", 0, 0, 2)?; + + // URI + assert(":2", 2, 0, 2)?; + assert(":2:", 2, 0, 3)?; + assert(":22:", 22, 0, 4)?; + + // URN + assert("::1", 0, 1, 3)?; + assert(":2:1", 2, 1, 4)?; + assert(":22:11", 22, 11, 6)?; + Ok(()) } } diff --git a/yazi-shared/src/url/url.rs b/yazi-shared/src/url/url.rs index 740a925e..83d0c32c 100644 --- a/yazi-shared/src/url/url.rs +++ b/yazi-shared/src/url/url.rs @@ -45,7 +45,8 @@ impl TryFrom<&[u8]> for Url { fn try_from(bytes: &[u8]) -> Result { let (scheme, path, port) = Self::parse(bytes)?; - let loc = if let Some(urn) = port { Loc::with(urn, path)? } else { Loc::from(path) }; + let loc = + if let Some((uri, urn)) = port { Loc::with(path, uri, urn)? } else { Loc::from(path) }; Ok(Self { loc, scheme }) } @@ -85,13 +86,6 @@ impl From> for Url { } impl Url { - #[inline] - pub fn with(&self, loc: impl Into) -> Self { - let loc: Loc = loc.into(); - // FIXME: simplify this - Self { loc: Loc::with_lossy(self.loc.base(), loc.into_path()), scheme: self.scheme.clone() } - } - #[inline] pub fn base(&self) -> Url { use Scheme as S; @@ -99,22 +93,25 @@ impl Url { let loc: Loc = self.loc.base().into(); match self.scheme { S::Regular => Self { loc, scheme: S::Regular }, - S::Search(_) => self.with(loc), - S::Archive(_) => self.with(loc), - S::Sftp(_) => self.with(loc), + S::Search(_) => Self { loc, scheme: self.scheme.clone() }, + S::Archive(_) => Self { loc, scheme: self.scheme.clone() }, + S::Sftp(_) => Self { loc, scheme: self.scheme.clone() }, } } pub fn join(&self, path: impl AsRef) -> Self { use Scheme as S; - let loc: Loc = self.loc.join(path).into(); - match self.scheme { - S::Regular => Self { loc, scheme: S::Regular }, - S::Search(_) => self.with(loc), - S::Archive(_) => self.with(loc), - S::Sftp(_) => Self { loc, scheme: self.scheme.clone() }, - } + let join = self.loc.join(path); + + let loc = match self.scheme { + S::Regular => join.into(), + S::Search(_) => Loc::new(join, self.loc.base(), self.loc.base()), + S::Archive(_) => Loc::floated(join, self.loc.base()), + S::Sftp(_) => join.into(), + }; + + Self { loc, scheme: self.scheme.clone() } } #[inline] @@ -135,17 +132,29 @@ impl Url { use Scheme as S; let parent = self.loc.parent()?; - let urn = self.loc.urn(); + let uri = self.loc.uri(); Some(match self.scheme { + // Regular S::Regular => Self { loc: parent.into(), scheme: S::Regular }, - S::Search(_) if urn.is_empty() => Self { loc: parent.into(), scheme: S::Regular }, - S::Search(_) => self.with(parent), + // Search + S::Search(_) if uri.is_empty() => Self { loc: parent.into(), scheme: S::Regular }, + S::Search(_) => Self { + loc: Loc::new(parent, self.loc.base(), self.loc.base()), + scheme: self.scheme.clone(), + }, - S::Archive(_) if urn.is_empty() => Self { loc: parent.into(), scheme: S::Regular }, - S::Archive(_) => self.with(parent), + // Archive + S::Archive(_) if uri.is_empty() => Self { loc: parent.into(), scheme: S::Regular }, + S::Archive(_) if uri.nth(1).is_none() => { + Self { loc: Loc::zeroed(parent), scheme: self.scheme.clone() } + } + S::Archive(_) => { + Self { loc: Loc::floated(parent, self.loc.base()), scheme: self.scheme.clone() } + } + // SFTP S::Sftp(_) => Self { loc: parent.into(), scheme: self.scheme.clone() }, }) } @@ -168,10 +177,10 @@ impl Url { (S::Search(_), S::Regular) => Some(prefix), // Only the entry of archives is a local file - (S::Regular, S::Archive(_)) => Some(prefix).filter(|_| base.urn().is_empty()), - (S::Search(_), S::Archive(_)) => Some(prefix).filter(|_| base.urn().is_empty()), - (S::Archive(_), S::Regular) => Some(prefix).filter(|_| self.urn().is_empty()), - (S::Archive(_), S::Search(_)) => Some(prefix).filter(|_| self.urn().is_empty()), + (S::Regular, S::Archive(_)) => Some(prefix).filter(|_| base.uri().is_empty()), + (S::Search(_), S::Archive(_)) => Some(prefix).filter(|_| base.uri().is_empty()), + (S::Archive(_), S::Regular) => Some(prefix).filter(|_| self.uri().is_empty()), + (S::Archive(_), S::Search(_)) => Some(prefix).filter(|_| self.uri().is_empty()), // Independent virtual file space (S::Regular, S::Sftp(_)) => None, @@ -203,7 +212,7 @@ impl Url { self.loc.rebase(parent).into() } - pub fn parse(bytes: &[u8]) -> Result<(Scheme, PathBuf, Option)> { + pub fn parse(bytes: &[u8]) -> Result<(Scheme, PathBuf, Option<(usize, usize)>)> { let mut skip = 0; let (scheme, tilde, port) = Scheme::parse(bytes, &mut skip)?; @@ -302,30 +311,94 @@ mod tests { use super::*; #[test] - fn test_search() -> Result<()> { + fn test_join() -> anyhow::Result<()> { + let cases = [ + // Regular + ("/a", "b/c", "regular:///a/b/c"), + // Search + ("search://kw//a", "b/c", "search://kw:2:2//a/b/c"), + ("search://kw:2:2//a/b/c", "d/e", "search://kw:4:4//a/b/c/d/e"), + // Archive + ("archive:////a/b.zip", "c/d", "archive://:2:1//a/b.zip/c/d"), + ("archive://:2:1//a/b.zip/c/d", "e/f", "archive://:4:1//a/b.zip/c/d/e/f"), + ("archive://:2:2//a/b.zip/c/d", "e/f", "archive://:4:1//a/b.zip/c/d/e/f"), + // SFTP + ("sftp://remote//a", "b/c", "sftp://remote:1:1//a/b/c"), + ("sftp://remote:1:1//a/b/c", "d/e", "sftp://remote:1:1//a/b/c/d/e"), + // Relative + ("search://kw", "b/c", "search://kw:2:2/b/c"), + ("search://kw/", "b/c", "search://kw:2:2/b/c"), + ]; + + for (base, path, expected) in cases { + let base: Url = base.parse()?; + #[cfg(unix)] + assert_eq!(format!("{:?}", base.join(path)), expected); + #[cfg(windows)] + assert_eq!(format!("{:?}", base.join(path)).replace(r"\", "/"), expected.replace(r"\", "/")); + } + + Ok(()) + } + + #[test] + fn test_parent_url() -> anyhow::Result<()> { + let cases = [ + // Regular + ("/a", Some("regular:///")), + ("/", None), + // Search + ("search://kw:2:2//a/b/c", Some("search://kw:1:1//a/b")), + ("search://kw:1:1//a/b", Some("search://kw//a")), + ("search://kw//a", Some("regular:///")), + // Archive + ("archive://:2:1//a/b.zip/c/d", Some("archive://:1:1//a/b.zip/c")), + ("archive://:1:1//a/b.zip/c", Some("archive:////a/b.zip")), + ("archive:////a/b.zip", Some("regular:///a")), + // SFTP + ("sftp://remote:1:1//a/b", Some("sftp://remote:1:1//a")), + ("sftp://remote:1:1//a", Some("sftp://remote:1//")), + ("sftp://remote:1//", None), + ("sftp://remote//", None), + // Relative + ("search://kw:2:2/a/b", Some("search://kw:1:1/a")), + ("search://kw:1:1/a", Some("search://kw/")), + ("search://kw/", None), + ]; + + for (path, expected) in cases { + let path: Url = path.parse()?; + assert_eq!(path.parent_url().map(|u| format!("{:?}", u)).as_deref(), expected); + } + + Ok(()) + } + + #[test] + fn test_into_search() -> Result<()> { const S: char = std::path::MAIN_SEPARATOR; - let u: Url = "/root/project".parse()?; - assert_eq!(format!("{u:?}"), "regular:///root/project"); + let u: Url = "/root".parse()?; + assert_eq!(format!("{u:?}"), "regular:///root"); - let u = u.into_search("readme"); - assert_eq!(format!("{u:?}"), "search://readme//root/project"); - assert_eq!(format!("{:?}", u.parent_url().unwrap()), "regular:///root"); + let u = u.into_search("kw"); + assert_eq!(format!("{u:?}"), "search://kw//root"); + assert_eq!(format!("{:?}", u.parent_url().unwrap()), "regular:///"); let u = u.join("examples"); - assert_eq!(format!("{u:?}"), format!("search://readme:1//root/project{S}examples")); + assert_eq!(format!("{u:?}"), format!("search://kw:1:1//root{S}examples")); let u = u.join("README.md"); - assert_eq!(format!("{u:?}"), format!("search://readme:2//root/project{S}examples{S}README.md")); + assert_eq!(format!("{u:?}"), format!("search://kw:2:2//root{S}examples{S}README.md")); let u = u.parent_url().unwrap(); - assert_eq!(format!("{u:?}"), format!("search://readme:1//root/project{S}examples")); + assert_eq!(format!("{u:?}"), format!("search://kw:1:1//root{S}examples")); let u = u.parent_url().unwrap(); - assert_eq!(format!("{u:?}"), "search://readme//root/project"); + assert_eq!(format!("{u:?}"), "search://kw//root"); let u = u.parent_url().unwrap(); - assert_eq!(format!("{u:?}"), "regular:///root"); + assert_eq!(format!("{u:?}"), "regular:///"); Ok(()) } diff --git a/yazi-shared/src/url/urn.rs b/yazi-shared/src/url/urn.rs index e850b05a..303db99c 100644 --- a/yazi-shared/src/url/urn.rs +++ b/yazi-shared/src/url/urn.rs @@ -1,4 +1,4 @@ -use std::{borrow::{Borrow, Cow}, ffi::OsStr, ops::Deref, path::{Path, PathBuf}}; +use std::{borrow::{Borrow, Cow}, ffi::OsStr, ops::Deref, path::{Component, Path, PathBuf}}; use serde::Serialize; @@ -15,6 +15,12 @@ impl Urn { #[inline] pub fn name(&self) -> Option<&OsStr> { self.0.file_name() } + #[inline] + pub fn count(&self) -> usize { self.0.components().count() } + + #[inline] + pub fn nth(&self, n: usize) -> Option> { self.0.components().nth(n) } + #[inline] pub fn encoded_bytes(&self) -> &[u8] { self.0.as_os_str().as_encoded_bytes() } @@ -49,8 +55,8 @@ impl ToOwned for Urn { fn to_owned(&self) -> Self::Owned { UrnBuf(self.0.to_owned()) } } -impl PartialEq for Urn { - fn eq(&self, other: &OsStr) -> bool { self.0 == other } +impl> PartialEq for Urn { + fn eq(&self, other: &T) -> bool { self.0 == other.as_ref() } } impl PartialEq> for &Urn {