refactor: refine URN for archives (#3043)

This commit is contained in:
三咲雅 misaki masa 2025-08-11 01:02:53 +08:00 committed by GitHub
parent 292d37ccde
commit a109aa92e7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 614 additions and 238 deletions

View file

@ -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 `~/`

View file

@ -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()
});

View file

@ -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};

View file

@ -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() {

View file

@ -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!()
}

View file

@ -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)]

View file

@ -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())
};

View file

@ -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;

View file

@ -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;

View file

@ -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() }

View file

@ -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());

81
yazi-fs/src/path/clean.rs Normal file
View file

@ -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<Cow<'a, Url>>) -> 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(())
}
}

170
yazi-fs/src/path/expand.rs Normal file
View file

@ -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>>) -> 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<Path>) -> 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: &regex::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(())
}
}

1
yazi-fs/src/path/mod.rs Normal file
View file

@ -0,0 +1 @@
yazi_macro::mod_flat!(clean expand path);

View file

@ -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>>) -> 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<Path>) -> 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>>) -> 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: &regex::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<Cow<'a, Url>> {
}
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<Cow<'a, Url>> {
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

View file

@ -1,6 +1,6 @@
use std::{env, path::PathBuf};
use crate::expand_path;
use crate::path::expand_path;
pub struct Xdg;

View file

@ -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)]

View file

@ -48,7 +48,7 @@ impl CopySeparator {
pub fn transform<T: AsRef<Path> + ?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()),
};

View file

@ -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;

View file

@ -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)]

View file

@ -155,7 +155,7 @@ fn calc_size(lua: &Lua) -> mlua::Result<Function> {
fn expand_url(lua: &Lua) -> mlua::Result<Function> {
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::<yazi_binding::Url>()?) {
@ -169,7 +169,7 @@ fn expand_url(lua: &Lua) -> mlua::Result<Function> {
fn unique_name(lua: &Lua) -> mlua::Result<Function> {
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),
}

View file

@ -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};

View file

@ -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};

View file

@ -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();

View file

@ -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())
}
}

View file

@ -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<String> for Loc {
impl From<PathBuf> 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<PathBuf> 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<T: ?Sized + AsRef<OsStr>> 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<PathBuf>, 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<Self> {
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<Self> {
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<PathBuf>) -> Self {
let mut loc = Self::from(path.into());
(loc.uri, loc.urn) = (0, 0);
loc
}
pub fn floated(path: impl Into<PathBuf>, 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(())
}
}

View file

@ -36,7 +36,10 @@ impl Scheme {
}
}
pub(super) fn parse(bytes: &[u8], skip: &mut usize) -> Result<(Self, bool, Option<usize>)> {
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<usize> {
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(())
}
}

View file

@ -45,7 +45,8 @@ impl TryFrom<&[u8]> for Url {
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
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<Cow<'_, Url>> for Url {
}
impl Url {
#[inline]
pub fn with(&self, loc: impl Into<Loc>) -> 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<Path>) -> 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<usize>)> {
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(())
}

View file

@ -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<Component<'_>> { 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<OsStr> for Urn {
fn eq(&self, other: &OsStr) -> bool { self.0 == other }
impl<T: AsRef<OsStr>> PartialEq<T> for Urn {
fn eq(&self, other: &T) -> bool { self.0 == other.as_ref() }
}
impl PartialEq<Cow<'_, OsStr>> for &Urn {