feat: new hub VFS kind (#4131)

This commit is contained in:
三咲雅 misaki masa 2026-07-17 03:04:19 +08:00 committed by GitHub
parent b25ae9f82d
commit 52640fcece
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
84 changed files with 1247 additions and 547 deletions

View file

@ -4,7 +4,7 @@ use anyhow::Result;
use yazi_core::cmp::CmpItem; use yazi_core::cmp::CmpItem;
use yazi_macro::{render, succ}; use yazi_macro::{render, succ};
use yazi_parser::cmp::ShowForm; use yazi_parser::cmp::ShowForm;
use yazi_shared::{data::Data, path::{AsPath, PathDyn}, strand::StrandLike}; use yazi_shared::{data::Data, path::{DynPath, PathDyn}, strand::StrandLike};
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -30,7 +30,7 @@ impl Actor for Show {
succ!(); succ!();
}; };
cmp.matches = Self::match_candidates(opt.word.as_path(), cache); cmp.matches = Self::match_candidates(opt.word.dyn_path(), cache);
if cmp.matches.is_empty() { if cmp.matches.is_empty() {
succ!(render!(mem::replace(&mut cmp.visible, false))); succ!(render!(mem::replace(&mut cmp.visible, false)));
} }

View file

@ -6,7 +6,7 @@ use yazi_fs::{engine::{DirReader, FileHolder}, path::clean_url};
use yazi_macro::{act, render, succ}; use yazi_macro::{act, render, succ};
use yazi_parser::cmp::TriggerForm; use yazi_parser::cmp::TriggerForm;
use yazi_proxy::CmpProxy; use yazi_proxy::CmpProxy;
use yazi_shared::{AnyAsciiChar, BytePredictor, data::Data, natsort, path::{AsPath, PathBufDyn, PathLike}, spec::Spec, strand::{AsStrand, StrandLike}, url::{UrlBuf, UrlCow, UrlLike}}; use yazi_shared::{AnyAsciiChar, BytePredictor, data::Data, natsort, path::{DynPath, PathBufDyn, PathLike}, spec::Spec, strand::{AsStrand, StrandLike}, url::{UrlBuf, UrlCow, UrlLike}};
use yazi_vfs::engine; use yazi_vfs::engine;
use crate::{Actor, Ctx}; use crate::{Actor, Ctx};
@ -78,7 +78,7 @@ impl Trigger {
} }
// Child // Child
let child = path.rsplit_pred(AnyAsciiChar::SEP).map_or(path.as_path(), |(_, c)| c).to_owned(); let child = path.rsplit_pred(AnyAsciiChar::SEP).map_or(path.dyn_path(), |(_, c)| c).to_owned();
// Parent // Parent
let url = UrlCow::try_from((spec.clone().zeroed(), path)).ok()?; let url = UrlCow::try_from((spec.clone().zeroed(), path)).ok()?;

View file

@ -4,7 +4,7 @@ use mlua::{AnyUserData, IntoLua, UserData, UserDataFields, UserDataMethods};
use yazi_binding::{Range, style::Style}; use yazi_binding::{Range, style::Style};
use yazi_config::THEME; use yazi_config::THEME;
use yazi_fs::file::FileInventory; use yazi_fs::file::FileInventory;
use yazi_shared::{path::AsPath, url::UrlLike}; use yazi_shared::{path::DynPath, url::UrlLike};
use super::{FILE_CACHE, Lives}; use super::{FILE_CACHE, Lives};
use crate::lives::{CoreRef, PtrCell}; use crate::lives::{CoreRef, PtrCell};
@ -73,7 +73,11 @@ impl UserData for File {
Ok(yazi_config::THEME.icon.matches(me, me.is_hovered())) Ok(yazi_config::THEME.icon.matches(me, me.is_hovered()))
}); });
methods.add_method("size", |_, me, ()| { methods.add_method("size", |_, me, ()| {
Ok(if me.is_dir() { me.folder.entries.sizes.get(&me.urn()).copied() } else { Some(me.len) }) Ok(if me.is_dir() {
me.folder.entries.sizes.get(&me.entry_key()).copied()
} else {
Some(me.len)
})
}); });
methods.add_method("mime", |lua, me, ()| { methods.add_method("mime", |lua, me, ()| {
let core: CoreRef = lua.named_registry_value("cx")?; let core: CoreRef = lua.named_registry_value("cx")?;
@ -86,7 +90,7 @@ impl UserData for File {
let mut comp = me.url.try_strip_prefix(me.url.trail()).unwrap_or(me.url.loc()).components(); let mut comp = me.url.try_strip_prefix(me.url.trail()).unwrap_or(me.url.loc()).components();
comp.next_back(); comp.next_back();
Some(lua.create_string(comp.as_path().encoded_bytes())).transpose() Some(lua.create_string(comp.dyn_path().encoded_bytes())).transpose()
}); });
methods.add_method("style", |lua, me, ()| { methods.add_method("style", |lua, me, ()| {
let core: CoreRef = lua.named_registry_value("cx")?; let core: CoreRef = lua.named_registry_value("cx")?;
@ -122,7 +126,7 @@ impl UserData for File {
return Ok(None); return Ok(None);
}; };
let Some(idx) = finder.matched_idx(&me.folder, me.urn()) else { let Some(idx) = finder.matched_idx(&me.folder, me.entry_key()) else {
return Ok(None); return Ok(None);
}; };

View file

@ -81,8 +81,8 @@ impl Cd {
return MgrProxy::cd(&url, CdSource::Cd); return MgrProxy::cd(&url, CdSource::Cd);
} }
if let Some(p) = url.parent() { if let Some((p, k)) = url.pair2() {
FilesOp::Upserting(p.into(), [(url.urn().into(), file)].into()).emit(); FilesOp::Upserting(p.into(), [(k.into(), file)].into()).emit();
} }
MgrProxy::reveal(url); MgrProxy::reveal(url);
} }

View file

@ -64,10 +64,10 @@ impl Create {
if dir { if dir {
engine::create_dir_all(&new).await?; engine::create_dir_all(&new).await?;
} else if let Ok(real) = engine::casefold(&new).await } else if let Ok(real) = engine::casefold(&new).await
&& let Some((parent, urn)) = real.pair() && let Some((parent, key)) = real.pair2()
{ {
ok_or_not_found!(engine::remove_file(&new).await); ok_or_not_found!(engine::remove_file(&new).await);
FilesOp::Deleting(parent.into(), [urn.into()].into()).emit(); FilesOp::Deleting(parent.into(), [key.into()].into()).emit();
engine::create(&new).await?; engine::create(&new).await?;
} else if let Some(parent) = new.parent() { } else if let Some(parent) = new.parent() {
engine::create_dir_all(parent).await.ok(); engine::create_dir_all(parent).await.ok();
@ -78,10 +78,10 @@ impl Create {
} }
if let Ok(real) = engine::casefold(&new).await if let Ok(real) = engine::casefold(&new).await
&& let Some((parent, urn)) = real.pair() && let Some((parent, key)) = real.pair2()
{ {
let file = File::new(&real).await?; let file = File::new(&real).await?;
FilesOp::Upserting(parent.into(), [(urn.into(), file)].into()).emit(); FilesOp::Upserting(parent.into(), [(key.into(), file)].into()).emit();
MgrProxy::reveal(&real); MgrProxy::reveal(&real);
} }

View file

@ -16,10 +16,10 @@ impl Actor for FilterDo {
fn act(cx: &mut Ctx, Self::Form { opt }: Self::Form) -> Result<Data> { fn act(cx: &mut Ctx, Self::Form { opt }: Self::Form) -> Result<Data> {
let filter = if opt.query.is_empty() { None } else { Some(Filter::new(&opt.query, opt.case)?) }; let filter = if opt.query.is_empty() { None } else { Some(Filter::new(&opt.query, opt.case)?) };
let hovered = cx.hovered().map(|f| f.urn().into()); let hovered = cx.hovered().map(|f| f.entry_key().into());
cx.current_mut().entries.set_filter(filter); cx.current_mut().entries.set_filter(filter);
if cx.hovered().map(|f| f.urn()) != hovered.as_ref().map(Into::into) { if cx.hovered().map(|f| f.entry_key()) != hovered.as_ref().map(Into::into) {
act!(mgr:hover, cx, hovered)?; act!(mgr:hover, cx, hovered)?;
act!(mgr:peek, cx)?; act!(mgr:peek, cx)?;
act!(mgr:watch, cx)?; act!(mgr:watch, cx)?;

View file

@ -18,7 +18,7 @@ impl Actor for Hidden {
let state = form.state.bool(cx.tab().pref.show_hidden); let state = form.state.bool(cx.tab().pref.show_hidden);
cx.tab_mut().pref.show_hidden = state; cx.tab_mut().pref.show_hidden = state;
let hovered = cx.hovered().map(|f| f.urn().to_owned()); let hovered = cx.hovered().map(|f| f.entry_key().to_owned());
let apply = |f: &mut Folder| { let apply = |f: &mut Folder| {
if f.stage == FolderStage::Loading { if f.stage == FolderStage::Loading {
render!(); render!();
@ -43,7 +43,7 @@ impl Actor for Hidden {
{ {
render!(h.repos(None)); render!(h.repos(None));
act!(mgr:peek, cx, true)?; act!(mgr:peek, cx, true)?;
} else if cx.hovered().map(|f| f.urn()) != hovered.as_ref().map(Into::into) { } else if cx.hovered().map(|f| f.entry_key()) != hovered.as_ref().map(Into::into) {
act!(mgr:peek, cx)?; act!(mgr:peek, cx)?;
act!(mgr:watch, cx)?; act!(mgr:watch, cx)?;
} }

View file

@ -18,19 +18,19 @@ impl Actor for Hover {
// Parent should always track CWD // Parent should always track CWD
if let Some(p) = &mut tab.parent { if let Some(p) = &mut tab.parent {
render!(p.repos(tab.current.url.try_strip_prefix(&p.url).ok())); render!(p.repos(Some(tab.current.url.entry_key())));
} }
// Repos CWD // Repos CWD
render!(tab.current.repos(form.urn.as_ref().map(Into::into))); render!(tab.current.repos(form.key.as_ref().map(Into::into)));
// Turn on tracing // Turn on tracing
if let (Some(h), Some(u)) = (tab.hovered(), form.urn) if let (Some(h), Some(key)) = (tab.hovered(), form.key)
&& h.urn() == u && h.entry_key() == key
{ {
// `hover(Some)` occurs after user actions, such as create, rename, reveal, etc. // `hover(Some)` occurs after user actions, such as create, rename, reveal, etc.
// At this point, it's intuitive to track the file location regardless. // At this point, it's intuitive to track the entry regardless.
tab.current.trace = Some(u.clone()); tab.current.trace = Some(key.clone());
cx.tasks.scheduler.behavior.reset(); cx.tasks.scheduler.behavior.reset();
} }

View file

@ -66,8 +66,8 @@ impl Actor for Rename {
impl Rename { impl Rename {
async fn r#do(tab: Id, old: UrlBuf, new: UrlBuf) -> Result<()> { async fn r#do(tab: Id, old: UrlBuf, new: UrlBuf) -> Result<()> {
let Some((old_p, old_n)) = old.pair() else { return Ok(()) }; let Some((old_p, old_k)) = old.pair2() else { return Ok(()) };
let Some(_) = new.pair() else { return Ok(()) }; let Some(_) = new.pair2() else { return Ok(()) };
let _permit = WATCHER.acquire().await.unwrap(); let _permit = WATCHER.acquire().await.unwrap();
let overwritten = engine::casefold(&new).await; let overwritten = engine::casefold(&new).await;
@ -75,21 +75,21 @@ impl Rename {
if let Ok(u) = overwritten if let Ok(u) = overwritten
&& u != new && u != new
&& let Some((parent, urn)) = u.pair() && let Some((parent, key)) = u.pair2()
{ {
ok_or_not_found!(engine::rename(&u, &new).await); ok_or_not_found!(engine::rename(&u, &new).await);
FilesOp::Deleting(parent.to_owned(), [urn.into()].into()).emit(); FilesOp::Deleting(parent.to_owned(), [key.into()].into()).emit();
} }
let new = engine::casefold(&new).await?; let new = engine::casefold(&new).await?;
let Some((new_p, new_n)) = new.pair() else { return Ok(()) }; let Some((new_p, new_k)) = new.pair2() else { return Ok(()) };
let file = File::new(&new).await?; let file = File::new(&new).await?;
if new_p == old_p { if new_p == old_p {
FilesOp::Upserting(old_p.into(), [(old_n.into(), file)].into()).emit(); FilesOp::Upserting(old_p.into(), [(old_k.into(), file)].into()).emit();
} else { } else {
FilesOp::Deleting(old_p.into(), [old_n.into()].into()).emit(); FilesOp::Deleting(old_p.into(), [old_k.into()].into()).emit();
FilesOp::Upserting(new_p.into(), [(new_n.into(), file)].into()).emit(); FilesOp::Upserting(new_p.into(), [(new_k.into(), file)].into()).emit();
} }
MgrProxy::reveal(&new); MgrProxy::reveal(&new);

View file

@ -14,7 +14,7 @@ impl Actor for Reveal {
const NAME: &str = "reveal"; const NAME: &str = "reveal";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> { fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
let Some((parent, child)) = form.target.pair() else { succ!() }; let Some((parent, child)) = form.target.pair2() else { succ!() };
// Cd to the parent directory // Cd to the parent directory
act!(mgr:cd, cx, (parent, form.source))?; act!(mgr:cd, cx, (parent, form.source))?;
@ -25,7 +25,7 @@ impl Actor for Reveal {
// If the child is not hovered, which means it doesn't exist, // If the child is not hovered, which means it doesn't exist,
// create a dummy file // create a dummy file
if !form.no_dummy && tab.hovered().is_none_or(|f| child != f.urn()) { if !form.no_dummy && tab.hovered().is_none_or(|f| f.entry_key() != child) {
let op = FilesOp::Creating(parent.into(), vec![File::from_dummy(&form.target, None)]); let op = FilesOp::Creating(parent.into(), vec![File::from_dummy(&form.target, None)]);
tab.current.update_pub(tab.id, op); tab.current.update_pub(tab.id, op);
} }

View file

@ -24,7 +24,7 @@ impl Actor for Sort {
pref.sort_fallback = form.fallback.unwrap_or(pref.sort_fallback); pref.sort_fallback = form.fallback.unwrap_or(pref.sort_fallback);
let sorter = FilesSorter::from(&*pref); let sorter = FilesSorter::from(&*pref);
let hovered = cx.hovered().map(|f| f.urn().to_owned()); let hovered = cx.hovered().map(|f| f.entry_key().to_owned());
let apply = |f: &mut Folder| { let apply = |f: &mut Folder| {
if f.stage == FolderStage::Loading { if f.stage == FolderStage::Loading {
render!(); render!();
@ -50,7 +50,7 @@ impl Actor for Sort {
{ {
render!(h.repos(None)); render!(h.repos(None));
act!(mgr:peek, cx, true)?; act!(mgr:peek, cx, true)?;
} else if cx.hovered().map(|f| f.urn()) != hovered.as_ref().map(Into::into) { } else if cx.hovered().map(|f| f.entry_key()) != hovered.as_ref().map(Into::into) {
act!(mgr:peek, cx)?; act!(mgr:peek, cx)?;
act!(mgr:watch, cx)?; act!(mgr:watch, cx)?;
} }

View file

@ -57,12 +57,12 @@ impl UpdateFiles {
fn update_parent(cx: &mut Ctx, op: FilesOp) -> Result<Data> { fn update_parent(cx: &mut Ctx, op: FilesOp) -> Result<Data> {
let tab = cx.tab_mut(); let tab = cx.tab_mut();
let urn = tab.current.url.urn(); let key = tab.current.url.entry_key();
let leave = matches!(op, FilesOp::Deleting(_, ref urns) if urns.contains(&urn)); let leave = matches!(op, FilesOp::Deleting(_, ref keys) if keys.contains(&key));
if let Some(f) = tab.parent.as_mut() { if let Some(f) = tab.parent.as_mut() {
render!(f.update_pub(tab.id, op)); render!(f.update_pub(tab.id, op));
render!(f.hover(urn)); render!(f.hover(key));
} }
if leave { if leave {
@ -97,8 +97,8 @@ impl UpdateFiles {
fn update_history(cx: &mut Ctx, op: FilesOp) -> Result<Data> { fn update_history(cx: &mut Ctx, op: FilesOp) -> Result<Data> {
let tab = &mut cx.tab_mut(); let tab = &mut cx.tab_mut();
let leave = tab.parent.as_ref().and_then(|f| f.url.parent().map(|p| (p, f.url.urn()))).is_some_and( let leave = tab.parent.as_ref().and_then(|f| f.url.pair2()).is_some_and(
|(p, n)| matches!(op, FilesOp::Deleting(ref parent, ref urns) if *parent == p && urns.contains(&n)), |(pp, key)| matches!(&op, FilesOp::Deleting(parent, keys) if parent == pp && keys.contains(&key)),
); );
tab.history.get_or_insert_with(op.cwd(), |u| Folder::from(u)).update_pub(tab.id, op); tab.history.get_or_insert_with(op.cwd(), |u| Folder::from(u)).update_pub(tab.id, op);

View file

@ -19,10 +19,10 @@ impl Actor for UpdateSucceed {
if form.track if form.track
&& form.id == cx.tasks.scheduler.behavior.first_id() && form.id == cx.tasks.scheduler.behavior.first_id()
&& let Some((parent, urn)) = form.urls[0].pair() && let Some((parent, key)) = form.urls[0].pair2()
&& parent == *cx.cwd() && parent == *cx.cwd()
{ {
cx.current_mut().trace = Some(urn.into()); cx.current_mut().trace = Some(key.into());
act!(mgr:hover, cx)?; act!(mgr:hover, cx)?;
} }

View file

@ -1,7 +1,7 @@
use serde::Deserialize; use serde::Deserialize;
use yazi_binding::position::{Offset, Origin, Position}; use yazi_binding::position::{Offset, Origin, Position};
use yazi_codegen::{DeserializeOver, DeserializeOver2}; use yazi_codegen::{DeserializeOver, DeserializeOver2};
use yazi_shared::{spec::Encode as EncodeSpec, url::Url}; use yazi_shared::{spec::EncodeSpec, url::Url};
use yazi_widgets::input::InputOpt; use yazi_widgets::input::InputOpt;
#[derive(Deserialize, DeserializeOver, DeserializeOver2)] #[derive(Deserialize, DeserializeOver, DeserializeOver2)]

View file

@ -1,16 +1,28 @@
use std::sync::Arc;
use hashbrown::HashMap; use hashbrown::HashMap;
use serde::{Deserialize, Deserializer, de::{MapAccess, Visitor}}; use serde::{Deserialize, Deserializer, de::{MapAccess, Visitor}};
use yazi_shared::auth::Scheme; use yazi_shared::auth::{Auth, Domain, Scheme};
use yazi_shim::toml::DeserializeOverWith; use yazi_shim::toml::DeserializeOverWith;
use super::{DomainSeed, Domains, Service}; use super::{DomainSeed, Domains};
use crate::vfs::Service;
pub struct Authorities(HashMap<Scheme, Domains>); pub struct Authorities(HashMap<Scheme, Domains>);
impl Authorities { impl Authorities {
pub fn get(&self, scheme: &Scheme, domain: &str) -> Option<&Service> { pub fn service(&self, scheme: &Scheme, domain: &Domain<'_>) -> Option<&Service> {
self.0.get(scheme)?.get(domain) self.0.get(scheme)?.get(domain)
} }
pub fn auth(&self, scheme: &Scheme, domain: &Domain<'_>) -> Option<Arc<Auth>> {
let service = self.service(scheme, domain)?;
if service.auth().domain.is_catchall() {
Some(Auth::new(service.kind(), scheme.clone(), domain.clone()))
} else {
Some(service.auth().clone())
}
}
} }
impl<'de> Deserialize<'de> for Authorities { impl<'de> Deserialize<'de> for Authorities {
@ -41,7 +53,7 @@ impl<'de> Deserialize<'de> for Authorities {
impl DeserializeOverWith for Authorities { impl DeserializeOverWith for Authorities {
fn deserialize_over_with<'de, D: Deserializer<'de>>(mut self, de: D) -> Result<Self, D::Error> { fn deserialize_over_with<'de, D: Deserializer<'de>>(mut self, de: D) -> Result<Self, D::Error> {
for (scheme, domains) in Self::deserialize(de)?.0 { for (scheme, domains) in Self::deserialize(de)?.0 {
self.0.entry(scheme).or_default().extend(domains.0); self.0.entry(scheme).or_default().extend(domains);
} }
Ok(self) Ok(self)
} }

View file

@ -1,34 +1,66 @@
use std::{ops::{Deref, DerefMut}, sync::Arc}; use std::sync::Arc;
use hashbrown::HashMap; use hashbrown::HashMap;
use serde::{Deserialize, Deserializer, de::{DeserializeSeed, Error}}; use serde::{Deserialize, Deserializer, de::{self, DeserializeSeed, Error}};
use yazi_shared::{KebabCasedKey, auth::Scheme}; use yazi_shared::auth::{AuthKind, Domain, Scheme};
use super::{Service, ServiceSftp}; use super::{Service, ServiceSftp};
#[derive(Default)] #[derive(Default)]
pub struct Domains(pub(super) HashMap<KebabCasedKey, Service>); pub struct Domains {
exact: HashMap<Domain<'static>, Service>,
impl Deref for Domains { catchall: Option<Service>,
type Target = HashMap<KebabCasedKey, Service>;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl DerefMut for Domains {
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
} }
impl Domains { impl Domains {
pub fn get(&self, domain: &Domain<'_>) -> Option<&Service> {
self.exact.get(domain.as_ref()).or(self.catchall.as_ref())
}
pub fn extend(&mut self, other: Self) {
self.exact.extend(other.exact);
if other.catchall.is_some() {
self.catchall = other.catchall;
}
}
fn init(&mut self, scheme: &Scheme) { fn init(&mut self, scheme: &Scheme) {
for (domain, service) in &mut self.0 { for (domain, service) in &mut self.exact {
let kind = service.kind(); let kind = service.kind();
let auth = Arc::get_mut(service.auth_mut()).expect("unique auth arc"); let auth = Arc::get_mut(service.auth_mut()).expect("unique auth arc");
auth.kind = kind; auth.kind = kind;
auth.scheme = scheme.clone(); auth.scheme = scheme.clone();
auth.domain = domain.clone().into(); auth.domain = domain.clone();
} }
if let Some(service) = &mut self.catchall {
let kind = service.kind();
let auth = Arc::get_mut(service.auth_mut()).expect("unique auth arc");
auth.kind = kind;
auth.scheme = scheme.clone();
auth.domain = Domain::CATCHALL;
}
}
fn from_map<E>(map: HashMap<Domain<'static>, Service>) -> Result<Self, E>
where
E: de::Error,
{
let mut domains = Self::default();
for (domain, service) in map {
if domain.is_catchall() {
domains.catchall = Some(service);
continue;
}
if service.kind() == AuthKind::Hub {
return Err(E::custom("Hub services require a `*` catch-all domain"));
}
domains.exact.insert(domain, service);
}
Ok(domains)
} }
} }
@ -39,22 +71,24 @@ impl<'de> DeserializeSeed<'de> for DomainSeed<'_> {
type Value = Domains; type Value = Domains;
fn deserialize<D: Deserializer<'de>>(self, deserializer: D) -> Result<Self::Value, D::Error> { fn deserialize<D: Deserializer<'de>>(self, deserializer: D) -> Result<Self::Value, D::Error> {
let mut domains = Domains(match self.0 { let mut domains = match self.0 {
Scheme::Regular | Scheme::Search => { Scheme::Regular | Scheme::Search => {
return Err(D::Error::custom("scheme cannot be configured")); return Err(D::Error::custom("scheme cannot be configured"));
} }
Scheme::Sftp => { Scheme::Sftp => {
let map = HashMap::<KebabCasedKey, ServiceSftp>::deserialize(deserializer)?; let map = HashMap::<Domain<'static>, ServiceSftp>::deserialize(deserializer)?;
map.into_iter().map(|(domain, service)| (domain, Service::Sftp(service))).collect() Domains::from_map(
map.into_iter().map(|(domain, service)| (domain, Service::Sftp(service))).collect(),
)?
} }
Scheme::Custom(_) => { Scheme::Custom(_) => {
let map = HashMap::<KebabCasedKey, Service>::deserialize(deserializer)?; let map = HashMap::<Domain<'static>, Service>::deserialize(deserializer)?;
if map.values().any(|service| matches!(service, Service::Sftp(_))) { if map.values().any(|service| matches!(service, Service::Sftp(_))) {
return Err(D::Error::custom("SFTP services must use the `sftp` scheme")); return Err(D::Error::custom("SFTP services must use the `sftp` scheme"));
} }
map Domains::from_map(map)?
} }
}); };
domains.init(self.0); domains.init(self.0);
Ok(domains) Ok(domains)

View file

@ -10,6 +10,7 @@ use super::{ServiceLua, ServiceSftp};
pub enum Service { pub enum Service {
Sftp(ServiceSftp), Sftp(ServiceSftp),
Mount(ServiceLua), Mount(ServiceLua),
Hub(ServiceLua),
Scope(ServiceLua), Scope(ServiceLua),
} }
@ -19,7 +20,7 @@ impl TryFrom<&'static Service> for &'static ServiceSftp {
fn try_from(value: &'static Service) -> Result<Self, Self::Error> { fn try_from(value: &'static Service) -> Result<Self, Self::Error> {
match value { match value {
Service::Sftp(p) => Ok(p), Service::Sftp(p) => Ok(p),
Service::Mount(_) | Service::Scope(_) => { Service::Mount(_) | Service::Hub(_) | Service::Scope(_) => {
Err("expected an SFTP service, got a custom VFS service") Err("expected an SFTP service, got a custom VFS service")
} }
} }
@ -32,7 +33,7 @@ impl TryFrom<&'static Service> for &'static ServiceLua {
fn try_from(value: &'static Service) -> Result<Self, Self::Error> { fn try_from(value: &'static Service) -> Result<Self, Self::Error> {
match value { match value {
Service::Sftp(_) => Err("expected a custom VFS service, got an SFTP service"), Service::Sftp(_) => Err("expected a custom VFS service, got an SFTP service"),
Service::Mount(lua) | Service::Scope(lua) => Ok(lua), Service::Mount(lua) | Service::Hub(lua) | Service::Scope(lua) => Ok(lua),
} }
} }
} }
@ -42,6 +43,7 @@ impl Service {
match self { match self {
Self::Sftp(_) => AuthKind::Sftp, Self::Sftp(_) => AuthKind::Sftp,
Self::Mount(_) => AuthKind::Mount, Self::Mount(_) => AuthKind::Mount,
Self::Hub(_) => AuthKind::Hub,
Self::Scope(_) => AuthKind::Scope, Self::Scope(_) => AuthKind::Scope,
} }
} }
@ -50,6 +52,7 @@ impl Service {
match self { match self {
Self::Sftp(sftp) => &sftp.auth, Self::Sftp(sftp) => &sftp.auth,
Self::Mount(lua) => &lua.auth, Self::Mount(lua) => &lua.auth,
Self::Hub(lua) => &lua.auth,
Self::Scope(lua) => &lua.auth, Self::Scope(lua) => &lua.auth,
} }
} }
@ -58,6 +61,7 @@ impl Service {
match self { match self {
Self::Sftp(sftp) => &mut sftp.auth, Self::Sftp(sftp) => &mut sftp.auth,
Self::Mount(lua) => &mut lua.auth, Self::Mount(lua) => &mut lua.auth,
Self::Hub(lua) => &mut lua.auth,
Self::Scope(lua) => &mut lua.auth, Self::Scope(lua) => &mut lua.auth,
} }
} }

View file

@ -21,7 +21,7 @@ impl Vfs {
where where
P: TryFrom<&'static Service, Error = &'static str>, P: TryFrom<&'static Service, Error = &'static str>,
{ {
let Some(value) = VFS.authorities.get(&auth.scheme, &auth.domain) else { let Some(value) = VFS.authorities.service(&auth.scheme, &auth.domain) else {
return Err(io::Error::other(format!("No such VFS service: {auth}"))); return Err(io::Error::other(format!("No such VFS service: {auth}")));
}; };
@ -47,8 +47,6 @@ impl DeserializeOverWith for Vfs {
// --- Inject // --- Inject
inventory::submit! { inventory::submit! {
AuthInventory { AuthInventory {
get: |scheme, domain| { get: |scheme, domain| VFS.authorities.auth(scheme, domain),
VFS.authorities.get(scheme, domain).map(|service| service.auth().clone())
},
} }
} }

View file

@ -67,7 +67,7 @@ impl Yanked {
} }
pub fn apply_op(&mut self, op: &FilesOp) { pub fn apply_op(&mut self, op: &FilesOp) {
let (removal, addition) = op.diff_recoverable(|u| self.contains(u)); let (removal, addition) = op.diff_recoverable(self.urls());
if !removal.is_empty() { if !removal.is_empty() {
let old = self.files.len(); let old = self.files.len();
self.files.retain(|f| !removal.iter().any(|u| f.url.covariant(u))); self.files.retain(|f| !removal.iter().any(|u| f.url.covariant(u)));

View file

@ -1,7 +1,7 @@
use anyhow::Result; use anyhow::Result;
use hashbrown::HashMap; use hashbrown::HashMap;
use yazi_fs::{Entries, Filter, FilterCase}; use yazi_fs::{Entries, Filter, FilterCase};
use yazi_shared::{path::{AsPath, PathBufDyn}, url::UrlBuf}; use yazi_shared::{path::{DynPath, PathBufDyn}, url::UrlBuf};
use crate::tab::Folder; use crate::tab::Folder;
@ -62,7 +62,7 @@ impl Finder {
continue; continue;
} }
self.matched.insert(file.urn().into(), i); self.matched.insert(file.entry_key().into(), i);
if self.matched.len() > 99 { if self.matched.len() > 99 {
break; break;
} }
@ -78,9 +78,9 @@ impl Finder {
impl Finder { impl Finder {
pub fn matched_idx<T>(&self, folder: &Folder, urn: T) -> Option<u8> pub fn matched_idx<T>(&self, folder: &Folder, urn: T) -> Option<u8>
where where
T: AsPath, T: DynPath,
{ {
if self.lock == *folder { self.matched.get(&urn.as_path()).copied() } else { None } if self.lock == *folder { self.matched.get(&urn.dyn_path()).copied() } else { None }
} }
} }

View file

@ -4,7 +4,7 @@ use yazi_config::{LAYOUT, YAZI};
use yazi_dds::Pubsub; use yazi_dds::Pubsub;
use yazi_fs::{Entries, FilesOp, FolderStage, cha::Cha, file::File}; use yazi_fs::{Entries, FilesOp, FolderStage, cha::Cha, file::File};
use yazi_macro::err; use yazi_macro::err;
use yazi_shared::{id::Id, path::{AsPath, PathBufDyn, PathDyn}, url::UrlBuf}; use yazi_shared::{id::Id, path::{DynPath, PathBufDyn, PathDyn}, url::UrlBuf};
use yazi_widgets::{Scrollable, Step}; use yazi_widgets::{Scrollable, Step};
use crate::MgrProxy; use crate::MgrProxy;
@ -104,24 +104,26 @@ impl Folder {
b b
} }
pub fn hover(&mut self, urn: PathDyn) -> bool { pub fn hover(&mut self, key: PathDyn) -> bool {
if self.hovered().map(|h| h.urn()) == Some(urn) { if key.is_empty() {
return self.arrow(0);
} else if self.hovered().map(|h| h.entry_key()) == Some(key) {
return self.arrow(0); return self.arrow(0);
} }
let new = self.entries.position(urn).unwrap_or(self.cursor) as isize; let new = self.entries.position(key).unwrap_or(self.cursor) as isize;
let b = self.arrow(new - self.cursor as isize); let b = self.arrow(new - self.cursor as isize);
self.retrace(); self.retrace();
b b
} }
pub fn repos(&mut self, urn: Option<PathDyn>) -> bool { pub fn repos(&mut self, key: Option<PathDyn>) -> bool {
if let Some(u) = urn { if let Some(k) = key {
self.hover(u) self.hover(k)
} else if let Some(u) = self.trace.take() { } else if let Some(k) = self.trace.take() {
let b = self.hover(u.as_path()); let b = self.hover(k.dyn_path());
self.trace = Some(u); self.trace = Some(k);
b b
} else { } else {
self.arrow(0) self.arrow(0)
@ -129,7 +131,7 @@ impl Folder {
} }
pub fn retrace(&mut self) { pub fn retrace(&mut self) {
self.trace = self.hovered().map(|h| h.urn().into()).or(self.trace.take()); self.trace = self.hovered().map(|h| h.entry_key().into()).or(self.trace.take());
} }
pub fn sync_page(&mut self, force: bool) { pub fn sync_page(&mut self, force: bool) {

View file

@ -133,7 +133,7 @@ impl Selected {
} }
pub fn apply_op(&mut self, op: &FilesOp) { pub fn apply_op(&mut self, op: &FilesOp) {
let (removal, addition) = op.diff_recoverable(|u| self.contains(u)); let (removal, addition) = op.diff_recoverable(self.urls());
if !removal.is_empty() { if !removal.is_empty() {
self.remove_many(&removal); self.remove_many(&removal);
} }
@ -190,6 +190,12 @@ mod tests {
assert!(!s.remove(Path::new("/a/b"))); assert!(!s.remove(Path::new("/a/b")));
assert!(s.inner.is_empty()); assert!(s.inner.is_empty());
assert!(s.parents.is_empty()); assert!(s.parents.is_empty());
// Relative path with an empty parent (root)
assert!(s.add(f("a")));
assert!(s.remove(Path::new("a")));
assert!(s.inner.is_empty());
assert!(s.parents.is_empty());
} }
#[test] #[test]

View file

@ -85,7 +85,7 @@ impl Tab {
pub fn hovered_mut(&mut self) -> Option<&mut File> { self.current.hovered_mut() } pub fn hovered_mut(&mut self) -> Option<&mut File> { self.current.hovered_mut() }
pub fn hovered_rect(&self) -> Option<Rect> { pub fn hovered_rect(&self) -> Option<Rect> {
let y = self.current.entries.position(self.hovered()?.urn())? - self.current.offset; let y = self.current.entries.position(self.hovered()?.entry_key())? - self.current.offset;
let mut rect = LAYOUT.get().current; let mut rect = LAYOUT.get().current;
rect.y = rect.y.saturating_sub(1) + y as u16; rect.y = rect.y.saturating_sub(1) + y as u16;

View file

@ -54,7 +54,11 @@ impl Tasks {
targets targets
.iter() .iter()
.filter(|f| { .filter(|f| {
f.is_dir() && !targets.sizes.contains_key(&f.urn()) && !loading.contains(&f.url) let key = f.entry_key();
f.is_dir()
&& !key.is_empty()
&& !targets.sizes.contains_key(&key)
&& !loading.contains(&f.url)
}) })
.map(|f| &f.url) .map(|f| &f.url)
.collect() .collect()

View file

@ -51,6 +51,6 @@ fn try_absolute_impl<'a>(url: UrlCow<'a>) -> Option<UrlCow<'a>> {
Some(match url.as_url() { Some(match url.as_url() {
Url::Regular(_) => UrlBuf::Regular(loc).into(), Url::Regular(_) => UrlBuf::Regular(loc).into(),
Url::Search { auth, .. } => UrlBuf::Search { loc, auth: auth.clone() }.into(), Url::Search { auth, .. } => UrlBuf::Search { loc, auth: auth.clone() }.into(),
Url::Mount { .. } | Url::Scope { .. } | Url::Sftp { .. } => None?, Url::Mount { .. } | Url::Hub { .. } | Url::Scope { .. } | Url::Sftp { .. } => None?,
}) })
} }

View file

@ -1,7 +1,7 @@
use std::{fs::FileTimes, io, path::Path, sync::Arc}; use std::{fs::FileTimes, io, path::Path, sync::Arc};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use yazi_shared::{auth::AuthKind, path::{AsPath, PathBufDyn}, strand::AsStrand, url::{Url, UrlBuf, UrlCow}}; use yazi_shared::{auth::AuthKind, path::{DynPath, PathBufDyn}, strand::AsStrand, url::{Url, UrlBuf, UrlCow}};
use crate::{cha::{Cha, ChaMode}, engine::{Attrs, Capabilities, Engine}}; use crate::{cha::{Cha, ChaMode}, engine::{Attrs, Capabilities, Engine}};
@ -45,19 +45,19 @@ impl<'a> Engine for Local<'a> {
#[inline] #[inline]
async fn copy<P>(&self, to: P, attrs: Attrs) -> io::Result<u64> async fn copy<P>(&self, to: P, attrs: Attrs) -> io::Result<u64>
where where
P: AsPath, P: DynPath,
{ {
let to = to.as_path().to_os_owned()?; let to = to.dyn_path().to_os_owned()?;
let from = self.path.to_owned(); let from = self.path.to_owned();
super::copy_impl(from, to, attrs).await super::copy_impl(from, to, attrs).await
} }
fn copy_progressive<P, A>(&self, to: P, attrs: A) -> io::Result<mpsc::Receiver<io::Result<u64>>> fn copy_progressive<P, A>(&self, to: P, attrs: A) -> io::Result<mpsc::Receiver<io::Result<u64>>>
where where
P: AsPath, P: DynPath,
A: Into<Attrs>, A: Into<Attrs>,
{ {
let to = to.as_path().to_os_owned()?; let to = to.dyn_path().to_os_owned()?;
let from = self.path.to_owned(); let from = self.path.to_owned();
Ok(super::copy_progressive_impl(from, to, attrs.into())) Ok(super::copy_progressive_impl(from, to, attrs.into()))
} }
@ -71,9 +71,9 @@ impl<'a> Engine for Local<'a> {
#[inline] #[inline]
async fn hard_link<P>(&self, to: P) -> io::Result<()> async fn hard_link<P>(&self, to: P) -> io::Result<()>
where where
P: AsPath, P: DynPath,
{ {
let to = to.as_path().as_os()?; let to = to.dyn_path().as_os()?;
tokio::fs::hard_link(self.path, to).await tokio::fs::hard_link(self.path, to).await
} }
@ -87,7 +87,7 @@ impl<'a> Engine for Local<'a> {
async fn new<'b>(url: Url<'b>) -> io::Result<Self::Me<'b>> { async fn new<'b>(url: Url<'b>) -> io::Result<Self::Me<'b>> {
match url { match url {
Url::Regular(loc) | Url::Search { loc, .. } => Ok(Self::Me { url, path: loc.as_inner() }), Url::Regular(loc) | Url::Search { loc, .. } => Ok(Self::Me { url, path: loc.as_inner() }),
Url::Mount { .. } | Url::Scope { .. } | Url::Sftp { .. } => { Url::Mount { .. } | Url::Hub { .. } | Url::Scope { .. } | Url::Sftp { .. } => {
Err(io::Error::new(io::ErrorKind::InvalidInput, format!("Not a local URL: {url:?}"))) Err(io::Error::new(io::ErrorKind::InvalidInput, format!("Not a local URL: {url:?}")))
} }
} }
@ -101,7 +101,7 @@ impl<'a> Engine for Local<'a> {
reader: tokio::fs::read_dir(self.path).await?, reader: tokio::fs::read_dir(self.path).await?,
dir: Arc::new(self.url.to_owned()), dir: Arc::new(self.url.to_owned()),
}, },
AuthKind::Mount | AuthKind::Scope | AuthKind::Sftp => Err(io::Error::new( AuthKind::Mount | AuthKind::Hub | AuthKind::Scope | AuthKind::Sftp => Err(io::Error::new(
io::ErrorKind::InvalidInput, io::ErrorKind::InvalidInput,
format!("Not a local URL: {:?}", self.url), format!("Not a local URL: {:?}", self.url),
))?, ))?,
@ -125,9 +125,9 @@ impl<'a> Engine for Local<'a> {
#[inline] #[inline]
async fn rename<P>(&self, to: P) -> io::Result<()> async fn rename<P>(&self, to: P) -> io::Result<()>
where where
P: AsPath, P: DynPath,
{ {
let to = to.as_path().as_os()?; let to = to.dyn_path().as_os()?;
tokio::fs::rename(self.path, to).await tokio::fs::rename(self.path, to).await
} }

View file

@ -2,7 +2,7 @@ use std::io;
use tokio::{io::{AsyncRead, AsyncSeek, AsyncWrite, AsyncWriteExt}, sync::mpsc}; use tokio::{io::{AsyncRead, AsyncSeek, AsyncWrite, AsyncWriteExt}, sync::mpsc};
use yazi_macro::ok_or_not_found; use yazi_macro::ok_or_not_found;
use yazi_shared::{path::{AsPath, PathBufDyn}, strand::{AsStrand, StrandCow}, url::{AsUrl, Url, UrlBuf}}; use yazi_shared::{path::{DynPath, PathBufDyn}, strand::{AsStrand, StrandCow}, url::{AsUrl, Url, UrlBuf}};
use crate::{cha::{Cha, ChaType}, engine::{Attrs, Capabilities}}; use crate::{cha::{Cha, ChaType}, engine::{Attrs, Capabilities}};
@ -23,11 +23,11 @@ pub trait Engine: Sized {
fn copy<P>(&self, to: P, attrs: Attrs) -> impl Future<Output = io::Result<u64>> fn copy<P>(&self, to: P, attrs: Attrs) -> impl Future<Output = io::Result<u64>>
where where
P: AsPath; P: DynPath;
fn copy_progressive<P, A>(&self, to: P, attrs: A) -> io::Result<mpsc::Receiver<io::Result<u64>>> fn copy_progressive<P, A>(&self, to: P, attrs: A) -> io::Result<mpsc::Receiver<io::Result<u64>>>
where where
P: AsPath, P: DynPath,
A: Into<Attrs>; A: Into<Attrs>;
fn create(&self) -> impl Future<Output = io::Result<Self::File>> { fn create(&self) -> impl Future<Output = io::Result<Self::File>> {
@ -80,7 +80,7 @@ pub trait Engine: Sized {
fn hard_link<P>(&self, to: P) -> impl Future<Output = io::Result<()>> fn hard_link<P>(&self, to: P) -> impl Future<Output = io::Result<()>>
where where
P: AsPath; P: DynPath;
fn metadata(&self) -> impl Future<Output = io::Result<Cha>>; fn metadata(&self) -> impl Future<Output = io::Result<Cha>>;
@ -157,7 +157,7 @@ pub trait Engine: Sized {
fn rename<P>(&self, to: P) -> impl Future<Output = io::Result<()>> fn rename<P>(&self, to: P) -> impl Future<Output = io::Result<()>>
where where
P: AsPath; P: DynPath;
fn set_attrs(&self, attrs: Attrs) -> impl Future<Output = io::Result<()>>; fn set_attrs(&self, attrs: Attrs) -> impl Future<Output = io::Result<()>>;

View file

@ -1,7 +1,7 @@
use std::{mem, ops::{Deref, DerefMut, Not}}; use std::{mem, ops::{Deref, DerefMut, Not}};
use hashbrown::{HashMap, HashSet}; use hashbrown::{HashMap, HashSet};
use yazi_shared::{id::Id, path::{PathBufDyn, PathDyn}}; use yazi_shared::{id::Id, path::{PathBufDyn, PathDyn, PathLike}};
use super::{FilesSorter, Filter}; use super::{FilesSorter, Filter};
use crate::{FILES_TICKET, SortBy, file::File}; use crate::{FILES_TICKET, SortBy, file::File};
@ -69,19 +69,19 @@ impl Entries {
} }
} }
pub fn update_size(&mut self, mut sizes: HashMap<PathBufDyn, u64>) { pub fn update_size(&mut self, sizes: HashMap<PathBufDyn, u64>) {
if sizes.len() <= 50 { self.sizes.reserve(if self.sizes.is_empty() { sizes.len() } else { sizes.len().div_ceil(2) });
sizes.retain(|k, v| self.sizes.get(k) != Some(v));
let mut changed = false;
for (key, size) in sizes {
if !key.is_empty() {
changed |= self.sizes.insert(key, size) != Some(size);
}
} }
if sizes.is_empty() { if changed && self.sorter.by == SortBy::Size {
return;
}
if self.sorter.by == SortBy::Size {
self.revision += 1; self.revision += 1;
} }
self.sizes.extend(sizes);
} }
pub fn update_ioerr(&mut self) { pub fn update_ioerr(&mut self) {
@ -97,9 +97,10 @@ impl Entries {
macro_rules! go { macro_rules! go {
($dist:expr, $src:expr, $inc:literal) => { ($dist:expr, $src:expr, $inc:literal) => {
let mut todo: HashMap<_, _> = $src.into_iter().map(|f| (f.urn().to_owned(), f)).collect(); let mut todo: HashMap<_, _> =
$src.into_iter().map(|f| (f.entry_key().to_owned(), f)).collect();
for f in &$dist { for f in &$dist {
if todo.remove(&f.urn()).is_some() && todo.is_empty() { if todo.remove(&f.entry_key()).is_some() && todo.is_empty() {
break; break;
} }
} }
@ -119,48 +120,14 @@ impl Entries {
} }
} }
#[cfg(unix)] pub fn update_deleting(&mut self, mut keys: HashSet<PathBufDyn>) -> Vec<usize> {
pub fn update_deleting(&mut self, urns: HashSet<PathBufDyn>) -> Vec<usize> { keys.retain(|k| !k.is_empty());
use yazi_shared::path::PathLike; let mut deleted = Vec::with_capacity(keys.len());
if urns.is_empty() {
return vec![];
}
let (mut hidden, mut items) = if let Some(filter) = &self.filter { if !keys.is_empty() {
urns.into_iter().partition(|u| (!self.show_hidden && u.is_hidden()) || !filter.matches(u))
} else if self.show_hidden {
(HashSet::new(), urns)
} else {
urns.into_iter().partition(|u| u.is_hidden())
};
let mut deleted = Vec::with_capacity(items.len());
if !items.is_empty() {
let mut i = 0; let mut i = 0;
self.items.retain(|f| { self.items.retain(|f| {
let b = items.remove(&f.urn()); let b = keys.remove(&f.entry_key());
if b {
deleted.push(i);
}
i += 1;
!b
});
}
if !hidden.is_empty() {
self.hidden.retain(|f| !hidden.remove(&f.urn()));
}
self.revision += deleted.is_empty().not() as u64;
deleted
}
#[cfg(windows)]
pub fn update_deleting(&mut self, mut urns: HashSet<PathBufDyn>) -> Vec<usize> {
let mut deleted = Vec::with_capacity(urns.len());
if !urns.is_empty() {
let mut i = 0;
self.items.retain(|f| {
let b = urns.remove(&f.urn());
if b { if b {
deleted.push(i) deleted.push(i)
} }
@ -168,8 +135,9 @@ impl Entries {
!b !b
}); });
} }
if !urns.is_empty() {
self.hidden.retain(|f| !urns.remove(&f.urn())); if !keys.is_empty() {
self.hidden.retain(|f| !keys.remove(&f.entry_key()));
} }
self.revision += deleted.is_empty().not() as u64; self.revision += deleted.is_empty().not() as u64;
@ -178,8 +146,9 @@ impl Entries {
pub fn update_updating( pub fn update_updating(
&mut self, &mut self,
files: HashMap<PathBufDyn, File>, mut files: HashMap<PathBufDyn, File>,
) -> (HashMap<PathBufDyn, File>, HashMap<PathBufDyn, File>) { ) -> (HashMap<PathBufDyn, File>, HashMap<PathBufDyn, File>) {
files.retain(|k, f| !k.is_empty() && !f.entry_key().is_empty());
if files.is_empty() { if files.is_empty() {
return Default::default(); return Default::default();
} }
@ -188,9 +157,9 @@ impl Entries {
($dist:expr, $src:expr, $inc:literal) => { ($dist:expr, $src:expr, $inc:literal) => {
let mut b = true; let mut b = true;
for i in 0..$dist.len() { for i in 0..$dist.len() {
if let Some(f) = $src.remove(&$dist[i].urn()) { if let Some(f) = $src.remove(&$dist[i].entry_key()) {
b = b && $dist[i].cha.hits(f.cha); b = b && $dist[i].cha.hits(f.cha);
b = b && $dist[i].urn() == f.urn(); b = b && $dist[i].entry_key() == f.entry_key();
$dist[i] = f; $dist[i] = f;
if $src.is_empty() { if $src.is_empty() {
@ -221,13 +190,18 @@ impl Entries {
(hidden, items) (hidden, items)
} }
pub fn update_upserting(&mut self, files: HashMap<PathBufDyn, File>) { pub fn update_upserting(&mut self, mut files: HashMap<PathBufDyn, File>) {
files.retain(|k, f| !k.is_empty() && !f.entry_key().is_empty());
if files.is_empty() { if files.is_empty() {
return; return;
} }
self.update_deleting( self.update_deleting(
files.iter().filter(|&(u, f)| u != f.urn()).map(|(_, f)| f.urn().into()).collect(), files
.iter()
.filter(|&(k, f)| k != f.entry_key())
.map(|(_, f)| f.entry_key().into())
.collect(),
); );
let (hidden, items) = self.update_updating(files); let (hidden, items) = self.update_updating(files);
@ -255,14 +229,13 @@ impl Entries {
} }
fn split_files(&self, files: impl IntoIterator<Item = File>) -> (Vec<File>, Vec<File>) { fn split_files(&self, files: impl IntoIterator<Item = File>) -> (Vec<File>, Vec<File>) {
let files = files.into_iter().filter(|f| !f.entry_key().is_empty());
if let Some(filter) = &self.filter { if let Some(filter) = &self.filter {
files files.partition(|f| (f.is_hidden() && !self.show_hidden) || !filter.matches(f.urn()))
.into_iter()
.partition(|f| (f.is_hidden() && !self.show_hidden) || !filter.matches(f.urn()))
} else if self.show_hidden { } else if self.show_hidden {
(vec![], files.into_iter().collect()) (vec![], files.collect())
} else { } else {
files.into_iter().partition(|f| f.is_hidden()) files.partition(|f| f.is_hidden())
} }
} }
} }
@ -270,7 +243,9 @@ impl Entries {
impl Entries { impl Entries {
// --- Items // --- Items
#[inline] #[inline]
pub fn position(&self, urn: PathDyn) -> Option<usize> { self.iter().position(|f| urn == f.urn()) } pub fn position(&self, key: PathDyn) -> Option<usize> {
if key.is_empty() { None } else { self.iter().position(|f| f.entry_key() == key) }
}
// --- Ticket // --- Ticket
#[inline] #[inline]

View file

@ -63,6 +63,9 @@ impl File {
#[inline] #[inline]
pub fn urn(&self) -> PathDyn<'_> { self.url.urn() } pub fn urn(&self) -> PathDyn<'_> { self.url.urn() }
#[inline]
pub fn entry_key(&self) -> PathDyn<'_> { self.url.entry_key() }
#[inline] #[inline]
pub fn name(&self) -> Option<Strand<'_>> { self.url.name() } pub fn name(&self) -> Option<Strand<'_>> { self.url.name() }

View file

@ -70,6 +70,8 @@ fn test_max_common_root() {
assert_eq!(max_common_root(&[]), 0); assert_eq!(max_common_root(&[]), 0);
assert(&[""], ""); assert(&[""], "");
assert(&["a"], ""); assert(&["a"], "");
assert(&["search://kw:1:1/a", "search://kw:1:1/b"], "search://kw/");
assert(&["test-hub://a1/@root/a", "test-hub://b1/@root/b"], "test-hub://root/@/");
assert(&["/a"], "/"); assert(&["/a"], "/");
assert(&["/a/b"], "/a"); assert(&["/a/b"], "/a");

View file

@ -2,7 +2,7 @@ use std::{iter, path::Path};
use hashbrown::{HashMap, HashSet}; use hashbrown::{HashMap, HashSet};
use yazi_macro::{impl_data_any, relay}; use yazi_macro::{impl_data_any, relay};
use yazi_shared::{id::{Id, Ids}, path::PathBufDyn, url::{UrlBuf, UrlLike, UrlMapExt}}; use yazi_shared::{id::{Id, Ids}, path::{PathBufDyn, PathLike}, url::{UrlBuf, UrlLike, UrlMapExt}};
use crate::{cha::Cha, file::File}; use crate::{cha::Cha, file::File};
@ -42,16 +42,17 @@ impl FilesOp {
pub fn files(&self) -> Box<dyn Iterator<Item = &File> + '_> { pub fn files(&self) -> Box<dyn Iterator<Item = &File> + '_> {
match self { match self {
Self::Full(_, files, _) => Box::new(files.iter()), Self::Full(_, files, _) | Self::Part(_, files, _) | Self::Creating(_, files) => {
Self::Part(_, files, _) => Box::new(files.iter()), Box::new(files.iter().filter(|f| !f.entry_key().is_empty()))
}
Self::Done(..) => Box::new(iter::empty()), Self::Done(..) => Box::new(iter::empty()),
Self::Size(..) => Box::new(iter::empty()), Self::Size(..) => Box::new(iter::empty()),
Self::IOErr(..) => Box::new(iter::empty()), Self::IOErr(..) => Box::new(iter::empty()),
Self::Creating(_, files) => Box::new(files.iter()),
Self::Deleting(..) => Box::new(iter::empty()), Self::Deleting(..) => Box::new(iter::empty()),
Self::Updating(_, map) => Box::new(map.values()), Self::Updating(_, map) | Self::Upserting(_, map) => {
Self::Upserting(_, map) => Box::new(map.values()), Box::new(map.values().filter(|f| !f.entry_key().is_empty()))
}
} }
} }
@ -68,7 +69,7 @@ impl FilesOp {
pub fn create(files: Vec<File>) { pub fn create(files: Vec<File>) {
let mut parents: HashMap<UrlBuf, Vec<_>> = Default::default(); let mut parents: HashMap<UrlBuf, Vec<_>> = Default::default();
for file in files { for file in files {
if let Some(p) = file.url.parent() { if let Some((p, _)) = file.url.pair2() {
parents.get_or_insert_default(p).push(file); parents.get_or_insert_default(p).push(file);
} }
} }
@ -80,18 +81,18 @@ impl FilesOp {
pub fn rename(map: HashMap<UrlBuf, File>) { pub fn rename(map: HashMap<UrlBuf, File>) {
let mut parents: HashMap<UrlBuf, (HashSet<_>, HashMap<_, _>)> = Default::default(); let mut parents: HashMap<UrlBuf, (HashSet<_>, HashMap<_, _>)> = Default::default();
for (o, n) in map { for (o, n) in map {
let Some(o_p) = o.parent() else { continue }; let Some((o_p, o_k)) = o.pair2() else { continue };
let Some(n_p) = n.url.parent() else { continue }; let Some((n_p, n_k)) = n.url.pair2() else { continue };
if o_p == n_p { if o_p == n_p {
parents.get_or_insert_default(o_p).1.insert(o.urn().into(), n); parents.get_or_insert_default(o_p).1.insert(o_k.into(), n);
} else { } else {
parents.get_or_insert_default(o_p).0.insert(o.urn().into()); parents.get_or_insert_default(o_p).0.insert(o_k.into());
parents.get_or_insert_default(n_p).1.insert(n.urn().into(), n); parents.get_or_insert_default(n_p).1.insert(n_k.into(), n);
} }
} }
for (p, (o, n)) in parents { for (p, (o, n)) in parents {
match (o.is_empty(), n.is_empty()) { match (o.is_empty(), n.is_empty()) {
(true, true) => unreachable!(), (true, true) => {}
(true, false) => Self::Upserting(p, n).emit(), (true, false) => Self::Upserting(p, n).emit(),
(false, true) => Self::Deleting(p, o).emit(), (false, true) => Self::Deleting(p, o).emit(),
(false, false) => { (false, false) => {
@ -106,14 +107,20 @@ impl FilesOp {
let mut parents: HashMap<_, (HashMap<_, _>, HashSet<_>)> = Default::default(); let mut parents: HashMap<_, (HashMap<_, _>, HashSet<_>)> = Default::default();
for op in ops { for op in ops {
match op { match op {
Self::Upserting(p, map) => parents.entry(p).or_default().0.extend(map), Self::Upserting(p, map) => parents
Self::Deleting(p, urns) => parents.entry(p).or_default().1.extend(urns), .entry(p)
.or_default()
.0
.extend(map.into_iter().filter(|(k, f)| !k.is_empty() && !f.entry_key().is_empty())),
Self::Deleting(p, keys) => {
parents.entry(p).or_default().1.extend(keys.into_iter().filter(|k| !k.is_empty()))
}
_ => unreachable!(), _ => unreachable!(),
} }
} }
for (p, (u, d)) in parents { for (p, (u, d)) in parents {
match (u.is_empty(), d.is_empty()) { match (u.is_empty(), d.is_empty()) {
(true, true) => unreachable!(), (true, true) => {}
(true, false) => Self::Deleting(p, d).emit(), (true, false) => Self::Deleting(p, d).emit(),
(false, true) => Self::Upserting(p, u).emit(), (false, true) => Self::Upserting(p, u).emit(),
(false, false) => { (false, false) => {
@ -147,17 +154,25 @@ impl FilesOp {
} }
} }
pub fn diff_recoverable(&self, contains: impl Fn(&UrlBuf) -> bool) -> (Vec<UrlBuf>, Vec<File>) { pub fn diff_recoverable<'a, I>(&self, urls: I) -> (Vec<UrlBuf>, Vec<File>)
where
I: IntoIterator<Item = &'a UrlBuf>,
{
let cwd = self.cwd();
let it = urls
.into_iter()
.filter(|u| u.parent().is_some_and(|p| p == *cwd))
.filter(|u| !u.entry_key().is_empty());
match self { match self {
Self::Deleting(cwd, urns) => { Self::Deleting(_, keys) => {
(urns.iter().filter_map(|u| cwd.try_join(u).ok()).collect(), vec![]) (it.filter(|u| keys.contains(&u.entry_key())).cloned().collect(), vec![])
} }
Self::Updating(cwd, urns) | Self::Upserting(cwd, urns) => urns Self::Updating(_, files) | Self::Upserting(_, files) => it
.iter() .filter_map(|u| files.get(&u.entry_key()).map(|f| (u, f)))
.filter(|&(u, f)| u != f.urn()) .filter(|(_, f)| !f.entry_key().is_empty())
.filter_map(|(u, f)| cwd.try_join(u).ok().map(|u| (u, f))) .filter(|&(u, f)| u != f.url)
.filter(|(u, _)| contains(u)) .map(|(u, f)| (u.clone(), f.clone()))
.map(|(u, f)| (u, f.clone()))
.unzip(), .unzip(),
_ => (vec![], vec![]), _ => (vec![], vec![]),
} }

View file

@ -1,27 +1,44 @@
use yazi_shared::{path::{PathBufDyn, PathDyn}, url::{UrlBuf, UrlCow, UrlLike}}; use std::{iter, sync::Arc};
use yazi_shared::{auth::{Auth, AuthKind}, path::{DynPath, PathBufDyn, PathDyn}, url::{UrlBuf, UrlCow, UrlLike}};
pub fn clean_url<'a>(url: impl Into<UrlCow<'a>>) -> UrlBuf { pub fn clean_url<'a>(url: impl Into<UrlCow<'a>>) -> UrlBuf {
let cow: UrlCow = url.into(); let cow: UrlCow = url.into();
let (path, uri, urn) = clean_path_impl(
cow.loc(), let depth = cow.loc().components().count();
cow.base().components().count() - 1, let base = depth - cow.uri().components().count();
cow.trail().components().count() - 1, let trail = depth - cow.urn().components().count();
let (mut spec, path) = cow.into_pair();
let (path, uri, urn, auth) = clean_path_impl(
path.dyn_path(),
base,
trail,
(spec.kind == AuthKind::Hub).then(|| spec.auth.clone()),
); );
let spec = cow.into_spec().with_ports(uri, urn); spec.auth = auth.unwrap_or(spec.auth);
(spec, path).try_into().expect("UrlBuf from cleaned path") (spec.with_ports(uri, urn), path).try_into().expect("UrlBuf from cleaned path")
} }
fn clean_path_impl(path: PathDyn, base: usize, trail: usize) -> (PathBufDyn, usize, usize) { fn clean_path_impl(
path: PathDyn,
base: usize,
trail: usize,
auth: Option<Arc<Auth>>,
) -> (PathBufDyn, usize, usize, Option<Arc<Auth>>) {
use yazi_shared::path::Component::*; use yazi_shared::path::Component::*;
let mut auths: Vec<_> = iter::successors(auth, |auth| auth.parent.clone()).collect();
let root = auths.pop();
let mut out = vec![]; let mut out = vec![];
let mut uri_count = 0; let mut uri_count = 0;
let mut urn_count = 0; let mut urn_count = 0;
macro_rules! push { macro_rules! push {
($i:ident, $c:ident) => {{ ($i:ident, $c:ident, $auth:ident) => {{
out.push(($i, $c)); out.push(($i, $c, $auth));
if $i >= base { if $i >= base {
uri_count += 1; uri_count += 1;
} }
@ -33,7 +50,7 @@ fn clean_path_impl(path: PathDyn, base: usize, trail: usize) -> (PathBufDyn, usi
macro_rules! pop { macro_rules! pop {
() => {{ () => {{
if let Some((i, _)) = out.pop() { if let Some((i, ..)) = out.pop() {
if i >= base { if i >= base {
uri_count -= 1; uri_count -= 1;
} }
@ -45,14 +62,15 @@ fn clean_path_impl(path: PathDyn, base: usize, trail: usize) -> (PathBufDyn, usi
} }
for (i, c) in path.components().enumerate() { for (i, c) in path.components().enumerate() {
let auth = (root.is_some() && c.has_auth()).then(|| auths.pop().unwrap());
match c { match c {
CurDir => {} CurDir => {}
ParentDir => match out.last().map(|(_, c)| c) { ParentDir => match out.last().map(|(_, c, _)| c) {
Some(RootDir) => {} Some(RootDir) => {}
Some(Normal(_)) => pop!(), Some(Normal(_)) => pop!(),
None | Some(CurDir) | Some(ParentDir) | Some(Prefix(_)) => push!(i, c), None | Some(CurDir) | Some(ParentDir) | Some(Prefix(_)) => push!(i, c, auth),
}, },
c => push!(i, c), c => push!(i, c, auth),
} }
} }
@ -60,11 +78,18 @@ fn clean_path_impl(path: PathDyn, base: usize, trail: usize) -> (PathBufDyn, usi
let path = if out.is_empty() { let path = if out.is_empty() {
PathBufDyn::with_str(kind, ".") PathBufDyn::with_str(kind, ".")
} else { } else {
PathBufDyn::from_components(kind, out.into_iter().map(|(_, c)| c)) PathBufDyn::from_components(kind, out.iter().map(|(_, c, _)| *c))
.expect("components with same kind") .expect("components with same kind")
}; };
(path, uri_count, urn_count) let auth = root.map(|mut parent| {
for mut auth in out.into_iter().filter_map(|(_, _, auth)| auth) {
Arc::make_mut(&mut auth).parent = Some(parent);
parent = auth;
}
parent
});
(path, uri_count, urn_count, auth)
} }
#[cfg(test)] #[cfg(test)]
@ -106,4 +131,24 @@ mod tests {
} }
Ok(()) Ok(())
} }
#[test]
fn test_clean_hub() -> anyhow::Result<()> {
yazi_shared::init_tests();
for (input, expected) in [
("test-hub://dotdot/@foo,root/foo/..", "test-hub://root/@/."),
("test-hub://bar/@dotdot,foo,root/foo/../bar", "test-hub://bar/@root/bar"),
("test-hub://dotdot/@root//..", "test-hub://root/@//"),
("test-hub://bar/@dotdot,foo,root//foo/../bar", "test-hub://bar/@root//bar"),
("test-hub://d3/@d2,d1,root/../../..", "test-hub://d3/@d2,d1,root/../../.."),
] {
let input: UrlBuf = input.parse()?;
#[cfg(unix)]
assert_eq!(format!("{:?}", clean_url(input)), expected);
#[cfg(windows)]
assert_eq!(format!("{:?}", clean_url(input)).replace(r"\", "/"), expected);
}
Ok(())
}
} }

View file

@ -40,6 +40,10 @@ fn expand_url_impl(url: UrlCow) -> UrlCow {
loc: LocBuf::<std::path::PathBuf>::with(path.into_os().unwrap(), uri, urn).unwrap(), loc: LocBuf::<std::path::PathBuf>::with(path.into_os().unwrap(), uri, urn).unwrap(),
auth: auth.clone(), auth: auth.clone(),
}, },
Url::Hub { auth, .. } => UrlBuf::Hub {
auth: auth.clone().with_parent_depth(path.components().auth_depth()),
loc: LocBuf::<std::path::PathBuf>::with(path.into_os().unwrap(), uri, urn).unwrap(),
},
Url::Scope { auth, .. } => UrlBuf::Scope { Url::Scope { auth, .. } => UrlBuf::Scope {
loc: LocBuf::<typed_path::UnixPathBuf>::with(path.into_unix().unwrap(), uri, urn).unwrap(), loc: LocBuf::<typed_path::UnixPathBuf>::with(path.into_unix().unwrap(), uri, urn).unwrap(),
auth: auth.clone(), auth: auth.clone(),
@ -112,6 +116,7 @@ mod tests {
("test-mount://7z:2:1//tmp/test.zip/$BAR_BAZ", "test-mount://7z:3:2//tmp/test.zip/bar/baz"), ("test-mount://7z:2:1//tmp/test.zip/$BAR_BAZ", "test-mount://7z:3:2//tmp/test.zip/bar/baz"),
("test-mount://7z:2:2//tmp/$BAR_BAZ/test.zip", "test-mount://7z:3:3//tmp/bar/baz/test.zip"), ("test-mount://7z:2:2//tmp/$BAR_BAZ/test.zip", "test-mount://7z:3:3//tmp/bar/baz/test.zip"),
("test-mount://7z:2:2//$BAR_BAZ/tmp/test.zip", "test-mount://7z:2:2//bar/baz/tmp/test.zip"), ("test-mount://7z:2:2//$BAR_BAZ/tmp/test.zip", "test-mount://7z:2:2//bar/baz/tmp/test.zip"),
("test-hub://a1/@root/$BAR_BAZ", "test-hub://a1:2:2/@,root/bar/baz"),
// -1 component // -1 component
("test-mount://7z//tmp/test.zip/${BAR/BAZ}", "test-mount://7z//tmp/test.zip/bar_baz"), ("test-mount://7z//tmp/test.zip/${BAR/BAZ}", "test-mount://7z//tmp/test.zip/bar_baz"),
("test-mount://7z:1//tmp/test.zip/${BAR/BAZ}", "test-mount://7z:1//tmp/test.zip/${BAR/BAZ}"), ("test-mount://7z:1//tmp/test.zip/${BAR/BAZ}", "test-mount://7z:1//tmp/test.zip/${BAR/BAZ}"),
@ -134,6 +139,7 @@ mod tests {
), ),
("test-mount://7z:3:3//tmp/test.zip/${BAR/BAZ}", "test-mount://7z:2:2//tmp/test.zip/bar_baz"), ("test-mount://7z:3:3//tmp/test.zip/${BAR/BAZ}", "test-mount://7z:2:2//tmp/test.zip/bar_baz"),
("test-mount://7z:3:3//tmp/${BAR/BAZ}/test.zip", "test-mount://7z:2:2//tmp/bar_baz/test.zip"), ("test-mount://7z:3:3//tmp/${BAR/BAZ}/test.zip", "test-mount://7z:2:2//tmp/bar_baz/test.zip"),
("test-hub://a1:2:2/@b1,root/${BAR/BAZ}", "test-hub://a1/@root/bar_baz"),
// Zeros all components // Zeros all components
("test-mount://7z//${EM/PT/Y}", "test-mount://7z//"), ("test-mount://7z//${EM/PT/Y}", "test-mount://7z//"),
("test-mount://7z:1//${EM/PT/Y}", "test-mount://7z:1//${EM/PT/Y}"), ("test-mount://7z:1//${EM/PT/Y}", "test-mount://7z:1//${EM/PT/Y}"),

View file

@ -4,8 +4,16 @@ use anyhow::Result;
use percent_encoding::{AsciiSet, CONTROLS, percent_decode, percent_encode}; use percent_encoding::{AsciiSet, CONTROLS, percent_decode, percent_encode};
use yazi_shared::path::{PathCow, PathDyn, PathKind}; use yazi_shared::path::{PathCow, PathDyn, PathKind};
const SET: &AsciiSet = const SET: &AsciiSet = &CONTROLS
&CONTROLS.add(b'"').add(b'*').add(b':').add(b'<').add(b'>').add(b'?').add(b'\\').add(b'|'); .add(b'"')
.add(b'*')
.add(b':')
.add(b'<')
.add(b'>')
.add(b'?')
.add(b'\\')
.add(b'|')
.add(b'%');
pub trait PercentEncoding<'a> { pub trait PercentEncoding<'a> {
fn percent_encode(self) -> Cow<'a, Path>; fn percent_encode(self) -> Cow<'a, Path>;

View file

@ -67,8 +67,8 @@ impl FilesSorter {
}), }),
SortBy::Size => items.sort_unstable_by(|a, b| { SortBy::Size => items.sort_unstable_by(|a, b| {
promote!(a, b); promote!(a, b);
let aa = if a.is_dir() { sizes.get(&a.urn()).copied() } else { None }; let aa = if a.is_dir() { sizes.get(&a.entry_key()).copied() } else { None };
let bb = if b.is_dir() { sizes.get(&b.urn()).copied() } else { None }; let bb = if b.is_dir() { sizes.get(&b.entry_key()).copied() } else { None };
self.fallback(a, b, self.cmp(aa.unwrap_or(a.len), bb.unwrap_or(b.len))) self.fallback(a, b, self.cmp(aa.unwrap_or(a.len), bb.unwrap_or(b.len)))
}), }),
SortBy::Random => { SortBy::Random => {

View file

@ -1,6 +1,6 @@
use std::path::PathBuf; use std::path::PathBuf;
use yazi_shared::{auth::{Auth, AuthKind, Encode}, path::PathBufDyn, spec::SpecInventory}; use yazi_shared::{auth::{Auth, AuthKind, EncodeAuth}, path::PathBufDyn, spec::SpecInventory};
use yazi_shim::{mlua::UserDataFieldsExt, strum::IntoStr}; use yazi_shim::{mlua::UserDataFieldsExt, strum::IntoStr};
use crate::Xdg; use crate::Xdg;
@ -13,12 +13,14 @@ impl FsSpec for Auth {
fn cache(&self) -> Option<PathBuf> { fn cache(&self) -> Option<PathBuf> {
match self.kind { match self.kind {
AuthKind::Regular | AuthKind::Search => None, AuthKind::Regular | AuthKind::Search => None,
AuthKind::Mount | AuthKind::Scope | AuthKind::Sftp => Some(Xdg::temp_dir().join(format!( AuthKind::Mount | AuthKind::Hub | AuthKind::Scope | AuthKind::Sftp => {
"{}_{}_{}", Some(Xdg::temp_dir().join(format!(
self.kind.into_str(), "{}_{}_{}",
self.scheme, self.kind.into_str(),
Encode::domain(&self.domain) self.scheme,
))), EncodeAuth::domain(&self.domain)
)))
}
} }
} }
} }

View file

@ -1,7 +1,7 @@
use std::{borrow::Cow, ffi::OsStr, path::{Path, PathBuf}}; use std::{borrow::Cow, ffi::OsStr, path::{Path, PathBuf}};
use mlua::UserDataFields; use mlua::UserDataFields;
use yazi_shared::{path::{AsPath, PathDyn}, url::{AsUrl, Url, UrlBuf, UrlBufInventory, UrlCow, UrlLike}}; use yazi_shared::{path::{DynPath, PathDyn}, url::{AsUrl, Url, UrlBuf, UrlBufInventory, UrlCow, UrlLike}};
use yazi_shim::mlua::UserDataFieldsExt; use yazi_shim::mlua::UserDataFieldsExt;
use crate::{FsHash128, FsSpec, path::PercentEncoding}; use crate::{FsHash128, FsSpec, path::PercentEncoding};
@ -29,7 +29,7 @@ impl<'a> FsUrl<'a> for Url<'a> {
fn with_loc(loc: PathDyn, mut root: PathBuf) -> PathBuf { fn with_loc(loc: PathDyn, mut root: PathBuf) -> PathBuf {
let mut it = loc.components(); let mut it = loc.components();
if it.next() == Some(yazi_shared::path::Component::RootDir) { if it.next() == Some(yazi_shared::path::Component::RootDir) {
root.push(it.as_path().percent_encode()); root.push(it.dyn_path().percent_encode());
} else { } else {
root.push(".%2F"); root.push(".%2F");
root.push(loc.percent_encode()); root.push(loc.percent_encode());
@ -51,7 +51,7 @@ impl<'a> FsUrl<'a> for Url<'a> {
fn unified_path(self) -> Cow<'a, Path> { fn unified_path(self) -> Cow<'a, Path> {
match self { match self {
Self::Regular(loc) | Self::Search { loc, .. } => loc.as_inner().into(), Self::Regular(loc) | Self::Search { loc, .. } => loc.as_inner().into(),
Self::Mount { .. } | Self::Scope { .. } | Self::Sftp { .. } => { Self::Mount { .. } | Self::Hub { .. } | Self::Scope { .. } | Self::Sftp { .. } => {
self.cache().expect("non-local URL should have a cache path").into() self.cache().expect("non-local URL should have a cache path").into()
} }
} }
@ -66,7 +66,7 @@ impl FsUrl<'_> for UrlBuf {
fn unified_path(self) -> Cow<'static, Path> { fn unified_path(self) -> Cow<'static, Path> {
match self { match self {
Self::Regular(loc) | Self::Search { loc, .. } => loc.into_inner().into(), Self::Regular(loc) | Self::Search { loc, .. } => loc.into_inner().into(),
Self::Mount { .. } | Self::Scope { .. } | Self::Sftp { .. } => { Self::Mount { .. } | Self::Hub { .. } | Self::Scope { .. } | Self::Sftp { .. } => {
self.cache().expect("non-local URL should have a cache path").into() self.cache().expect("non-local URL should have a cache path").into()
} }
} }
@ -81,7 +81,7 @@ impl<'a> FsUrl<'a> for UrlCow<'a> {
fn unified_path(self) -> Cow<'a, Path> { fn unified_path(self) -> Cow<'a, Path> {
match self { match self {
Self::Regular(loc) | Self::Search { loc, .. } => loc.into_inner(), Self::Regular(loc) | Self::Search { loc, .. } => loc.into_inner(),
Self::Mount { .. } | Self::Scope { .. } | Self::Sftp { .. } => { Self::Mount { .. } | Self::Hub { .. } | Self::Scope { .. } | Self::Sftp { .. } => {
self.cache().expect("non-local URL should have a cache path").into() self.cache().expect("non-local URL should have a cache path").into()
} }
} }

View file

@ -3,11 +3,11 @@ use yazi_shared::path::PathBufDyn;
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct HoverForm { pub struct HoverForm {
pub urn: Option<PathBufDyn>, pub key: Option<PathBufDyn>,
} }
impl From<Option<PathBufDyn>> for HoverForm { impl From<Option<PathBufDyn>> for HoverForm {
fn from(urn: Option<PathBufDyn>) -> Self { Self { urn } } fn from(key: Option<PathBufDyn>) -> Self { Self { key } }
} }
impl FromLua for HoverForm { impl FromLua for HoverForm {

View file

@ -206,7 +206,7 @@ impl Drop for FileInCut {
impl FileInCut { impl FileInCut {
pub fn new(from: UrlBuf, to: UrlBuf, force: bool) -> Self { pub fn new(from: UrlBuf, to: UrlBuf, force: bool) -> Self {
Self { Self {
follow: !from.auth().covariant(to.auth()), follow: !from.auth().same_service(to.auth()),
id: Id::ZERO, id: Id::ZERO,
from, from,
to, to,

View file

@ -78,7 +78,7 @@ impl Scheduler {
} }
pub fn file_copy(&self, from: UrlBuf, to: UrlBuf, force: bool, follow: bool) { pub fn file_copy(&self, from: UrlBuf, to: UrlBuf, force: bool, follow: bool) {
let follow = follow || !from.auth().covariant(to.auth()); let follow = follow || !from.auth().same_service(to.auth());
let mut r#in = FileInCopy { id: Id::ZERO, from, to, force, cha: None, follow, retry: 0 }; let mut r#in = FileInCopy { id: Id::ZERO, from, to, force, cha: None, follow, retry: 0 };
self.add(&mut r#in, |_| ()); self.add(&mut r#in, |_| ());
@ -109,7 +109,7 @@ impl Scheduler {
let mut r#in = FileInHardlink { id: Id::ZERO, from, to, force, cha: None, follow }; let mut r#in = FileInHardlink { id: Id::ZERO, from, to, force, cha: None, follow };
self.add(&mut r#in, |_| ()); self.add(&mut r#in, |_| ());
if !r#in.from.auth().covariant(r#in.to.auth()) { if !r#in.from.auth().same_service(r#in.to.auth()) {
return self return self
.ops .ops
.out(r#in.id, FileOutHardlink::Fail("Cannot hardlink cross filesystem".to_owned())); .out(r#in.id, FileOutHardlink::Fail("Cannot hardlink cross filesystem".to_owned()));

View file

@ -35,9 +35,10 @@ impl Size {
} }
let parent = buf[0].0.parent().unwrap(); let parent = buf[0].0.parent().unwrap();
// FIXME: use PathBufDyn instead of UrlBuf in SizeIn
FilesOp::Size( FilesOp::Size(
parent.into(), parent.into(),
HashMap::from_iter(buf.into_iter().map(|(u, s)| (u.urn().into(), s))), HashMap::from_iter(buf.into_iter().map(|(u, s)| (u.entry_key().into(), s))),
) )
.emit(); .emit();
}); });

View file

@ -1,19 +1,37 @@
use std::{fmt, sync::Arc}; use std::{fmt, hash::{Hash, Hasher}, sync::Arc};
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
use percent_encoding::percent_decode_str; use percent_encoding::percent_decode_str;
use serde::Deserialize; use serde::Deserialize;
use yazi_shim::{SStr, cell::RoCell}; use yazi_shim::cell::RoCell;
use crate::auth::{AuthInventory, AuthKind, Encode, Scheme}; use crate::{auth::{AuthInventory, AuthKind, Domain, EncodeAuth, Scheme}, path::{Component, Components}};
pub(super) static DEFAULT_ARC: RoCell<Arc<Auth>> = RoCell::new(); pub(super) static DEFAULT_ARC: RoCell<Arc<Auth>> = RoCell::new();
#[derive(Debug, Deserialize, Eq, Hash, PartialEq)] #[derive(Clone, Debug, Deserialize)]
pub struct Auth { pub struct Auth {
pub kind: AuthKind, pub kind: AuthKind,
pub scheme: Scheme, pub scheme: Scheme,
pub domain: SStr, pub domain: Domain<'static>,
#[serde(default)]
pub parent: Option<Arc<Auth>>,
}
impl PartialEq for Auth {
fn eq(&self, other: &Self) -> bool {
self.kind == other.kind && self.scheme == other.scheme && self.domain == other.domain
}
}
impl Eq for Auth {}
impl Hash for Auth {
fn hash<H: Hasher>(&self, state: &mut H) {
self.kind.hash(state);
self.scheme.hash(state);
self.domain.hash(state);
}
} }
impl Default for Auth { impl Default for Auth {
@ -21,20 +39,28 @@ impl Default for Auth {
} }
impl fmt::Display for Auth { impl fmt::Display for Auth {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { Encode(self, false).fmt(f) } fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { EncodeAuth(self, false).fmt(f) }
} }
impl Auth { impl Auth {
pub const DEFAULT: Self = pub const DEFAULT: Self = Self {
Self { kind: AuthKind::Regular, scheme: Scheme::Regular, domain: SStr::Borrowed("") }; kind: AuthKind::Regular,
scheme: Scheme::Regular,
domain: Domain::EMPTY,
parent: None,
};
pub fn default_arc() -> Arc<Self> { DEFAULT_ARC.clone() } pub fn default_arc() -> Arc<Self> { DEFAULT_ARC.clone() }
pub fn new(kind: AuthKind, scheme: Scheme, domain: impl Into<SStr>) -> Arc<Self> { pub fn new<'a>(kind: AuthKind, scheme: Scheme, domain: impl Into<Domain<'a>>) -> Arc<Self> {
Arc::new(Self { kind, scheme, domain: domain.into() }) Arc::new(Self { kind, scheme, domain: domain.into().into_owned(), parent: None })
} }
pub fn get(scheme: &Scheme, domain: &str) -> Option<Arc<Self>> { pub fn search<'a>(query: impl Into<Domain<'a>>) -> Arc<Self> {
Self::new(AuthKind::Search, Scheme::Search, query)
}
pub fn get(scheme: &Scheme, domain: &Domain<'_>) -> Option<Arc<Self>> {
match scheme { match scheme {
Scheme::Regular => Some(Self::default_arc()), Scheme::Regular => Some(Self::default_arc()),
Scheme::Search => Some(Self::search(domain)), Scheme::Search => Some(Self::search(domain)),
@ -42,22 +68,88 @@ impl Auth {
} }
} }
pub fn search(query: impl Into<String>) -> Arc<Self> { pub fn child(self: Arc<Self>) -> Arc<Self> {
Self::new(AuthKind::Search, Scheme::Search, query.into()) Arc::new(Self {
kind: self.kind,
scheme: self.scheme.clone(),
domain: Domain::default(),
parent: Some(self),
})
}
pub fn root(mut self: &Arc<Self>) -> &Arc<Self> {
while let Some(parent) = &self.parent {
self = parent;
}
self
}
pub fn descend<'a>(mut self: Arc<Self>, components: impl Into<Components<'a>>) -> Arc<Self> {
for component in components.into() {
match component {
Component::RootDir => self = Self::new(self.kind, self.scheme.clone(), Domain::EMPTY),
c if c.has_auth() => self = self.child(),
_ => {}
}
}
self
}
pub fn parent_at(mut self: &Arc<Self>, depth: usize) -> &Arc<Self> {
for _ in 0..depth {
self = self.parent.as_ref().expect("Auth parent depth out of bounds");
}
self
}
pub fn with_parent_depth(mut self: Arc<Self>, depth: usize) -> Arc<Self> {
let current = self.parent_depth();
if current == depth {
return self;
}
let mut parent = if current < depth {
self.parent.clone()
} else {
self.parent_at(current - depth).parent.clone()
};
for _ in current..depth {
parent = Some(Arc::new(Self {
kind: self.kind,
scheme: self.scheme.clone(),
domain: Domain::default(),
parent,
}));
}
Arc::make_mut(&mut self).parent = parent;
self
}
pub fn parent_depth(&self) -> usize {
let mut depth = 0;
let mut parent = self.parent.as_deref();
while let Some(auth) = parent {
depth += 1;
parent = auth.parent.as_deref();
}
depth
} }
pub fn covariant(&self, other: &Self) -> bool { pub fn covariant(&self, other: &Self) -> bool {
!self.kind.is_virtual() && !other.kind.is_virtual() || self == other !self.kind.is_virtual() && !other.kind.is_virtual() || self == other
} }
pub fn same_service(&self, other: &Self) -> bool {
self.covariant(other)
|| self.kind == AuthKind::Hub && other.kind == AuthKind::Hub && self.scheme == other.scheme
}
pub fn parse_cache(cache: &str) -> Result<Arc<Self>> { pub fn parse_cache(cache: &str) -> Result<Arc<Self>> {
let (kind, rest) = cache.split_once('_').ok_or_else(|| anyhow!("invalid cache: {cache}"))?; let (kind, rest) = cache.split_once('_').ok_or_else(|| anyhow!("invalid cache: {cache}"))?;
let (scheme, domain) = rest.split_once('_').ok_or_else(|| anyhow!("invalid cache: {cache}"))?; let (scheme, domain) = rest.split_once('_').ok_or_else(|| anyhow!("invalid cache: {cache}"))?;
Ok(Arc::new(Self { Ok(Self::new(kind.parse()?, scheme.parse()?, percent_decode_str(domain).decode_utf8()?))
kind: kind.parse()?,
scheme: scheme.parse()?,
domain: percent_decode_str(domain).decode_utf8()?.into_owned().into(),
}))
} }
} }

View file

@ -0,0 +1,69 @@
use std::{borrow::{Borrow, Cow}, fmt::{self, Display, Formatter}, ops::Deref};
use serde::{Deserialize, Deserializer, Serialize, de::{self, IntoDeserializer, value::CowStrDeserializer}};
#[derive(Default, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(transparent)]
pub struct Domain<'a>(Cow<'a, str>);
impl Domain<'static> {
pub const CATCHALL: Self = Self(Cow::Borrowed("*"));
pub const EMPTY: Self = Self(Cow::Borrowed(""));
}
impl Domain<'_> {
#[inline]
pub fn into_owned(self) -> Domain<'static> { Domain(Cow::Owned(self.0.into_owned())) }
#[inline]
pub fn is_catchall(&self) -> bool { *self == Domain::CATCHALL }
}
impl Deref for Domain<'_> {
type Target = str;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl Borrow<str> for Domain<'_> {
fn borrow(&self) -> &str { &self.0 }
}
impl AsRef<str> for Domain<'_> {
fn as_ref(&self) -> &str { &self.0 }
}
impl<'a> From<String> for Domain<'a> {
fn from(value: String) -> Self { Self(Cow::Owned(value)) }
}
impl<'a> From<&'a str> for Domain<'a> {
fn from(value: &'a str) -> Self { Self(Cow::Borrowed(value)) }
}
impl<'a> From<Cow<'a, str>> for Domain<'a> {
fn from(value: Cow<'a, str>) -> Self { Self(value) }
}
impl<'a> From<&'a Domain<'_>> for Domain<'a> {
fn from(value: &'a Domain<'_>) -> Self { Self(Cow::Borrowed(&value.0)) }
}
impl Display for Domain<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { Display::fmt(&self.0, f) }
}
impl<'de> Deserialize<'de> for Domain<'_> {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Ok(Self(Cow::Owned(String::deserialize(deserializer)?)))
}
}
impl<'de, 'a, E> IntoDeserializer<'de, E> for Domain<'a>
where
E: de::Error,
{
type Deserializer = CowStrDeserializer<'a, E>;
fn into_deserializer(self) -> Self::Deserializer { self.0.into_deserializer() }
}

View file

@ -2,18 +2,18 @@ use std::fmt;
use percent_encoding::{AsciiSet, CONTROLS, PercentEncode, percent_encode}; use percent_encoding::{AsciiSet, CONTROLS, PercentEncode, percent_encode};
use super::Auth; use super::{Auth, AuthKind, Domain};
pub struct Encode<'a>(pub &'a Auth, pub bool); pub struct EncodeAuth<'a>(pub &'a Auth, pub bool);
impl Encode<'_> { impl EncodeAuth<'_> {
pub fn domain(s: &str) -> PercentEncode<'_> { pub fn domain<'a>(s: &'a Domain<'_>) -> PercentEncode<'a> {
const SET: &AsciiSet = &CONTROLS.add(b'/').add(b':'); const SET: &AsciiSet = &CONTROLS.add(b'/').add(b':').add(b'%');
percent_encode(s.as_bytes(), SET) percent_encode(s.as_bytes(), SET)
} }
} }
impl fmt::Display for Encode<'_> { impl fmt::Display for EncodeAuth<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!( write!(
f, f,
@ -24,3 +24,32 @@ impl fmt::Display for Encode<'_> {
) )
} }
} }
// --- EncodePrefix
pub struct EncodePrefix<'a>(pub &'a Auth);
impl EncodePrefix<'_> {
pub fn parent<'a>(s: &'a Domain<'_>) -> PercentEncode<'a> {
const SET: &AsciiSet = &CONTROLS.add(b'/').add(b',').add(b'@').add(b'%');
percent_encode(s.as_bytes(), SET)
}
}
impl fmt::Display for EncodePrefix<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.0.kind != AuthKind::Hub {
return f.write_str("/");
}
f.write_str("/@")?;
let (mut first, mut parent) = (true, self.0.parent.as_deref());
while let Some(auth) = parent {
if !first {
f.write_str(",")?;
}
Self::parent(&auth.domain).fmt(f)?;
(first, parent) = (false, auth.parent.as_deref());
}
f.write_str("/")
}
}

View file

@ -1,9 +1,9 @@
use std::sync::Arc; use std::sync::Arc;
use crate::auth::{Auth, Scheme}; use crate::auth::{Auth, Domain, Scheme};
pub struct AuthInventory { pub struct AuthInventory {
pub get: fn(&Scheme, &str) -> Option<Arc<Auth>>, pub get: fn(&Scheme, &Domain<'_>) -> Option<Arc<Auth>>,
} }
inventory::collect!(AuthInventory); inventory::collect!(AuthInventory);

View file

@ -11,6 +11,7 @@ pub enum AuthKind {
Regular, Regular,
Search, Search,
Mount, Mount,
Hub,
Scope, Scope,
Sftp, Sftp,
} }
@ -20,7 +21,7 @@ impl AuthKind {
pub fn is_local(self) -> bool { pub fn is_local(self) -> bool {
match self { match self {
Self::Regular | Self::Search => true, Self::Regular | Self::Search => true,
Self::Mount | Self::Scope | Self::Sftp => false, Self::Mount | Self::Hub | Self::Scope | Self::Sftp => false,
} }
} }
@ -28,7 +29,7 @@ impl AuthKind {
pub fn is_remote(self) -> bool { pub fn is_remote(self) -> bool {
match self { match self {
Self::Regular | Self::Search | Self::Mount => false, Self::Regular | Self::Search | Self::Mount => false,
Self::Scope | Self::Sftp => true, Self::Hub | Self::Scope | Self::Sftp => true,
} }
} }
@ -36,7 +37,7 @@ impl AuthKind {
pub fn is_virtual(self) -> bool { pub fn is_virtual(self) -> bool {
match self { match self {
Self::Regular | Self::Search => false, Self::Regular | Self::Search => false,
Self::Mount | Self::Scope | Self::Sftp => true, Self::Mount | Self::Hub | Self::Scope | Self::Sftp => true,
} }
} }
} }

View file

@ -1,3 +1,3 @@
yazi_macro::mod_flat!(auth encode inventory kind scheme); yazi_macro::mod_flat!(auth domain encode inventory kind scheme);
pub(super) fn init() { DEFAULT_ARC.with(Default::default); } pub(super) fn init() { DEFAULT_ARC.with(Default::default); }

View file

@ -1,6 +1,6 @@
use std::{ffi::{OsStr, OsString}, fmt::Debug, hash::Hash}; use std::{ffi::{OsStr, OsString}, fmt::Debug, hash::Hash};
use crate::{path::{AsPath, AsPathView}, strand::AsStrandView}; use crate::{path::{DynPath, PathView}, strand::AsStrandView};
// --- LocAble // --- LocAble
pub trait LocAble<'p> pub trait LocAble<'p>
@ -27,14 +27,10 @@ impl<'p> LocAble<'p> for &'p typed_path::UnixPath {
// --- LocBufAble // --- LocBufAble
pub trait LocBufAble pub trait LocBufAble
where where
Self: 'static + AsPath + Default, Self: 'static + DynPath + Default,
{ {
type Strand<'a>: StrandAble<'a>; type Strand<'a>: StrandAble<'a>;
type Borrowed<'a>: LocAble<'a> type Borrowed<'a>: LocAble<'a> + LocAbleImpl<'a> + PathView<'a, Self::Borrowed<'a>> + Debug + Hash;
+ LocAbleImpl<'a>
+ AsPathView<'a, Self::Borrowed<'a>>
+ Debug
+ Hash;
} }
impl LocBufAble for std::path::PathBuf { impl LocBufAble for std::path::PathBuf {

View file

@ -2,7 +2,7 @@ use std::{cmp, ffi::OsStr, fmt::{self, Debug, Formatter}, hash::{Hash, Hasher},
use anyhow::Result; use anyhow::Result;
use crate::{auth::AuthKind, loc::{Loc, LocAble, LocAbleImpl, LocBufAble, LocBufAbleImpl}, path::{AsPath, AsPathView, PathDyn, SetNameError}, strand::AsStrandView}; use crate::{auth::AuthKind, loc::{Loc, LocAble, LocAbleImpl, LocBufAble, LocBufAbleImpl}, path::{DynPath, PathDyn, PathView, SetNameError}, strand::AsStrandView};
#[derive(Clone, Default, Eq, PartialEq)] #[derive(Clone, Default, Eq, PartialEq)]
pub struct LocBuf<P = std::path::PathBuf> { pub struct LocBuf<P = std::path::PathBuf> {
@ -25,18 +25,18 @@ impl AsRef<std::path::Path> for LocBuf<std::path::PathBuf> {
fn as_ref(&self) -> &std::path::Path { self.inner.as_ref() } fn as_ref(&self) -> &std::path::Path { self.inner.as_ref() }
} }
impl<T> AsPath for LocBuf<T> impl<T> DynPath for LocBuf<T>
where where
T: LocBufAble + AsPath, T: LocBufAble + DynPath,
{ {
fn as_path(&self) -> PathDyn<'_> { self.inner.as_path() } fn dyn_path(&self) -> PathDyn<'_> { self.inner.dyn_path() }
} }
impl<T> AsPath for &LocBuf<T> impl<T> DynPath for &LocBuf<T>
where where
T: LocBufAble + AsPath, T: LocBufAble + DynPath,
{ {
fn as_path(&self) -> PathDyn<'_> { self.inner.as_path() } fn dyn_path(&self) -> PathDyn<'_> { self.inner.dyn_path() }
} }
impl<P> Ord for LocBuf<P> impl<P> Ord for LocBuf<P>
@ -59,7 +59,7 @@ where
impl<P> Hash for LocBuf<P> impl<P> Hash for LocBuf<P>
where where
P: LocBufAble + LocBufAbleImpl, P: LocBufAble + LocBufAbleImpl,
for<'a> &'a P: AsPathView<'a, P::Borrowed<'a>>, for<'a> &'a P: PathView<'a, P::Borrowed<'a>>,
{ {
fn hash<H: Hasher>(&self, state: &mut H) { self.as_loc().hash(state) } fn hash<H: Hasher>(&self, state: &mut H) { self.as_loc().hash(state) }
} }
@ -67,7 +67,7 @@ where
impl<P> Debug for LocBuf<P> impl<P> Debug for LocBuf<P>
where where
P: LocBufAble + LocBufAbleImpl + Debug, P: LocBufAble + LocBufAbleImpl + Debug,
for<'a> &'a P: AsPathView<'a, P::Borrowed<'a>>, for<'a> &'a P: PathView<'a, P::Borrowed<'a>>,
{ {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.debug_struct("LocBuf") f.debug_struct("LocBuf")
@ -81,7 +81,7 @@ where
impl<P> From<P> for LocBuf<P> impl<P> From<P> for LocBuf<P>
where where
P: LocBufAble + LocBufAbleImpl, P: LocBufAble + LocBufAbleImpl,
for<'a> &'a P: AsPathView<'a, P::Borrowed<'a>>, for<'a> &'a P: PathView<'a, P::Borrowed<'a>>,
{ {
fn from(path: P) -> Self { fn from(path: P) -> Self {
let Loc { inner, uri, urn, _phantom } = Loc::bare(&path); let Loc { inner, uri, urn, _phantom } = Loc::bare(&path);
@ -100,7 +100,7 @@ impl<T: ?Sized + AsRef<OsStr>> From<&T> for LocBuf<std::path::PathBuf> {
impl<P> LocBuf<P> impl<P> LocBuf<P>
where where
P: LocBufAble + LocBufAbleImpl, P: LocBufAble + LocBufAbleImpl,
for<'a> &'a P: AsPathView<'a, P::Borrowed<'a>>, for<'a> &'a P: PathView<'a, P::Borrowed<'a>>,
{ {
pub fn new<'a, S>(path: P, base: S, trail: S) -> Self pub fn new<'a, S>(path: P, base: S, trail: S) -> Self
where where
@ -157,7 +157,7 @@ where
#[inline] #[inline]
pub fn as_loc<'a>(&'a self) -> Loc<'a, P::Borrowed<'a>> { pub fn as_loc<'a>(&'a self) -> Loc<'a, P::Borrowed<'a>> {
Loc { Loc {
inner: self.inner.as_path_view(), inner: self.inner.path_view(),
uri: self.uri, uri: self.uri,
urn: self.urn, urn: self.urn,
_phantom: PhantomData, _phantom: PhantomData,
@ -223,7 +223,7 @@ where
impl<P> LocBuf<P> impl<P> LocBuf<P>
where where
P: LocBufAble + LocBufAbleImpl, P: LocBufAble + LocBufAbleImpl,
for<'a> &'a P: AsPathView<'a, P::Borrowed<'a>>, for<'a> &'a P: PathView<'a, P::Borrowed<'a>>,
{ {
#[inline] #[inline]
pub fn uri(&self) -> P::Borrowed<'_> { self.as_loc().uri() } pub fn uri(&self) -> P::Borrowed<'_> { self.as_loc().uri() }

View file

@ -3,7 +3,7 @@ use std::{hash::{Hash, Hasher}, marker::PhantomData, ops::Deref};
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use super::LocAbleImpl; use super::LocAbleImpl;
use crate::{auth::AuthKind, loc::{LocAble, LocBuf, LocBufAble, StrandAbleImpl}, path::{AsPath, AsPathView, PathDyn}, strand::AsStrandView}; use crate::{auth::AuthKind, loc::{LocAble, LocBuf, LocBufAble, StrandAbleImpl}, path::{DynPath, PathDyn, PathView}, strand::AsStrandView};
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub struct Loc<'p, P = &'p std::path::Path> { pub struct Loc<'p, P = &'p std::path::Path> {
@ -29,11 +29,11 @@ where
fn deref(&self) -> &Self::Target { &self.inner } fn deref(&self) -> &Self::Target { &self.inner }
} }
impl<'p, P> AsPath for Loc<'p, P> impl<'p, P> DynPath for Loc<'p, P>
where where
P: LocAble<'p> + AsPath, P: LocAble<'p> + DynPath,
{ {
fn as_path(&self) -> PathDyn<'_> { self.inner.as_path() } fn dyn_path(&self) -> PathDyn<'_> { self.inner.dyn_path() }
} }
// FIXME: remove // FIXME: remove
@ -81,9 +81,9 @@ where
pub fn bare<T>(path: T) -> Self pub fn bare<T>(path: T) -> Self
where where
T: AsPathView<'p, P>, T: PathView<'p, P>,
{ {
let path = path.as_path_view(); let path = path.path_view();
let Some(name) = path.file_name() else { let Some(name) = path.file_name() else {
let p = path.strip_prefix(P::empty()).unwrap(); let p = path.strip_prefix(P::empty()).unwrap();
return Self { inner: p, uri: 0, urn: 0, _phantom: PhantomData }; return Self { inner: p, uri: 0, urn: 0, _phantom: PhantomData };
@ -114,7 +114,7 @@ where
pub fn floated<'a, T, S>(path: T, base: S) -> Self pub fn floated<'a, T, S>(path: T, base: S) -> Self
where where
T: AsPathView<'p, P>, T: PathView<'p, P>,
S: AsStrandView<'a, P::Strand<'a>>, S: AsStrandView<'a, P::Strand<'a>>,
{ {
let mut loc = Self::bare(path); let mut loc = Self::bare(path);
@ -133,7 +133,7 @@ where
pub fn new<'a, T, S>(path: T, base: S, trail: S) -> Self pub fn new<'a, T, S>(path: T, base: S, trail: S) -> Self
where where
T: AsPathView<'p, P>, T: PathView<'p, P>,
S: AsStrandView<'a, P::Strand<'a>>, S: AsStrandView<'a, P::Strand<'a>>,
{ {
let mut loc = Self::bare(path); let mut loc = Self::bare(path);
@ -143,18 +143,17 @@ where
} }
#[inline] #[inline]
pub fn parent(self) -> Option<P> { pub fn parent(self) -> Option<P> { self.inner.parent() }
self.inner.parent().filter(|p| !p.as_encoded_bytes().is_empty())
}
pub fn saturated<'a, T>(path: T, kind: AuthKind) -> Self pub fn saturated<'a, T>(path: T, kind: AuthKind) -> Self
where where
T: AsPathView<'p, P>, T: PathView<'p, P>,
{ {
match kind { match kind {
AuthKind::Regular => Self::bare(path), AuthKind::Regular => Self::bare(path),
AuthKind::Search => Self::zeroed(path), AuthKind::Search => Self::zeroed(path),
AuthKind::Mount => Self::zeroed(path), AuthKind::Mount => Self::zeroed(path),
AuthKind::Hub => Self::bare(path),
AuthKind::Scope => Self::bare(path), AuthKind::Scope => Self::bare(path),
AuthKind::Sftp => Self::bare(path), AuthKind::Sftp => Self::bare(path),
} }
@ -206,7 +205,7 @@ where
pub fn with<T>(path: T, uri: usize, urn: usize) -> Result<Self> pub fn with<T>(path: T, uri: usize, urn: usize) -> Result<Self>
where where
T: AsPathView<'p, P>, T: PathView<'p, P>,
{ {
if urn > uri { if urn > uri {
bail!("URN cannot be longer than URI"); bail!("URN cannot be longer than URI");
@ -238,7 +237,7 @@ where
pub fn zeroed<T>(path: T) -> Self pub fn zeroed<T>(path: T) -> Self
where where
T: AsPathView<'p, P>, T: PathView<'p, P>,
{ {
let mut loc = Self::bare(path); let mut loc = Self::bare(path);
(loc.uri, loc.urn) = (0, 0); (loc.uri, loc.urn) = (0, 0);
@ -248,8 +247,21 @@ where
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::path::Path;
use typed_path::UnixPath;
use super::*; use super::*;
#[test]
fn test_parent() {
assert_eq!(Loc::bare(Path::new("foo")).parent(), Some(Path::new("")));
assert_eq!(Loc::bare(Path::new("")).parent(), None);
assert_eq!(Loc::bare(UnixPath::new("foo")).parent(), Some(UnixPath::new("")));
assert_eq!(Loc::bare(UnixPath::new("")).parent(), None);
}
#[test] #[test]
fn test_with() -> Result<()> { fn test_with() -> Result<()> {
let cases = [ let cases = [
@ -268,11 +280,11 @@ mod tests {
]; ];
for (path, uri, urn, expect_uri, expect_urn) in cases { for (path, uri, urn, expect_uri, expect_urn) in cases {
let loc = Loc::with(std::path::Path::new(path), uri, urn)?; let loc = Loc::with(Path::new(path), uri, urn)?;
assert_eq!(loc.uri().to_str().unwrap(), expect_uri); assert_eq!(loc.uri().to_str().unwrap(), expect_uri);
assert_eq!(loc.urn().to_str().unwrap(), expect_urn); assert_eq!(loc.urn().to_str().unwrap(), expect_urn);
let loc = Loc::with(typed_path::UnixPath::new(path), uri, urn)?; let loc = Loc::with(UnixPath::new(path), uri, urn)?;
assert_eq!(loc.uri().to_str().unwrap(), expect_uri); assert_eq!(loc.uri().to_str().unwrap(), expect_uri);
assert_eq!(loc.urn().to_str().unwrap(), expect_urn); assert_eq!(loc.urn().to_str().unwrap(), expect_urn);
} }

View file

@ -5,7 +5,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
use yazi_codegen::FromLuaOwned; use yazi_codegen::FromLuaOwned;
use yazi_shim::wtf8::FromWtf8Vec; use yazi_shim::wtf8::FromWtf8Vec;
use crate::{path::{AsPath, Component, PathDyn, PathDynError, PathKind, SetNameError}, strand::AsStrand}; use crate::{path::{Component, DynPath, PathDyn, PathDynError, PathKind, SetNameError}, strand::AsStrand};
// --- PathBufDyn // --- PathBufDyn
#[derive(Clone, Debug, Eq, FromLuaOwned)] #[derive(Clone, Debug, Eq, FromLuaOwned)]
@ -48,24 +48,24 @@ impl TryFrom<PathBufDyn> for typed_path::UnixPathBuf {
// --- Hash // --- Hash
impl Hash for PathBufDyn { impl Hash for PathBufDyn {
fn hash<H: Hasher>(&self, state: &mut H) { self.as_path().hash(state) } fn hash<H: Hasher>(&self, state: &mut H) { self.dyn_path().hash(state) }
} }
// --- PartialEq // --- PartialEq
impl PartialEq for PathBufDyn { impl PartialEq for PathBufDyn {
fn eq(&self, other: &Self) -> bool { self.as_path() == other.as_path() } fn eq(&self, other: &Self) -> bool { self.dyn_path() == other.dyn_path() }
} }
impl PartialEq<PathDyn<'_>> for PathBufDyn { impl PartialEq<PathDyn<'_>> for PathBufDyn {
fn eq(&self, other: &PathDyn<'_>) -> bool { self.as_path() == *other } fn eq(&self, other: &PathDyn<'_>) -> bool { self.dyn_path() == *other }
} }
impl PartialEq<PathDyn<'_>> for &PathBufDyn { impl PartialEq<PathDyn<'_>> for &PathBufDyn {
fn eq(&self, other: &PathDyn<'_>) -> bool { self.as_path() == *other } fn eq(&self, other: &PathDyn<'_>) -> bool { self.dyn_path() == *other }
} }
impl Equivalent<PathDyn<'_>> for PathBufDyn { impl Equivalent<PathDyn<'_>> for PathBufDyn {
fn equivalent(&self, key: &PathDyn<'_>) -> bool { self.as_path() == *key } fn equivalent(&self, key: &PathDyn<'_>) -> bool { self.dyn_path() == *key }
} }
impl PathBufDyn { impl PathBufDyn {
@ -126,7 +126,7 @@ impl PathBufDyn {
pub fn try_extend<T>(&mut self, paths: T) -> Result<(), PathDynError> pub fn try_extend<T>(&mut self, paths: T) -> Result<(), PathDynError>
where where
T: IntoIterator, T: IntoIterator,
T::Item: AsPath, T::Item: DynPath,
{ {
for p in paths { for p in paths {
self.try_push(p)?; self.try_push(p)?;
@ -136,9 +136,9 @@ impl PathBufDyn {
pub fn try_push<T>(&mut self, path: T) -> Result<(), PathDynError> pub fn try_push<T>(&mut self, path: T) -> Result<(), PathDynError>
where where
T: AsPath, T: DynPath,
{ {
let path = path.as_path(); let path = path.dyn_path();
Ok(match self { Ok(match self {
Self::Os(p) => p.push(path.as_os()?), Self::Os(p) => p.push(path.as_os()?),
Self::Unix(p) => p.push(path.encoded_bytes()), Self::Unix(p) => p.push(path.encoded_bytes()),
@ -199,7 +199,7 @@ impl Serialize for PathBufDyn {
path: &'a [u8], path: &'a [u8],
} }
let path = self.as_path(); let path = self.dyn_path();
Shadow { kind: path.kind(), path: path.encoded_bytes() }.serialize(serializer) Shadow { kind: path.kind(), path: path.encoded_bytes() }.serialize(serializer)
} }
} }

View file

@ -18,6 +18,9 @@ impl<'a> Component<'a> {
_ => None, _ => None,
} }
} }
#[inline]
pub fn has_auth(&self) -> bool { matches!(self, Self::ParentDir | Self::Normal(_)) }
} }
impl<'a> From<std::path::Component<'a>> for Component<'a> { impl<'a> From<std::path::Component<'a>> for Component<'a> {

View file

@ -10,7 +10,17 @@ pub enum Components<'a> {
Unix(typed_path::UnixComponents<'a>), Unix(typed_path::UnixComponents<'a>),
} }
impl<'a> From<&'a std::path::Path> for Components<'a> {
fn from(value: &'a std::path::Path) -> Self { Self::Os(value.components()) }
}
impl<'a> From<&'a typed_path::UnixPath> for Components<'a> {
fn from(value: &'a typed_path::UnixPath) -> Self { Self::Unix(value.components()) }
}
impl<'a> Components<'a> { impl<'a> Components<'a> {
pub fn auth_depth(self) -> usize { self.filter(Component::has_auth).count() }
pub fn path(&self) -> PathDyn<'a> { pub fn path(&self) -> PathDyn<'a> {
match self { match self {
Self::Os(c) => PathDyn::Os(c.as_path()), Self::Os(c) => PathDyn::Os(c.as_path()),

View file

@ -1,58 +0,0 @@
use super::{PathBufDyn, PathDyn};
use crate::path::PathCow;
// --- AsPath
pub trait AsPath {
fn as_path(&self) -> PathDyn<'_>;
}
impl AsPath for std::path::Path {
fn as_path(&self) -> PathDyn<'_> { PathDyn::Os(self) }
}
impl AsPath for std::path::PathBuf {
fn as_path(&self) -> PathDyn<'_> { PathDyn::Os(self) }
}
impl AsPath for typed_path::UnixPath {
fn as_path(&self) -> PathDyn<'_> { PathDyn::Unix(self) }
}
impl AsPath for typed_path::UnixPathBuf {
fn as_path(&self) -> PathDyn<'_> { PathDyn::Unix(self) }
}
impl AsPath for PathDyn<'_> {
fn as_path(&self) -> PathDyn<'_> { *self }
}
impl AsPath for PathBufDyn {
fn as_path(&self) -> PathDyn<'_> {
match self {
Self::Os(p) => PathDyn::Os(p),
Self::Unix(p) => PathDyn::Unix(p),
}
}
}
impl AsPath for PathCow<'_> {
fn as_path(&self) -> PathDyn<'_> {
match self {
PathCow::Borrowed(p) => *p,
PathCow::Owned(p) => p.as_path(),
}
}
}
impl AsPath for super::Components<'_> {
fn as_path(&self) -> PathDyn<'_> { self.path() }
}
// --- AsPathRef
pub trait AsPathRef<'a> {
fn as_path_ref(self) -> PathDyn<'a>;
}
impl<'a> AsPathRef<'a> for PathDyn<'a> {
fn as_path_ref(self) -> Self { self }
}

View file

@ -2,7 +2,7 @@ use std::borrow::Cow;
use anyhow::Result; use anyhow::Result;
use crate::path::{AsPath, PathBufDyn, PathDyn, PathDynError, PathKind}; use crate::path::{DynPath, PathBufDyn, PathDyn, PathDynError, PathKind};
#[derive(Debug)] #[derive(Debug)]
pub enum PathCow<'a> { pub enum PathCow<'a> {
@ -23,7 +23,7 @@ impl<'a> From<std::path::PathBuf> for PathCow<'a> {
} }
impl<'a> From<&'a PathCow<'_>> for PathCow<'a> { impl<'a> From<&'a PathCow<'_>> for PathCow<'a> {
fn from(value: &'a PathCow<'_>) -> Self { Self::Borrowed(value.as_path()) } fn from(value: &'a PathCow<'_>) -> Self { Self::Borrowed(value.dyn_path()) }
} }
impl From<PathCow<'_>> for PathBufDyn { impl From<PathCow<'_>> for PathBufDyn {
@ -31,14 +31,14 @@ impl From<PathCow<'_>> for PathBufDyn {
} }
impl PartialEq for PathCow<'_> { impl PartialEq for PathCow<'_> {
fn eq(&self, other: &Self) -> bool { self.as_path() == other.as_path() } fn eq(&self, other: &Self) -> bool { self.dyn_path() == other.dyn_path() }
} }
impl PartialEq<&str> for PathCow<'_> { impl PartialEq<&str> for PathCow<'_> {
fn eq(&self, other: &&str) -> bool { fn eq(&self, other: &&str) -> bool {
match self { match self {
Self::Borrowed(s) => s.as_path() == *other, Self::Borrowed(s) => s.dyn_path() == *other,
Self::Owned(s) => s.as_path() == *other, Self::Owned(s) => s.dyn_path() == *other,
} }
} }
} }

View file

@ -0,0 +1,66 @@
use super::{PathBufDyn, PathDyn};
use crate::path::PathCow;
// --- DynPath
pub trait DynPath {
fn dyn_path(&self) -> PathDyn<'_>;
}
impl DynPath for std::path::Path {
fn dyn_path(&self) -> PathDyn<'_> { PathDyn::Os(self) }
}
impl DynPath for std::path::PathBuf {
fn dyn_path(&self) -> PathDyn<'_> { PathDyn::Os(self) }
}
impl DynPath for typed_path::UnixPath {
fn dyn_path(&self) -> PathDyn<'_> { PathDyn::Unix(self) }
}
impl DynPath for typed_path::UnixPathBuf {
fn dyn_path(&self) -> PathDyn<'_> { PathDyn::Unix(self) }
}
impl DynPath for PathDyn<'_> {
fn dyn_path(&self) -> PathDyn<'_> { *self }
}
impl DynPath for PathBufDyn {
fn dyn_path(&self) -> PathDyn<'_> {
match self {
Self::Os(p) => PathDyn::Os(p),
Self::Unix(p) => PathDyn::Unix(p),
}
}
}
impl DynPath for PathCow<'_> {
fn dyn_path(&self) -> PathDyn<'_> {
match self {
PathCow::Borrowed(p) => *p,
PathCow::Owned(p) => p.dyn_path(),
}
}
}
impl DynPath for super::Components<'_> {
fn dyn_path(&self) -> PathDyn<'_> { self.path() }
}
// --- DynPathRef
pub trait DynPathRef<'a> {
fn dyn_path_ref(self) -> PathDyn<'a>;
}
impl<'a> DynPathRef<'a> for PathDyn<'a> {
fn dyn_path_ref(self) -> Self { self }
}
impl<'a> DynPathRef<'a> for &'a std::path::Path {
fn dyn_path_ref(self) -> PathDyn<'a> { PathDyn::Os(self) }
}
impl<'a> DynPathRef<'a> for &'a typed_path::UnixPath {
fn dyn_path_ref(self) -> PathDyn<'a> { PathDyn::Unix(self) }
}

View file

@ -15,6 +15,7 @@ impl From<AuthKind> for PathKind {
AuthKind::Regular => Self::Os, AuthKind::Regular => Self::Os,
AuthKind::Search => Self::Os, AuthKind::Search => Self::Os,
AuthKind::Mount => Self::Os, AuthKind::Mount => Self::Os,
AuthKind::Hub => Self::Os,
AuthKind::Scope => Self::Unix, AuthKind::Scope => Self::Unix,
AuthKind::Sftp => Self::Unix, AuthKind::Sftp => Self::Unix,
} }

View file

@ -2,95 +2,97 @@ use std::borrow::Cow;
use anyhow::Result; use anyhow::Result;
use crate::{Utf8BytePredictor, path::{AsPath, Components, Display, EndsWithError, JoinError, PathBufDyn, PathCow, PathDyn, PathDynError, PathKind, RsplitOnceError, StartsWithError, StripPrefixError, StripSuffixError}, strand::{AsStrand, Strand}}; use crate::{Utf8BytePredictor, path::{Components, Display, DynPath, EndsWithError, JoinError, PathBufDyn, PathCow, PathDyn, PathDynError, PathKind, RsplitOnceError, StartsWithError, StripPrefixError, StripSuffixError}, strand::{AsStrand, Strand}};
pub trait PathLike: AsPath { pub trait PathLike: DynPath {
fn as_os(&self) -> Result<&std::path::Path, PathDynError> { self.as_path().as_os() } fn as_os(&self) -> Result<&std::path::Path, PathDynError> { self.dyn_path().as_os() }
fn as_unix(&self) -> Result<&typed_path::UnixPath, PathDynError> { self.as_path().as_unix() } fn as_unix(&self) -> Result<&typed_path::UnixPath, PathDynError> { self.dyn_path().as_unix() }
fn components(&self) -> Components<'_> { self.as_path().components() } fn components(&self) -> Components<'_> { self.dyn_path().components() }
fn display(&self) -> Display<'_> { self.as_path().display() } fn display(&self) -> Display<'_> { self.dyn_path().display() }
fn encoded_bytes(&self) -> &[u8] { self.as_path().encoded_bytes() } fn encoded_bytes(&self) -> &[u8] { self.dyn_path().encoded_bytes() }
fn ext(&self) -> Option<Strand<'_>> { self.as_path().ext() } fn ext(&self) -> Option<Strand<'_>> { self.dyn_path().ext() }
fn has_root(&self) -> bool { self.as_path().has_root() } fn has_root(&self) -> bool { self.dyn_path().has_root() }
fn is_absolute(&self) -> bool { self.as_path().is_absolute() } fn is_absolute(&self) -> bool { self.dyn_path().is_absolute() }
fn is_empty(&self) -> bool { self.as_path().is_empty() } fn is_empty(&self) -> bool { self.dyn_path().is_empty() }
#[cfg(unix)] #[cfg(unix)]
fn is_hidden(&self) -> bool { self.as_path().is_hidden() } fn is_hidden(&self) -> bool { self.dyn_path().is_hidden() }
fn kind(&self) -> PathKind { self.as_path().kind() } fn kind(&self) -> PathKind { self.dyn_path().kind() }
fn len(&self) -> usize { self.as_path().len() } fn len(&self) -> usize { self.dyn_path().len() }
fn name(&self) -> Option<Strand<'_>> { self.as_path().name() } fn name(&self) -> Option<Strand<'_>> { self.dyn_path().name() }
fn parent(&self) -> Option<PathDyn<'_>> { self.as_path().parent() } fn parent(&self) -> Option<PathDyn<'_>> { self.dyn_path().parent() }
fn rsplit_pred<T>(&self, pred: T) -> Option<(PathDyn<'_>, PathDyn<'_>)> fn rsplit_pred<T>(&self, pred: T) -> Option<(PathDyn<'_>, PathDyn<'_>)>
where where
T: Utf8BytePredictor, T: Utf8BytePredictor,
{ {
self.as_path().rsplit_pred(pred) self.dyn_path().rsplit_pred(pred)
} }
fn stem(&self) -> Option<Strand<'_>> { self.as_path().stem() } fn stem(&self) -> Option<Strand<'_>> { self.dyn_path().stem() }
fn to_os_owned(&self) -> Result<std::path::PathBuf, PathDynError> { self.as_path().to_os_owned() } fn to_os_owned(&self) -> Result<std::path::PathBuf, PathDynError> {
self.dyn_path().to_os_owned()
}
fn to_owned(&self) -> PathBufDyn { self.as_path().to_owned() } fn to_owned(&self) -> PathBufDyn { self.dyn_path().to_owned() }
fn to_str(&self) -> Result<&str, std::str::Utf8Error> { self.as_path().to_str() } fn to_str(&self) -> Result<&str, std::str::Utf8Error> { self.dyn_path().to_str() }
fn to_string_lossy(&self) -> Cow<'_, str> { self.as_path().to_string_lossy() } fn to_string_lossy(&self) -> Cow<'_, str> { self.dyn_path().to_string_lossy() }
fn try_ends_with<T>(&self, child: T) -> Result<bool, EndsWithError> fn try_ends_with<T>(&self, child: T) -> Result<bool, EndsWithError>
where where
T: AsStrand, T: AsStrand,
{ {
self.as_path().try_ends_with(child) self.dyn_path().try_ends_with(child)
} }
fn try_join<T>(&self, path: T) -> Result<PathBufDyn, JoinError> fn try_join<T>(&self, path: T) -> Result<PathBufDyn, JoinError>
where where
T: AsStrand, T: AsStrand,
{ {
self.as_path().try_join(path) self.dyn_path().try_join(path)
} }
fn try_rsplit_seq<T>(&self, pat: T) -> Result<(PathDyn<'_>, PathDyn<'_>), RsplitOnceError> fn try_rsplit_seq<T>(&self, pat: T) -> Result<(PathDyn<'_>, PathDyn<'_>), RsplitOnceError>
where where
T: AsStrand, T: AsStrand,
{ {
self.as_path().try_rsplit_seq(pat) self.dyn_path().try_rsplit_seq(pat)
} }
fn try_starts_with<T>(&self, base: T) -> Result<bool, StartsWithError> fn try_starts_with<T>(&self, base: T) -> Result<bool, StartsWithError>
where where
T: AsStrand, T: AsStrand,
{ {
self.as_path().try_starts_with(base) self.dyn_path().try_starts_with(base)
} }
fn try_strip_prefix<T>(&self, base: T) -> Result<PathDyn<'_>, StripPrefixError> fn try_strip_prefix<T>(&self, base: T) -> Result<PathDyn<'_>, StripPrefixError>
where where
T: AsStrand, T: AsStrand,
{ {
self.as_path().try_strip_prefix(base) self.dyn_path().try_strip_prefix(base)
} }
fn try_strip_suffix<T>(&self, suffix: T) -> Result<PathDyn<'_>, StripSuffixError> fn try_strip_suffix<T>(&self, suffix: T) -> Result<PathDyn<'_>, StripSuffixError>
where where
T: AsStrand, T: AsStrand,
{ {
self.as_path().try_strip_suffix(suffix) self.dyn_path().try_strip_suffix(suffix)
} }
} }

View file

@ -1 +1 @@
yazi_macro::mod_flat!(buf component components conversion cow display error kind like lua path view); yazi_macro::mod_flat!(buf component components cow display dyn_path error kind like lua path view);

View file

@ -4,7 +4,7 @@ use anyhow::Result;
use hashbrown::Equivalent; use hashbrown::Equivalent;
use super::{RsplitOnceError, StartsWithError}; use super::{RsplitOnceError, StartsWithError};
use crate::{BytesExt, Utf8BytePredictor, path::{AsPath, Components, Display, EndsWithError, JoinError, PathBufDyn, PathDynError, PathKind, StripPrefixError, StripSuffixError}, strand::{AsStrand, Strand, StrandError}}; use crate::{BytesExt, Utf8BytePredictor, path::{Components, Display, DynPath, EndsWithError, JoinError, PathBufDyn, PathDynError, PathKind, StripPrefixError, StripSuffixError}, strand::{AsStrand, Strand, StrandError}};
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum PathDyn<'p> { pub enum PathDyn<'p> {
@ -21,11 +21,11 @@ impl<'a> From<&'a typed_path::UnixPath> for PathDyn<'a> {
} }
impl<'a> From<&'a PathBufDyn> for PathDyn<'a> { impl<'a> From<&'a PathBufDyn> for PathDyn<'a> {
fn from(value: &'a PathBufDyn) -> Self { value.as_path() } fn from(value: &'a PathBufDyn) -> Self { value.dyn_path() }
} }
impl PartialEq<PathBufDyn> for PathDyn<'_> { impl PartialEq<PathBufDyn> for PathDyn<'_> {
fn eq(&self, other: &PathBufDyn) -> bool { *self == other.as_path() } fn eq(&self, other: &PathBufDyn) -> bool { *self == other.dyn_path() }
} }
impl PartialEq<PathDyn<'_>> for &std::path::Path { impl PartialEq<PathDyn<'_>> for &std::path::Path {
@ -36,6 +36,12 @@ impl PartialEq<&std::path::Path> for PathDyn<'_> {
fn eq(&self, other: &&std::path::Path) -> bool { matches!(*self, PathDyn::Os(p) if p == *other) } fn eq(&self, other: &&std::path::Path) -> bool { matches!(*self, PathDyn::Os(p) if p == *other) }
} }
impl PartialEq<&typed_path::UnixPath> for PathDyn<'_> {
fn eq(&self, other: &&typed_path::UnixPath) -> bool {
matches!(*self, PathDyn::Unix(p) if p == *other)
}
}
impl PartialEq<&str> for PathDyn<'_> { impl PartialEq<&str> for PathDyn<'_> {
fn eq(&self, other: &&str) -> bool { fn eq(&self, other: &&str) -> bool {
match *self { match *self {
@ -46,7 +52,7 @@ impl PartialEq<&str> for PathDyn<'_> {
} }
impl Equivalent<PathBufDyn> for PathDyn<'_> { impl Equivalent<PathBufDyn> for PathDyn<'_> {
fn equivalent(&self, key: &PathBufDyn) -> bool { *self == key.as_path() } fn equivalent(&self, key: &PathBufDyn) -> bool { *self == key.dyn_path() }
} }
impl<'p> PathDyn<'p> { impl<'p> PathDyn<'p> {
@ -139,8 +145,8 @@ impl<'p> PathDyn<'p> {
pub fn parent(self) -> Option<Self> { pub fn parent(self) -> Option<Self> {
Some(match self { Some(match self {
Self::Os(p) => Self::Os(p.parent().filter(|p| !p.as_os_str().is_empty())?), Self::Os(p) => Self::Os(p.parent()?),
Self::Unix(p) => Self::Unix(p.parent().filter(|p| !p.as_bytes().is_empty())?), Self::Unix(p) => Self::Unix(p.parent()?),
}) })
} }
@ -296,3 +302,34 @@ impl<'p> PathDyn<'p> {
} }
} }
} }
#[cfg(test)]
mod tests {
use std::path::Path;
use typed_path::UnixPath;
use super::*;
#[test]
fn test_parent() {
let path = Path::new("foo").dyn_path();
assert_eq!(path.parent(), Some(Path::new("").dyn_path()));
assert_eq!(path.parent().unwrap().parent(), None);
let path = UnixPath::new("foo").dyn_path();
assert_eq!(path.parent(), Some(UnixPath::new("").dyn_path()));
assert_eq!(path.parent().unwrap().parent(), None);
}
#[cfg(windows)]
#[test]
fn test_windows_parent() {
let path = Path::new("foo").dyn_path();
assert_eq!(path.parent(), Some(Path::new("").dyn_path()));
let path = Path::new("C:foo").dyn_path();
assert_eq!(path.parent(), Some(Path::new("C:").dyn_path()));
assert_eq!(path.parent().unwrap().parent(), None);
}
}

View file

@ -1,20 +1,20 @@
// --- AsPathView // --- PathView
pub trait AsPathView<'a, T> { pub trait PathView<'a, T> {
fn as_path_view(self) -> T; fn path_view(self) -> T;
} }
impl<'a> AsPathView<'a, &'a std::path::Path> for &'a std::path::Path { impl<'a> PathView<'a, &'a std::path::Path> for &'a std::path::Path {
fn as_path_view(self) -> &'a std::path::Path { self } fn path_view(self) -> &'a std::path::Path { self }
} }
impl<'a> AsPathView<'a, &'a std::path::Path> for &'a std::path::PathBuf { impl<'a> PathView<'a, &'a std::path::Path> for &'a std::path::PathBuf {
fn as_path_view(self) -> &'a std::path::Path { self } fn path_view(self) -> &'a std::path::Path { self }
} }
impl<'a> AsPathView<'a, &'a typed_path::UnixPath> for &'a typed_path::UnixPath { impl<'a> PathView<'a, &'a typed_path::UnixPath> for &'a typed_path::UnixPath {
fn as_path_view(self) -> &'a typed_path::UnixPath { self } fn path_view(self) -> &'a typed_path::UnixPath { self }
} }
impl<'a> AsPathView<'a, &'a typed_path::UnixPath> for &'a typed_path::UnixPathBuf { impl<'a> PathView<'a, &'a typed_path::UnixPath> for &'a typed_path::UnixPathBuf {
fn as_path_view(self) -> &'a typed_path::UnixPath { self } fn path_view(self) -> &'a typed_path::UnixPath { self }
} }

View file

@ -1,17 +1,17 @@
use std::fmt::{self, Display}; use std::fmt::{self, Display};
use crate::{auth::{AuthKind, Encode as EncodeAuth}, url::Url}; use crate::{auth::{AuthKind, EncodeAuth, EncodePrefix}, url::Url};
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub struct Encode<'a>(pub Url<'a>); pub struct EncodeSpec<'a>(pub Url<'a>);
impl<'a> From<crate::url::Encode<'a>> for Encode<'a> { impl<'a> From<crate::url::Encode<'a>> for EncodeSpec<'a> {
fn from(value: crate::url::Encode<'a>) -> Self { Self(value.0) } fn from(value: crate::url::Encode<'a>) -> Self { Self(value.0) }
} }
impl<'a> Encode<'a> { impl<'a> EncodeSpec<'a> {
pub(crate) fn ports(self) -> impl Display { pub(crate) fn ports(self) -> impl Display {
struct D<'a>(Encode<'a>); struct D<'a>(EncodeSpec<'a>);
impl Display for D<'_> { impl Display for D<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@ -30,7 +30,7 @@ impl<'a> Encode<'a> {
match self.0.0.kind() { match self.0.0.kind() {
AuthKind::Regular => Ok(()), AuthKind::Regular => Ok(()),
AuthKind::Search | AuthKind::Mount => w!(0, 0), AuthKind::Search | AuthKind::Mount => w!(0, 0),
AuthKind::Scope | AuthKind::Sftp => { AuthKind::Hub | AuthKind::Scope | AuthKind::Sftp => {
w!(self.0.0.loc().name().is_some() as usize, self.0.0.loc().name().is_some() as usize) w!(self.0.0.loc().name().is_some() as usize, self.0.0.loc().name().is_some() as usize)
} }
} }
@ -41,15 +41,16 @@ impl<'a> Encode<'a> {
} }
} }
impl Display for Encode<'_> { impl Display for EncodeSpec<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0 { match self.0 {
Url::Regular(_) => write!(f, "regular://"), Url::Regular(_) => write!(f, "regular://"),
Url::Search { auth, .. } Url::Search { auth, .. }
| Url::Mount { auth, .. } | Url::Mount { auth, .. }
| Url::Hub { auth, .. }
| Url::Scope { auth, .. } | Url::Scope { auth, .. }
| Url::Sftp { auth, .. } => { | Url::Sftp { auth, .. } => {
write!(f, "{}{}/", EncodeAuth(auth, false), self.ports()) write!(f, "{}{}{}", EncodeAuth(auth, false), self.ports(), EncodePrefix(auth))
} }
} }
} }

View file

@ -4,7 +4,7 @@ use anyhow::{Result, anyhow, ensure};
use percent_encoding::percent_decode; use percent_encoding::percent_decode;
use serde::Deserialize; use serde::Deserialize;
use crate::{auth::{Auth, AuthKind, Encode, Scheme}, path::{PathCow, PathLike}, spec::ParsedSpec, url::Url}; use crate::{auth::{Auth, AuthKind, Domain, EncodeAuth, Scheme}, path::{PathCow, PathLike}, spec::ParsedSpec, url::Url};
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] #[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct Spec { pub struct Spec {
@ -28,18 +28,23 @@ impl Spec {
// Decode domain and ports // Decode domain and ports
let mut skip = 0; let mut skip = 0;
let (domain, uri, urn) = match &parsed.scheme { let (domain, uri, urn) = match &parsed.scheme {
Scheme::Regular => (Cow::Borrowed(""), None, None), Scheme::Regular => (Domain::default(), None, None),
_ => Self::decode_param(rest, &mut skip)?, _ => Self::decode_param(rest, &mut skip)?,
}; };
// Resolve authority // Resolve authority
let auth = Auth::get(&parsed.scheme, &domain) let auth = Auth::get(&parsed.scheme, &domain).ok_or_else(|| {
.ok_or_else(|| anyhow!("unknown VFS authority: {parsed}://{}", Encode::domain(&domain)))?; anyhow!("unknown VFS authority: {parsed}://{}", EncodeAuth::domain(&domain))
})?;
// Decode path // Decode path
let path = Self::decode_path(auth.kind, parsed.tilde, &rest[skip..])?; let (path, auth) = if auth.kind == AuthKind::Hub {
let (uri, urn) = Self::normalize_ports(auth.kind, uri, urn, &path)?; Self::decode_hub(auth, parsed.tilde, &rest[skip..])?
} else {
(Self::decode_path(auth.kind, parsed.tilde, &rest[skip..])?, auth)
};
let (uri, urn) = Self::normalize_ports(auth.kind, uri, urn, &path)?;
Ok((Self { auth, uri, urn }, path)) Ok((Self { auth, uri, urn }, path))
} }
@ -54,16 +59,13 @@ impl Spec {
fn decode_param<'a>( fn decode_param<'a>(
bytes: &'a [u8], bytes: &'a [u8],
skip: &mut usize, skip: &mut usize,
) -> Result<(Cow<'a, str>, Option<usize>, Option<usize>)> { ) -> Result<(Domain<'a>, Option<usize>, Option<usize>)> {
let mut len = bytes.iter().copied().take_while(|&b| b != b'/').count(); let mut len = bytes.iter().copied().take_while(|&b| b != b'/').count();
let slash = bytes.get(len).is_some_and(|&b| b == b'/'); let slash = bytes.get(len).is_some_and(|&b| b == b'/');
*skip += len + slash as usize; *skip += len + slash as usize;
let (uri, urn) = Self::decode_ports(&bytes[..len], &mut len)?; let (uri, urn) = Self::decode_ports(&bytes[..len], &mut len)?;
let domain = match Cow::from(percent_decode(&bytes[..len])) { let domain = percent_decode(&bytes[..len]).decode_utf8()?.into();
Cow::Borrowed(b) => str::from_utf8(b)?.into(),
Cow::Owned(b) => String::from_utf8(b)?.into(),
};
Ok((domain, uri, urn)) Ok((domain, uri, urn))
} }
@ -90,6 +92,41 @@ impl Spec {
PathCow::with(kind, bytes) PathCow::with(kind, bytes)
} }
fn decode_hub<'a>(
mut auth: Arc<Auth>,
tilde: bool,
bytes: &'a [u8],
) -> Result<(PathCow<'a>, Arc<Auth>)> {
ensure!(bytes.first() == Some(&b'@'), "Hub URL requires an `@` parent marker");
let end = bytes[1..]
.iter()
.position(|&b| b == b'/')
.map(|i| i + 1)
.ok_or_else(|| anyhow!("Hub URL requires a path delimiter"))?;
let path = Self::decode_path(AuthKind::Hub, tilde, &bytes[end + 1..])?;
let depth = path.components().auth_depth();
if depth == 0 {
ensure!(bytes[1..end].is_empty(), "Hub URL has too many parent domains");
return Ok((path, auth));
}
let (mut count, mut parent) = (0, None);
for domain in bytes[1..end].rsplit(|&b| b == b',') {
count += 1;
parent = Some(Arc::new(Auth {
kind: auth.kind,
scheme: auth.scheme.clone(),
domain: Domain::from(percent_decode(domain).decode_utf8()?).into_owned(),
parent,
}));
}
ensure!(count == depth, "Hub URL parent depth does not match its path");
Arc::make_mut(&mut auth).parent = parent;
Ok((path, auth))
}
fn normalize_ports( fn normalize_ports(
kind: AuthKind, kind: AuthKind,
uri: Option<usize>, uri: Option<usize>,
@ -107,7 +144,7 @@ impl Spec {
(uri, urn) (uri, urn)
} }
AuthKind::Mount => (uri.unwrap_or(0), urn.unwrap_or(0)), AuthKind::Mount => (uri.unwrap_or(0), urn.unwrap_or(0)),
AuthKind::Scope | AuthKind::Sftp => { AuthKind::Hub | AuthKind::Scope | AuthKind::Sftp => {
let uri = uri.unwrap_or(path.name().is_some() as usize); let uri = uri.unwrap_or(path.name().is_some() as usize);
let urn = urn.unwrap_or(path.name().is_some() as usize); let urn = urn.unwrap_or(path.name().is_some() as usize);
(uri, urn) (uri, urn)
@ -118,7 +155,7 @@ impl Spec {
pub fn retrieve_ports(url: Url) -> (usize, usize) { pub fn retrieve_ports(url: Url) -> (usize, usize) {
match url { match url {
Url::Regular(loc) => (loc.file_name().is_some() as usize, loc.file_name().is_some() as usize), Url::Regular(loc) => (loc.file_name().is_some() as usize, loc.file_name().is_some() as usize),
Url::Search { loc, .. } | Url::Mount { loc, .. } => { Url::Search { loc, .. } | Url::Mount { loc, .. } | Url::Hub { loc, .. } => {
(loc.uri().components().count(), loc.urn().components().count()) (loc.uri().components().count(), loc.urn().components().count())
} }
Url::Scope { loc, .. } | Url::Sftp { loc, .. } => { Url::Scope { loc, .. } | Url::Sftp { loc, .. } => {

View file

@ -22,6 +22,7 @@ impl From<AuthKind> for StrandKind {
AuthKind::Regular => Self::Os, AuthKind::Regular => Self::Os,
AuthKind::Search => Self::Os, AuthKind::Search => Self::Os,
AuthKind::Mount => Self::Os, AuthKind::Mount => Self::Os,
AuthKind::Hub => Self::Os,
AuthKind::Scope => Self::Bytes, AuthKind::Scope => Self::Bytes,
AuthKind::Sftp => Self::Bytes, AuthKind::Sftp => Self::Bytes,
} }

View file

@ -10,8 +10,9 @@ pub fn init_tests() {
inventory::submit! { inventory::submit! {
AuthInventory { AuthInventory {
get: |scheme, domain| match (scheme.as_str(), domain) { get: |scheme, domain| match (scheme.as_str(), domain.as_ref()) {
("test-mount", "7z") => Some(Auth::new(AuthKind::Mount, scheme.clone(), "7z")), ("test-mount", "7z") => Some(Auth::new(AuthKind::Mount, scheme.clone(), "7z")),
("test-hub", _) => Some(Auth::new(AuthKind::Hub, scheme.clone(), domain.clone())),
("test-scope", "aws") => Some(Auth::new(AuthKind::Scope, scheme.clone(), "aws")), ("test-scope", "aws") => Some(Auth::new(AuthKind::Scope, scheme.clone(), "aws")),
("sftp", "vps") => Some(Auth::new(AuthKind::Sftp, scheme.clone(), "vps")), ("sftp", "vps") => Some(Auth::new(AuthKind::Sftp, scheme.clone(), "vps")),
_ => None, _ => None,

View file

@ -5,13 +5,14 @@ use serde::{Deserialize, Serialize, de::{self, IntoDeserializer}};
use yazi_codegen::FromLuaOwned; use yazi_codegen::FromLuaOwned;
use yazi_macro::impl_data_any; use yazi_macro::impl_data_any;
use crate::{auth::Auth, loc::LocBuf, path::{PathBufDyn, PathDynError, SetNameError}, spec::Spec, strand::AsStrand, url::{AsUrl, Url, UrlCow, UrlDeserializer, UrlLike}}; use crate::{auth::{Auth, Domain}, loc::LocBuf, path::{PathBufDyn, PathDynError, SetNameError}, spec::Spec, strand::AsStrand, url::{AsUrl, Url, UrlCow, UrlDeserializer, UrlLike}};
#[derive(Clone, Eq, FromLuaOwned)] #[derive(Clone, Eq, FromLuaOwned)]
pub enum UrlBuf { pub enum UrlBuf {
Regular(LocBuf), Regular(LocBuf),
Search { loc: LocBuf, auth: Arc<Auth> }, Search { loc: LocBuf, auth: Arc<Auth> },
Mount { loc: LocBuf, auth: Arc<Auth> }, Mount { loc: LocBuf, auth: Arc<Auth> },
Hub { loc: LocBuf, auth: Arc<Auth> },
Scope { loc: LocBuf<typed_path::UnixPathBuf>, auth: Arc<Auth> }, Scope { loc: LocBuf<typed_path::UnixPathBuf>, auth: Arc<Auth> },
Sftp { loc: LocBuf<typed_path::UnixPathBuf>, auth: Arc<Auth> }, Sftp { loc: LocBuf<typed_path::UnixPathBuf>, auth: Arc<Auth> },
} }
@ -33,6 +34,7 @@ impl From<Url<'_>> for UrlBuf {
Url::Regular(loc) => Self::Regular(loc.into()), Url::Regular(loc) => Self::Regular(loc.into()),
Url::Search { loc, auth } => Self::Search { loc: loc.into(), auth: auth.clone() }, Url::Search { loc, auth } => Self::Search { loc: loc.into(), auth: auth.clone() },
Url::Mount { loc, auth } => Self::Mount { loc: loc.into(), auth: auth.clone() }, Url::Mount { loc, auth } => Self::Mount { loc: loc.into(), auth: auth.clone() },
Url::Hub { loc, auth } => Self::Hub { loc: loc.into(), auth: auth.clone() },
Url::Scope { loc, auth } => Self::Scope { loc: loc.into(), auth: auth.clone() }, Url::Scope { loc, auth } => Self::Scope { loc: loc.into(), auth: auth.clone() },
Url::Sftp { loc, auth } => Self::Sftp { loc: loc.into(), auth: auth.clone() }, Url::Sftp { loc, auth } => Self::Sftp { loc: loc.into(), auth: auth.clone() },
} }
@ -140,6 +142,7 @@ impl UrlBuf {
Self::Regular(loc) => loc.into_inner().into(), Self::Regular(loc) => loc.into_inner().into(),
Self::Search { loc, .. } => loc.into_inner().into(), Self::Search { loc, .. } => loc.into_inner().into(),
Self::Mount { loc, .. } => loc.into_inner().into(), Self::Mount { loc, .. } => loc.into_inner().into(),
Self::Hub { loc, .. } => loc.into_inner().into(),
Self::Scope { loc, .. } => loc.into_inner().into(), Self::Scope { loc, .. } => loc.into_inner().into(),
Self::Sftp { loc, .. } => loc.into_inner().into(), Self::Sftp { loc, .. } => loc.into_inner().into(),
} }
@ -156,6 +159,7 @@ impl UrlBuf {
Self::Regular(loc) => loc.try_set_name(name.as_os()?)?, Self::Regular(loc) => loc.try_set_name(name.as_os()?)?,
Self::Search { loc, .. } => loc.try_set_name(name.as_os()?)?, Self::Search { loc, .. } => loc.try_set_name(name.as_os()?)?,
Self::Mount { loc, .. } => loc.try_set_name(name.as_os()?)?, Self::Mount { loc, .. } => loc.try_set_name(name.as_os()?)?,
Self::Hub { loc, .. } => loc.try_set_name(name.as_os()?)?,
Self::Scope { loc, .. } => loc.try_set_name(name.encoded_bytes())?, Self::Scope { loc, .. } => loc.try_set_name(name.encoded_bytes())?,
Self::Sftp { loc, .. } => loc.try_set_name(name.encoded_bytes())?, Self::Sftp { loc, .. } => loc.try_set_name(name.encoded_bytes())?,
}) })
@ -166,6 +170,7 @@ impl UrlBuf {
Self::Regular(loc) => Self::Regular(loc.rebase(base)), Self::Regular(loc) => Self::Regular(loc.rebase(base)),
Self::Search { loc, auth } => Self::Search { loc: loc.rebase(base), auth: auth.clone() }, Self::Search { loc, auth } => Self::Search { loc: loc.rebase(base), auth: auth.clone() },
Self::Mount { loc, auth } => Self::Mount { loc: loc.rebase(base), auth: auth.clone() }, Self::Mount { loc, auth } => Self::Mount { loc: loc.rebase(base), auth: auth.clone() },
Self::Hub { .. } => todo!(),
Self::Scope { .. } => todo!(), Self::Scope { .. } => todo!(),
Self::Sftp { .. } => todo!(), Self::Sftp { .. } => todo!(),
} }
@ -188,6 +193,18 @@ impl UrlBuf {
auth: Auth::search(query.as_ref()), auth: Auth::search(query.as_ref()),
}) })
} }
pub fn into_domain<'a>(mut self, domain: impl Into<Domain<'a>>) -> Self {
match &mut self {
Self::Regular(_) => {}
Self::Search { auth, .. }
| Self::Mount { auth, .. }
| Self::Hub { auth, .. }
| Self::Scope { auth, .. }
| Self::Sftp { auth, .. } => Arc::make_mut(auth).domain = domain.into().into_owned(),
}
self
}
} }
impl Debug for UrlBuf { impl Debug for UrlBuf {
@ -265,10 +282,14 @@ impl<'de> IntoDeserializer<'de, de::value::Error> for &'de UrlBuf {
// --- Tests // --- Tests
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::fmt::Debug;
use anyhow::Result; use anyhow::Result;
use super::*; use super::*;
use crate::url::UrlLike; use crate::{path::PathKind, url::UrlLike};
fn debug(value: impl Debug) -> String { format!("{value:?}").replace(r"\", "/") }
#[test] #[test]
fn test_join() -> anyhow::Result<()> { fn test_join() -> anyhow::Result<()> {
@ -293,13 +314,7 @@ mod tests {
for (base, path, expected) in cases { for (base, path, expected) in cases {
let base: UrlBuf = base.parse()?; let base: UrlBuf = base.parse()?;
#[cfg(unix)] assert_eq!(debug(base.try_join(path)?), expected);
assert_eq!(format!("{:?}", base.try_join(path)?), expected);
#[cfg(windows)]
assert_eq!(
format!("{:?}", base.try_join(path)?).replace(r"\", "/"),
expected.replace(r"\", "/")
);
} }
Ok(()) Ok(())
@ -312,6 +327,8 @@ mod tests {
// Regular // Regular
("/a", Some("/")), ("/a", Some("/")),
("/", None), ("/", None),
("a", Some("")),
("", None),
// Search // Search
("search://kw:2:2//a/b/c", Some("search://kw:1:1//a/b")), ("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:1:1//a/b", Some("search://kw//a")),
@ -329,13 +346,16 @@ mod tests {
("sftp://vps//", None), ("sftp://vps//", None),
// Relative // Relative
("search://kw:2:2/a/b", Some("search://kw:1:1/a")), ("search://kw:2:2/a/b", Some("search://kw:1:1/a")),
("search://kw:1:1/a", None), ("search://kw:1:1/a", Some("search://kw/")),
("search://kw/", None), ("search://kw/", None),
("test-mount://7z:1:1/a", Some("test-mount://7z/")),
("test-scope://aws/a", Some("test-scope://aws/")),
("sftp://vps/a", Some("sftp://vps/")),
]; ];
for (path, expected) in cases { for (path, expected) in cases {
let path: UrlBuf = path.parse()?; let path: UrlBuf = path.parse()?;
assert_eq!(path.parent().map(|u| format!("{u:?}")).as_deref(), expected); assert_eq!(path.parent().map(debug).as_deref(), expected);
} }
Ok(()) Ok(())
@ -344,29 +364,166 @@ mod tests {
#[test] #[test]
fn test_into_search() -> Result<()> { fn test_into_search() -> Result<()> {
crate::init_tests(); crate::init_tests();
const S: char = std::path::MAIN_SEPARATOR;
let u: UrlBuf = "/root".parse()?; let u: UrlBuf = "/root".parse()?;
assert_eq!(format!("{u:?}"), "/root"); assert_eq!(debug(&u), "/root");
let u = u.into_search("kw")?; let u = u.into_search("kw")?;
assert_eq!(format!("{u:?}"), "search://kw//root"); assert_eq!(debug(&u), "search://kw//root");
assert_eq!(format!("{:?}", u.parent().unwrap()), "/"); assert_eq!(debug(u.parent().unwrap()), "/");
let u = u.try_join("examples")?; let u = u.try_join("examples")?;
assert_eq!(format!("{u:?}"), format!("search://kw:1:1//root{S}examples")); assert_eq!(debug(&u), "search://kw:1:1//root/examples");
let u = u.try_join("README.md")?; let u = u.try_join("README.md")?;
assert_eq!(format!("{u:?}"), format!("search://kw:2:2//root{S}examples{S}README.md")); assert_eq!(debug(&u), "search://kw:2:2//root/examples/README.md");
let u = u.parent().unwrap(); let u = u.parent().unwrap();
assert_eq!(format!("{u:?}"), format!("search://kw:1:1//root{S}examples")); assert_eq!(debug(u), "search://kw:1:1//root/examples");
let u = u.parent().unwrap(); let u = u.parent().unwrap();
assert_eq!(format!("{u:?}"), "search://kw//root"); assert_eq!(debug(u), "search://kw//root");
let u = u.parent().unwrap(); let u = u.parent().unwrap();
assert_eq!(format!("{u:?}"), "/"); assert_eq!(debug(u), "/");
Ok(())
}
#[test]
fn test_hub_parse() -> Result<()> {
crate::init_tests();
let root: UrlBuf = "test-hub://root/@/".parse()?;
assert_eq!(debug(&root), "test-hub://root/@/");
assert_eq!(root.loc().kind(), PathKind::Os);
let encoded: UrlBuf = "test-hub://%252C/@/".parse()?;
assert_eq!(debug(encoded), "test-hub://%252C/@/");
let encoded: UrlBuf = "test-hub://b1/@a%2Cb%40c%25d%2Fe,root/foo/bar".parse()?;
assert_eq!(debug(encoded), "test-hub://b1/@a%2Cb%40c%25d%2Fe,root/foo/bar");
Ok(())
}
#[test]
fn test_hub_domain() -> Result<()> {
crate::init_tests();
let root: UrlBuf = "test-hub://root/@/".parse()?;
let foo = root.try_join("foo")?.into_domain("a1");
assert_eq!(debug(&foo), "test-hub://a1/@root/foo");
let bar = foo.try_join("bar")?.into_domain("b1");
assert_eq!(bar.entry_key(), "b1");
assert_eq!(debug(&bar), "test-hub://b1/@a1,root/foo/bar");
assert_eq!(debug(bar.parent().unwrap()), "test-hub://a1/@root/foo");
assert_eq!(debug(bar.parent().unwrap().parent().unwrap()), "test-hub://root/@/");
let relative = UrlCow::try_from("test-hub://a1/@/@abc")?;
assert_eq!(debug(relative.as_url()), "test-hub://a1/@/@abc");
assert_eq!(debug(relative.parent().unwrap().as_url()), "test-hub:///@/");
assert_eq!(relative.entry_key(), "a1");
assert!(!relative.is_owned());
assert!(relative.parent().unwrap().entry_key().is_empty());
Ok(())
}
#[test]
fn test_hub_join() -> Result<()> {
crate::init_tests();
let root: UrlBuf = "test-hub://root/@/".parse()?;
let bar: UrlBuf = "test-hub://b1/@a1,root/foo/bar".parse()?;
assert_eq!(debug(bar.try_join(".")?), "test-hub://b1/@a1,root/foo/bar");
assert_eq!(debug(bar.try_join("..")?), "test-hub:///@b1,a1,root/foo/bar/..");
assert_eq!(debug(bar.try_join("/x/y")?), "test-hub:///@,//x/y");
assert_eq!(debug(root.try_join("../../..")?), "test-hub:///@,,root/../../..");
let absolute = root.try_join("/foo")?;
assert_eq!(debug(&absolute), "test-hub:///@//foo");
let absolute = absolute.into_domain("a1");
assert_eq!(debug(&absolute), "test-hub://a1/@//foo");
assert_eq!(debug(absolute.parent().unwrap().try_join("..")?), "test-hub:///@//..");
Ok(())
}
#[test]
fn test_hub_ports() -> Result<()> {
crate::init_tests();
let ports: UrlBuf = "test-hub://b1:2:1/@a1,root/foo/bar".parse()?;
assert_eq!(debug(ports.base()), "test-hub://root/@/");
assert_eq!(debug(ports.trail()), "test-hub://a1/@root/foo");
let ports: UrlBuf = "test-hub://b1:3:1/@a1,root//foo/bar".parse()?;
assert_eq!(debug(ports.base()), "test-hub://root/@/");
assert_eq!(debug(ports.trail()), "test-hub://a1/@root//foo");
let zeroed: UrlBuf = "test-hub://b1:0:0/@a1,root/foo/bar".parse()?;
assert_eq!(debug(zeroed.base()), "test-hub://b1/@a1,root/foo/bar");
assert_eq!(debug(zeroed.trail()), "test-hub://b1/@a1,root/foo/bar");
Ok(())
}
#[test]
fn test_hub_invalid() {
crate::init_tests();
assert!("test-hub://a1/foo".parse::<UrlBuf>().is_err());
assert!("test-hub://b1/@a1/foo/bar".parse::<UrlBuf>().is_err());
}
#[test]
fn test_hub_replace() -> Result<()> {
crate::init_tests();
let url: UrlBuf = "test-hub://b1/@a1,root/foo/bar".parse()?;
assert_eq!(
debug(url.try_replace(2, Path::new("baz/qux"))?.as_url()),
"test-hub://b1:2:2/@,a1,root/foo/baz/qux"
);
assert_eq!(debug(url.try_replace(1, Path::new("qux"))?.as_url()), "test-hub://b1/@root/qux");
Ok(())
}
#[cfg(windows)]
#[test]
fn test_hub_windows() -> Result<()> {
crate::init_tests();
let root: UrlBuf = "test-hub://root/@/".parse()?;
let c = root.try_join(r"C:\")?.into_domain("c-root");
assert_eq!(c.entry_key(), "c-root");
assert_eq!(c.auth().parent_depth(), 0);
let drive = c.try_join(r"Users\file.txt")?.into_domain("file");
assert_eq!(drive.loc(), Path::new(r"C:\Users\file.txt"));
assert_eq!(drive.auth().parent_depth(), 2);
let parent = drive.parent().unwrap();
assert_eq!(parent.loc(), Path::new(r"C:\Users"));
let parent = parent.parent().unwrap();
assert_eq!(parent.loc(), Path::new(r"C:\"));
assert_eq!(parent.entry_key(), "c-root");
assert!(parent.parent().is_none());
let relative = root.try_join(r"C:foo")?;
assert!(!relative.is_absolute());
assert_eq!(relative.parent().unwrap().loc(), Path::new("C:"));
assert!(relative.parent().unwrap().parent().is_none());
let unc = root.try_join(r"\\server\share\dir\file")?;
assert!(unc.is_absolute());
assert_eq!(unc.loc(), Path::new(r"\\server\share\dir\file"));
assert_eq!(unc.auth().parent_depth(), 2);
Ok(()) Ok(())
} }

View file

@ -1,11 +1,12 @@
use std::{borrow::Cow, ffi::{OsStr, OsString}, iter::FusedIterator, ops::Not}; use std::{borrow::Cow, ffi::{OsStr, OsString}, iter::FusedIterator, ops::Not};
use crate::{auth::Auth, loc::Loc, path, spec::{Encode as EncodeSpec, Spec}, strand::{StrandBuf, StrandCow}, url::{Component, Encode as EncodeUrl, Url}}; use crate::{auth::Auth, loc::Loc, path, spec::{EncodeSpec, Spec}, strand::{StrandBuf, StrandCow}, url::{Component, Encode as EncodeUrl, Url}};
#[derive(Clone)] #[derive(Clone)]
pub struct Components<'a> { pub struct Components<'a> {
inner: path::Components<'a>, inner: path::Components<'a>,
url: Url<'a>, url: Url<'a>,
auth_yields: usize,
back_yields: usize, back_yields: usize,
scheme_yielded: bool, scheme_yielded: bool,
} }
@ -15,6 +16,7 @@ impl<'a> From<Url<'a>> for Components<'a> {
Self { Self {
inner: value.loc().components(), inner: value.loc().components(),
url: value, url: value,
auth_yields: 0,
back_yields: 0, back_yields: 0,
scheme_yielded: false, scheme_yielded: false,
} }
@ -83,6 +85,10 @@ impl<'a> Components<'a> {
Url::Mount { auth, .. } => { Url::Mount { auth, .. } => {
Url::Mount { loc: Loc::with(path.as_os().unwrap(), uri, urn).unwrap(), auth } Url::Mount { loc: Loc::with(path.as_os().unwrap(), uri, urn).unwrap(), auth }
} }
Url::Hub { auth, .. } => Url::Hub {
loc: Loc::with(path.as_os().unwrap(), uri, urn).unwrap(),
auth: auth.parent_at(self.auth_yields),
},
Url::Scope { auth, .. } => { Url::Scope { auth, .. } => {
Url::Scope { loc: Loc::with(path.as_unix().unwrap(), uri, urn).unwrap(), auth } Url::Scope { loc: Loc::with(path.as_unix().unwrap(), uri, urn).unwrap(), auth }
} }
@ -116,6 +122,7 @@ impl<'a> Iterator for Components<'a> {
impl<'a> DoubleEndedIterator for Components<'a> { impl<'a> DoubleEndedIterator for Components<'a> {
fn next_back(&mut self) -> Option<Self::Item> { fn next_back(&mut self) -> Option<Self::Item> {
if let Some(c) = self.inner.next_back() { if let Some(c) = self.inner.next_back() {
self.auth_yields += c.has_auth() as usize;
self.back_yields += 1; self.back_yields += 1;
Some(c.into()) Some(c.into())
} else if !self.scheme_yielded { } else if !self.scheme_yielded {

View file

@ -1,16 +1,17 @@
use std::{borrow::Cow, hash::{Hash, Hasher}, path::PathBuf, sync::Arc}; use std::{borrow::Cow, hash::{Hash, Hasher}, path::PathBuf, sync::Arc};
use anyhow::Result; use anyhow::{Result, ensure};
use serde::{Deserialize, Deserializer, Serialize}; use serde::{Deserialize, Deserializer, Serialize};
use typed_path::{UnixPath, UnixPathBuf}; use typed_path::{UnixPath, UnixPathBuf};
use crate::{auth::{Auth, AuthKind}, loc::{Loc, LocBuf, LocCow}, path::{PathBufDyn, PathCow, PathDyn}, spec::Spec, url::{AsUrl, Url, UrlBuf}}; use crate::{auth::{Auth, AuthKind}, loc::{Loc, LocBuf, LocCow}, path::{DynPath, PathBufDyn, PathCow, PathDyn}, spec::Spec, url::{AsUrl, Url, UrlBuf}};
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum UrlCow<'a> { pub enum UrlCow<'a> {
Regular(LocCow<'a>), Regular(LocCow<'a>),
Search { loc: LocCow<'a>, auth: Arc<Auth> }, Search { loc: LocCow<'a>, auth: Arc<Auth> },
Mount { loc: LocCow<'a>, auth: Arc<Auth> }, Mount { loc: LocCow<'a>, auth: Arc<Auth> },
Hub { loc: LocCow<'a>, auth: Arc<Auth> },
Scope { loc: LocCow<'a, &'a UnixPath, UnixPathBuf>, auth: Arc<Auth> }, Scope { loc: LocCow<'a, &'a UnixPath, UnixPathBuf>, auth: Arc<Auth> },
Sftp { loc: LocCow<'a, &'a UnixPath, UnixPathBuf>, auth: Arc<Auth> }, Sftp { loc: LocCow<'a, &'a UnixPath, UnixPathBuf>, auth: Arc<Auth> },
} }
@ -21,6 +22,7 @@ impl<'a> From<Url<'a>> for UrlCow<'a> {
Url::Regular(loc) => Self::Regular(loc.into()), Url::Regular(loc) => Self::Regular(loc.into()),
Url::Search { loc, auth } => Self::Search { loc: loc.into(), auth: auth.clone() }, Url::Search { loc, auth } => Self::Search { loc: loc.into(), auth: auth.clone() },
Url::Mount { loc, auth } => Self::Mount { loc: loc.into(), auth: auth.clone() }, Url::Mount { loc, auth } => Self::Mount { loc: loc.into(), auth: auth.clone() },
Url::Hub { loc, auth } => Self::Hub { loc: loc.into(), auth: auth.clone() },
Url::Scope { loc, auth } => Self::Scope { loc: loc.into(), auth: auth.clone() }, Url::Scope { loc, auth } => Self::Scope { loc: loc.into(), auth: auth.clone() },
Url::Sftp { loc, auth } => Self::Sftp { loc: loc.into(), auth: auth.clone() }, Url::Sftp { loc, auth } => Self::Sftp { loc: loc.into(), auth: auth.clone() },
} }
@ -40,6 +42,7 @@ impl From<UrlBuf> for UrlCow<'_> {
UrlBuf::Regular(loc) => Self::Regular(loc.into()), UrlBuf::Regular(loc) => Self::Regular(loc.into()),
UrlBuf::Search { loc, auth } => Self::Search { loc: loc.into(), auth }, UrlBuf::Search { loc, auth } => Self::Search { loc: loc.into(), auth },
UrlBuf::Mount { loc, auth } => Self::Mount { loc: loc.into(), auth }, UrlBuf::Mount { loc, auth } => Self::Mount { loc: loc.into(), auth },
UrlBuf::Hub { loc, auth } => Self::Hub { loc: loc.into(), auth },
UrlBuf::Scope { loc, auth } => Self::Scope { loc: loc.into(), auth }, UrlBuf::Scope { loc, auth } => Self::Scope { loc: loc.into(), auth },
UrlBuf::Sftp { loc, auth } => Self::Sftp { loc: loc.into(), auth }, UrlBuf::Sftp { loc, auth } => Self::Sftp { loc: loc.into(), auth },
} }
@ -113,10 +116,13 @@ impl<'a> TryFrom<(Spec, PathDyn<'a>)> for UrlCow<'a> {
fn try_from((spec, path): (Spec, PathDyn<'a>)) -> Result<Self, Self::Error> { fn try_from((spec, path): (Spec, PathDyn<'a>)) -> Result<Self, Self::Error> {
let Spec { auth, uri, urn } = spec; let Spec { auth, uri, urn } = spec;
validate_auth_depth(&auth, path)?;
Ok(match auth.kind { Ok(match auth.kind {
AuthKind::Regular => Self::Regular(Loc::bare(path.as_os()?).into()), AuthKind::Regular => Self::Regular(Loc::bare(path.as_os()?).into()),
AuthKind::Search => Self::Search { loc: Loc::with(path.as_os()?, uri, urn)?.into(), auth }, AuthKind::Search => Self::Search { loc: Loc::with(path.as_os()?, uri, urn)?.into(), auth },
AuthKind::Mount => Self::Mount { loc: Loc::with(path.as_os()?, uri, urn)?.into(), auth }, AuthKind::Mount => Self::Mount { loc: Loc::with(path.as_os()?, uri, urn)?.into(), auth },
AuthKind::Hub => Self::Hub { loc: Loc::with(path.as_os()?, uri, urn)?.into(), auth },
AuthKind::Scope => Self::Scope { loc: Loc::with(path.as_unix()?, uri, urn)?.into(), auth }, AuthKind::Scope => Self::Scope { loc: Loc::with(path.as_unix()?, uri, urn)?.into(), auth },
AuthKind::Sftp => Self::Sftp { loc: Loc::with(path.as_unix()?, uri, urn)?.into(), auth }, AuthKind::Sftp => Self::Sftp { loc: Loc::with(path.as_unix()?, uri, urn)?.into(), auth },
}) })
@ -128,6 +134,8 @@ impl<'a> TryFrom<(Spec, PathBufDyn)> for UrlCow<'a> {
fn try_from((spec, path): (Spec, PathBufDyn)) -> Result<Self, Self::Error> { fn try_from((spec, path): (Spec, PathBufDyn)) -> Result<Self, Self::Error> {
let Spec { auth, uri, urn } = spec; let Spec { auth, uri, urn } = spec;
validate_auth_depth(&auth, path.dyn_path())?;
Ok(match auth.kind { Ok(match auth.kind {
AuthKind::Regular => { AuthKind::Regular => {
Self::Regular(LocBuf::<std::path::PathBuf>::from(path.into_os()?).into()) Self::Regular(LocBuf::<std::path::PathBuf>::from(path.into_os()?).into())
@ -140,6 +148,9 @@ impl<'a> TryFrom<(Spec, PathBufDyn)> for UrlCow<'a> {
loc: LocBuf::<std::path::PathBuf>::with(path.try_into()?, uri, urn)?.into(), loc: LocBuf::<std::path::PathBuf>::with(path.try_into()?, uri, urn)?.into(),
auth, auth,
}, },
AuthKind::Hub => {
Self::Hub { loc: LocBuf::<PathBuf>::with(path.try_into()?, uri, urn)?.into(), auth }
}
AuthKind::Scope => { AuthKind::Scope => {
Self::Scope { loc: LocBuf::<UnixPathBuf>::with(path.try_into()?, uri, urn)?.into(), auth } Self::Scope { loc: LocBuf::<UnixPathBuf>::with(path.try_into()?, uri, urn)?.into(), auth }
} }
@ -172,6 +183,7 @@ impl<'a> UrlCow<'a> {
Self::Regular(loc) => loc.is_owned(), Self::Regular(loc) => loc.is_owned(),
Self::Search { loc, .. } => loc.is_owned(), Self::Search { loc, .. } => loc.is_owned(),
Self::Mount { loc, .. } => loc.is_owned(), Self::Mount { loc, .. } => loc.is_owned(),
Self::Hub { loc, .. } => loc.is_owned(),
Self::Scope { loc, .. } => loc.is_owned(), Self::Scope { loc, .. } => loc.is_owned(),
Self::Sftp { loc, .. } => loc.is_owned(), Self::Sftp { loc, .. } => loc.is_owned(),
} }
@ -182,6 +194,7 @@ impl<'a> UrlCow<'a> {
Self::Regular(loc) => UrlBuf::Regular(loc.into_owned()), Self::Regular(loc) => UrlBuf::Regular(loc.into_owned()),
Self::Search { loc, auth } => UrlBuf::Search { loc: loc.into_owned(), auth }, Self::Search { loc, auth } => UrlBuf::Search { loc: loc.into_owned(), auth },
Self::Mount { loc, auth } => UrlBuf::Mount { loc: loc.into_owned(), auth }, Self::Mount { loc, auth } => UrlBuf::Mount { loc: loc.into_owned(), auth },
Self::Hub { loc, auth } => UrlBuf::Hub { loc: loc.into_owned(), auth },
Self::Scope { loc, auth } => UrlBuf::Scope { loc: loc.into_owned(), auth }, Self::Scope { loc, auth } => UrlBuf::Scope { loc: loc.into_owned(), auth },
Self::Sftp { loc, auth } => UrlBuf::Sftp { loc: loc.into_owned(), auth }, Self::Sftp { loc, auth } => UrlBuf::Sftp { loc: loc.into_owned(), auth },
} }
@ -191,7 +204,9 @@ impl<'a> UrlCow<'a> {
let (uri, urn) = Spec::retrieve_ports(self.as_url()); let (uri, urn) = Spec::retrieve_ports(self.as_url());
let (auth, path) = match self { let (auth, path) = match self {
Self::Regular(loc) => (Auth::default_arc(), loc.into_path()), Self::Regular(loc) => (Auth::default_arc(), loc.into_path()),
Self::Search { loc, auth } | Self::Mount { loc, auth } => (auth, loc.into_path()), Self::Search { loc, auth } | Self::Mount { loc, auth } | Self::Hub { loc, auth } => {
(auth, loc.into_path())
}
Self::Scope { loc, auth } | Self::Sftp { loc, auth } => (auth, loc.into_path()), Self::Scope { loc, auth } | Self::Sftp { loc, auth } => (auth, loc.into_path()),
}; };
(Spec { auth, uri, urn }, path) (Spec { auth, uri, urn }, path)
@ -222,6 +237,16 @@ impl<'de> Deserialize<'de> for UrlCow<'_> {
} }
} }
fn validate_auth_depth(auth: &Auth, path: PathDyn) -> Result<()> {
if auth.kind == AuthKind::Hub {
ensure!(
auth.parent_depth() == path.components().auth_depth(),
"Hub URL parent depth does not match its path"
);
}
Ok(())
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View file

@ -2,7 +2,7 @@ use std::borrow::Cow;
use serde::{Deserializer, de::{self, IntoDeserializer, MapAccess}}; use serde::{Deserializer, de::{self, IntoDeserializer, MapAccess}};
use crate::{data::BytesDeserializer, pool::{InternStr, Symbol}, url::UrlCow}; use crate::{auth::Domain, data::BytesDeserializer, pool::{InternStr, Symbol}, url::UrlCow};
pub struct UrlDeserializer<'a>(pub(super) UrlCow<'a>); pub struct UrlDeserializer<'a>(pub(super) UrlCow<'a>);
@ -33,7 +33,7 @@ impl<'de, 'a: 'de> Deserializer<'de> for UrlDeserializer<'a> {
struct MapDeserializer<'a> { struct MapDeserializer<'a> {
kind: Option<&'static str>, kind: Option<&'static str>,
scheme: Option<Symbol<str>>, scheme: Option<Symbol<str>>,
domain: Option<Symbol<str>>, domain: Option<Domain<'static>>,
uri: Option<usize>, uri: Option<usize>,
urn: Option<usize>, urn: Option<usize>,
path: Option<Cow<'a, [u8]>>, path: Option<Cow<'a, [u8]>>,
@ -47,7 +47,7 @@ impl<'a> MapDeserializer<'a> {
Self { Self {
kind: Some(spec.kind.into()), kind: Some(spec.kind.into()),
scheme: Some(spec.scheme.intern()), scheme: Some(spec.scheme.intern()),
domain: Some(spec.domain.intern()), domain: Some(spec.domain.clone()),
uri: Some(uri), uri: Some(uri),
urn: Some(urn), urn: Some(urn),
path: Some(path.into_encoded_bytes()), path: Some(path.into_encoded_bytes()),

View file

@ -1,4 +1,4 @@
use crate::{spec::Encode, url::Url}; use crate::{spec::EncodeSpec, url::Url};
pub struct Display<'a>(pub Url<'a>); pub struct Display<'a>(pub Url<'a>);
@ -6,7 +6,7 @@ impl std::fmt::Display for Display<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let (kind, loc) = (self.0.kind(), self.0.loc()); let (kind, loc) = (self.0.kind(), self.0.loc());
if kind.is_virtual() { if kind.is_virtual() {
Encode(self.0).fmt(f)?; EncodeSpec(self.0).fmt(f)?;
} }
loc.display().fmt(f) loc.display().fmt(f)
} }

View file

@ -1,24 +1,36 @@
use std::fmt::{self, Display}; use std::fmt::{self, Display};
use percent_encoding::{CONTROLS, percent_encode}; use percent_encoding::{AsciiSet, CONTROLS, PercentEncode, percent_encode};
use crate::{auth::Encode as EncodeAuth, url::Url}; use crate::{auth::{EncodeAuth, EncodePrefix}, spec::EncodeSpec, url::Url};
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub struct Encode<'a>(pub Url<'a>); pub struct Encode<'a>(pub Url<'a>);
impl Encode<'_> {
pub fn loc(b: &[u8]) -> PercentEncode<'_> {
const SET: &AsciiSet = &CONTROLS.add(b'%');
percent_encode(b, SET)
}
}
impl Display for Encode<'_> { impl Display for Encode<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use crate::spec::Encode as E; let loc = Self::loc(self.0.loc().encoded_bytes());
let loc = percent_encode(self.0.loc().encoded_bytes(), CONTROLS);
match self.0 { match self.0 {
Url::Regular(_) => write!(f, "regular~://{loc}"), Url::Regular(_) => write!(f, "regular~://{loc}"),
Url::Search { auth, .. } Url::Search { auth, .. }
| Url::Mount { auth, .. } | Url::Mount { auth, .. }
| Url::Hub { auth, .. }
| Url::Scope { auth, .. } | Url::Scope { auth, .. }
| Url::Sftp { auth, .. } => { | Url::Sftp { auth, .. } => {
write!(f, "{}{}/{loc}", EncodeAuth(auth, true), E::ports((*self).into())) write!(
f,
"{}{}{}{loc}",
EncodeAuth(auth, true),
EncodeSpec::ports((*self).into()),
EncodePrefix(auth)
)
} }
} }
} }

View file

@ -2,7 +2,7 @@ use std::{borrow::Cow, ffi::OsStr, path::Path};
use anyhow::Result; use anyhow::Result;
use crate::{auth::{Auth, AuthKind}, path::{AsPathRef, EndsWithError, JoinError, PathDyn, StartsWithError, StripPrefixError}, spec::Spec, strand::{AsStrand, Strand}, url::{AsUrl, Components, Display, Url, UrlBuf, UrlCow}}; use crate::{auth::{Auth, AuthKind}, path::{DynPathRef, EndsWithError, JoinError, PathDyn, StartsWithError, StripPrefixError}, spec::Spec, strand::{AsStrand, Strand}, url::{AsUrl, Components, Display, Url, UrlBuf, UrlCow}};
pub trait UrlLike pub trait UrlLike
where where
@ -22,6 +22,8 @@ where
fn ext(&self) -> Option<Strand<'_>> { self.as_url().ext() } fn ext(&self) -> Option<Strand<'_>> { self.as_url().ext() }
fn entry_key(&self) -> PathDyn<'_> { self.as_url().entry_key() }
fn has_base(&self) -> bool { self.as_url().has_base() } fn has_base(&self) -> bool { self.as_url().has_base() }
fn has_root(&self) -> bool { self.as_url().has_root() } fn has_root(&self) -> bool { self.as_url().has_root() }
@ -44,6 +46,8 @@ where
fn pair(&self) -> Option<(Url<'_>, PathDyn<'_>)> { self.as_url().pair() } fn pair(&self) -> Option<(Url<'_>, PathDyn<'_>)> { self.as_url().pair() }
fn pair2(&self) -> Option<(Url<'_>, PathDyn<'_>)> { self.as_url().pair2() }
fn parent(&self) -> Option<Url<'_>> { self.as_url().parent() } fn parent(&self) -> Option<Url<'_>> { self.as_url().parent() }
fn spec(&self) -> Spec { self.as_url().spec() } fn spec(&self) -> Spec { self.as_url().spec() }
@ -62,8 +66,8 @@ where
self.as_url().try_join(path) self.as_url().try_join(path)
} }
fn try_replace<'a>(&self, take: usize, path: impl AsPathRef<'a>) -> Result<UrlCow<'a>> { fn try_replace<'a>(&self, take: usize, to: impl DynPathRef<'a>) -> Result<UrlCow<'a>> {
self.as_url().try_replace(take, path) self.as_url().try_replace(take, to)
} }
fn try_starts_with(&self, base: impl AsUrl) -> Result<bool, StartsWithError> { fn try_starts_with(&self, base: impl AsUrl) -> Result<bool, StartsWithError> {

View file

@ -121,6 +121,9 @@ impl UserData for UrlBuf {
methods.add_method_once("into_search", |_, me, domain: LuaString| { methods.add_method_once("into_search", |_, me, domain: LuaString| {
me.into_search(domain.to_str()?).into_lua_err() me.into_search(domain.to_str()?).into_lua_err()
}); });
methods.add_method_once("into_domain", |_, me, domain: LuaString| {
Ok(me.into_domain(domain.to_str()?.to_owned()))
});
methods.add_meta_method(MetaMethod::Eq, |_, me, other: UrlRef| Ok(*me == *other)); methods.add_meta_method(MetaMethod::Eq, |_, me, other: UrlRef| Ok(*me == *other));
methods.add_meta_method(MetaMethod::ToString, |lua, me, ()| { methods.add_meta_method(MetaMethod::ToString, |lua, me, ()| {

View file

@ -42,6 +42,7 @@ impl AsUrl for UrlBuf {
Self::Regular(loc) => Url::Regular(loc.as_loc()), Self::Regular(loc) => Url::Regular(loc.as_loc()),
Self::Search { loc, auth } => Url::Search { loc: loc.as_loc(), auth }, Self::Search { loc, auth } => Url::Search { loc: loc.as_loc(), auth },
Self::Mount { loc, auth } => Url::Mount { loc: loc.as_loc(), auth }, Self::Mount { loc, auth } => Url::Mount { loc: loc.as_loc(), auth },
Self::Hub { loc, auth } => Url::Hub { loc: loc.as_loc(), auth },
Self::Scope { loc, auth } => Url::Scope { loc: loc.as_loc(), auth }, Self::Scope { loc, auth } => Url::Scope { loc: loc.as_loc(), auth },
Self::Sftp { loc, auth } => Url::Sftp { loc: loc.as_loc(), auth }, Self::Sftp { loc, auth } => Url::Sftp { loc: loc.as_loc(), auth },
} }
@ -64,6 +65,7 @@ impl AsUrl for UrlCow<'_> {
Self::Regular(loc) => Url::Regular(loc.as_loc()), Self::Regular(loc) => Url::Regular(loc.as_loc()),
Self::Search { loc, auth } => Url::Search { loc: loc.as_loc(), auth }, Self::Search { loc, auth } => Url::Search { loc: loc.as_loc(), auth },
Self::Mount { loc, auth } => Url::Mount { loc: loc.as_loc(), auth }, Self::Mount { loc, auth } => Url::Mount { loc: loc.as_loc(), auth },
Self::Hub { loc, auth } => Url::Hub { loc: loc.as_loc(), auth },
Self::Scope { loc, auth } => Url::Scope { loc: loc.as_loc(), auth }, Self::Scope { loc, auth } => Url::Scope { loc: loc.as_loc(), auth },
Self::Sftp { loc, auth } => Url::Sftp { loc: loc.as_loc(), auth }, Self::Sftp { loc, auth } => Url::Sftp { loc: loc.as_loc(), auth },
} }

View file

@ -5,13 +5,14 @@ use hashbrown::Equivalent;
use serde::Serialize; use serde::Serialize;
use super::Encode as EncodeUrl; use super::Encode as EncodeUrl;
use crate::{auth::{Auth, AuthKind}, loc::{Loc, LocBuf}, path::{AsPath, AsPathRef, EndsWithError, JoinError, PathBufDyn, PathDyn, PathDynError, PathLike, StartsWithError, StripPrefixError, StripSuffixError}, spec::{Encode as EncodeSpec, ParsedSpec, Spec}, strand::{AsStrand, Strand}, url::{AsUrl, Components, UrlBuf, UrlCow}}; use crate::{auth::{Auth, AuthKind}, loc::{Loc, LocBuf}, path::{DynPath, DynPathRef, EndsWithError, JoinError, PathBufDyn, PathDyn, PathDynError, PathLike, StartsWithError, StripPrefixError, StripSuffixError}, spec::{EncodeSpec, ParsedSpec, Spec}, strand::{AsStrand, Strand}, url::{AsUrl, Components, UrlBuf, UrlCow}};
#[derive(Clone, Copy, Eq, Hash, PartialEq)] #[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub enum Url<'a> { pub enum Url<'a> {
Regular(Loc<'a>), Regular(Loc<'a>),
Search { loc: Loc<'a>, auth: &'a Arc<Auth> }, Search { loc: Loc<'a>, auth: &'a Arc<Auth> },
Mount { loc: Loc<'a>, auth: &'a Arc<Auth> }, Mount { loc: Loc<'a>, auth: &'a Arc<Auth> },
Hub { loc: Loc<'a>, auth: &'a Arc<Auth> },
Scope { loc: Loc<'a, &'a typed_path::UnixPath>, auth: &'a Arc<Auth> }, Scope { loc: Loc<'a, &'a typed_path::UnixPath>, auth: &'a Arc<Auth> },
Sftp { loc: Loc<'a, &'a typed_path::UnixPath>, auth: &'a Arc<Auth> }, Sftp { loc: Loc<'a, &'a typed_path::UnixPath>, auth: &'a Arc<Auth> },
} }
@ -60,11 +61,22 @@ impl<'a> Url<'a> {
Self::Regular(_) => &Auth::DEFAULT, Self::Regular(_) => &Auth::DEFAULT,
Self::Search { auth, .. } Self::Search { auth, .. }
| Self::Mount { auth, .. } | Self::Mount { auth, .. }
| Self::Hub { auth, .. }
| Self::Scope { auth, .. } | Self::Scope { auth, .. }
| Self::Sftp { auth, .. } => auth, | Self::Sftp { auth, .. } => auth,
} }
} }
fn auth_at(self, base: Self) -> Option<&'a Auth> {
let (Self::Hub { auth, .. }, Self::Hub { .. }) = (self, base) else {
return self.auth().covariant(base.auth()).then_some(self.auth());
};
let depth =
self.loc().components().auth_depth().checked_sub(base.loc().components().auth_depth())?;
Some(auth.parent_at(depth))
}
#[inline] #[inline]
pub fn as_regular(self) -> Result<Self, PathDynError> { pub fn as_regular(self) -> Result<Self, PathDynError> {
Ok(Self::Regular(Loc::bare(self.loc().as_os()?))) Ok(Self::Regular(Loc::bare(self.loc().as_os()?)))
@ -75,6 +87,10 @@ impl<'a> Url<'a> {
Self::Regular(loc) => Self::Regular(Loc::bare(loc.base())), Self::Regular(loc) => Self::Regular(Loc::bare(loc.base())),
Self::Search { loc, auth } => Self::Search { loc: Loc::zeroed(loc.base()), auth }, Self::Search { loc, auth } => Self::Search { loc: Loc::zeroed(loc.base()), auth },
Self::Mount { loc, auth } => Self::Mount { loc: Loc::zeroed(loc.base()), auth }, Self::Mount { loc, auth } => Self::Mount { loc: Loc::zeroed(loc.base()), auth },
Self::Hub { loc, auth } => Self::Hub {
loc: Loc::bare(loc.base()),
auth: auth.parent_at(loc.uri().dyn_path().components().auth_depth()),
},
Self::Scope { loc, auth } => Self::Scope { loc: Loc::bare(loc.base()), auth }, Self::Scope { loc, auth } => Self::Scope { loc: Loc::bare(loc.base()), auth },
Self::Sftp { loc, auth } => Self::Sftp { loc: Loc::bare(loc.base()), auth }, Self::Sftp { loc, auth } => Self::Sftp { loc: Loc::bare(loc.base()), auth },
} }
@ -95,6 +111,7 @@ impl<'a> Url<'a> {
Self::Regular(loc) => loc.extension()?.as_strand(), Self::Regular(loc) => loc.extension()?.as_strand(),
Self::Search { loc, .. } => loc.extension()?.as_strand(), Self::Search { loc, .. } => loc.extension()?.as_strand(),
Self::Mount { loc, .. } => loc.extension()?.as_strand(), Self::Mount { loc, .. } => loc.extension()?.as_strand(),
Self::Hub { loc, .. } => loc.extension()?.as_strand(),
Self::Scope { loc, .. } => loc.extension()?.as_strand(), Self::Scope { loc, .. } => loc.extension()?.as_strand(),
Self::Sftp { loc, .. } => loc.extension()?.as_strand(), Self::Sftp { loc, .. } => loc.extension()?.as_strand(),
}) })
@ -106,6 +123,7 @@ impl<'a> Url<'a> {
Self::Regular(loc) => loc.has_base(), Self::Regular(loc) => loc.has_base(),
Self::Search { loc, .. } => loc.has_base(), Self::Search { loc, .. } => loc.has_base(),
Self::Mount { loc, .. } => loc.has_base(), Self::Mount { loc, .. } => loc.has_base(),
Self::Hub { loc, .. } => loc.has_base(),
Self::Scope { loc, .. } => loc.has_base(), Self::Scope { loc, .. } => loc.has_base(),
Self::Sftp { loc, .. } => loc.has_base(), Self::Sftp { loc, .. } => loc.has_base(),
} }
@ -120,6 +138,7 @@ impl<'a> Url<'a> {
Self::Regular(loc) => loc.has_trail(), Self::Regular(loc) => loc.has_trail(),
Self::Search { loc, .. } => loc.has_trail(), Self::Search { loc, .. } => loc.has_trail(),
Self::Mount { loc, .. } => loc.has_trail(), Self::Mount { loc, .. } => loc.has_trail(),
Self::Hub { loc, .. } => loc.has_trail(),
Self::Scope { loc, .. } => loc.has_trail(), Self::Scope { loc, .. } => loc.has_trail(),
Self::Sftp { loc, .. } => loc.has_trail(), Self::Sftp { loc, .. } => loc.has_trail(),
} }
@ -140,11 +159,12 @@ impl<'a> Url<'a> {
#[inline] #[inline]
pub fn loc(self) -> PathDyn<'a> { pub fn loc(self) -> PathDyn<'a> {
match self { match self {
Self::Regular(loc) => loc.as_path(), Self::Regular(loc) => loc.dyn_path(),
Self::Search { loc, .. } => loc.as_path(), Self::Search { loc, .. } => loc.dyn_path(),
Self::Mount { loc, .. } => loc.as_path(), Self::Mount { loc, .. } => loc.dyn_path(),
Self::Scope { loc, .. } => loc.as_path(), Self::Hub { loc, .. } => loc.dyn_path(),
Self::Sftp { loc, .. } => loc.as_path(), Self::Scope { loc, .. } => loc.dyn_path(),
Self::Sftp { loc, .. } => loc.dyn_path(),
} }
} }
@ -154,6 +174,7 @@ impl<'a> Url<'a> {
Self::Regular(loc) => loc.file_name()?.as_strand(), Self::Regular(loc) => loc.file_name()?.as_strand(),
Self::Search { loc, .. } => loc.file_name()?.as_strand(), Self::Search { loc, .. } => loc.file_name()?.as_strand(),
Self::Mount { loc, .. } => loc.file_name()?.as_strand(), Self::Mount { loc, .. } => loc.file_name()?.as_strand(),
Self::Hub { loc, .. } => loc.file_name()?.as_strand(),
Self::Scope { loc, .. } => loc.file_name()?.as_strand(), Self::Scope { loc, .. } => loc.file_name()?.as_strand(),
Self::Sftp { loc, .. } => loc.file_name()?.as_strand(), Self::Sftp { loc, .. } => loc.file_name()?.as_strand(),
}) })
@ -165,6 +186,19 @@ impl<'a> Url<'a> {
#[inline] #[inline]
pub fn pair(self) -> Option<(Self, PathDyn<'a>)> { Some((self.parent()?, self.urn())) } pub fn pair(self) -> Option<(Self, PathDyn<'a>)> { Some((self.parent()?, self.urn())) }
#[inline]
pub fn pair2(self) -> Option<(Self, PathDyn<'a>)> {
Some((self.parent()?, Some(self.entry_key()).filter(|k| !k.is_empty())?))
}
#[inline]
pub fn entry_key(self) -> PathDyn<'a> {
match self {
Self::Hub { auth, .. } => PathDyn::Unix(typed_path::UnixPath::new(auth.domain.as_bytes())),
_ => self.urn(),
}
}
pub fn parent(self) -> Option<Self> { pub fn parent(self) -> Option<Self> {
let uri = self.uri(); let uri = self.uri();
@ -187,6 +221,11 @@ impl<'a> Url<'a> {
Self::Mount { loc: Loc::floated(loc.parent()?, loc.base()), auth } Self::Mount { loc: Loc::floated(loc.parent()?, loc.base()), auth }
} }
// Hub
Self::Hub { loc, auth } => {
Self::Hub { loc: Loc::bare(loc.parent()?), auth: auth.parent.as_ref()? }
}
// Scope // Scope
Self::Scope { loc, auth } => Self::Scope { loc: Loc::bare(loc.parent()?), auth }, Self::Scope { loc, auth } => Self::Scope { loc: Loc::bare(loc.parent()?), auth },
@ -205,6 +244,7 @@ impl<'a> Url<'a> {
Self::Regular(_) => Auth::default_arc(), Self::Regular(_) => Auth::default_arc(),
Self::Search { auth, .. } Self::Search { auth, .. }
| Self::Mount { auth, .. } | Self::Mount { auth, .. }
| Self::Hub { auth, .. }
| Self::Scope { auth, .. } | Self::Scope { auth, .. }
| Self::Sftp { auth, .. } => auth.clone(), | Self::Sftp { auth, .. } => auth.clone(),
}; };
@ -219,6 +259,7 @@ impl<'a> Url<'a> {
Self::Regular(loc) => loc.file_stem()?.as_strand(), Self::Regular(loc) => loc.file_stem()?.as_strand(),
Self::Search { loc, .. } => loc.file_stem()?.as_strand(), Self::Search { loc, .. } => loc.file_stem()?.as_strand(),
Self::Mount { loc, .. } => loc.file_stem()?.as_strand(), Self::Mount { loc, .. } => loc.file_stem()?.as_strand(),
Self::Hub { loc, .. } => loc.file_stem()?.as_strand(),
Self::Scope { loc, .. } => loc.file_stem()?.as_strand(), Self::Scope { loc, .. } => loc.file_stem()?.as_strand(),
Self::Sftp { loc, .. } => loc.file_stem()?.as_strand(), Self::Sftp { loc, .. } => loc.file_stem()?.as_strand(),
}) })
@ -253,6 +294,11 @@ impl<'a> Url<'a> {
Self::Mount { loc: Loc::new(loc.trail(), loc.base(), loc.base()), auth } Self::Mount { loc: Loc::new(loc.trail(), loc.base(), loc.base()), auth }
} }
Self::Hub { loc, auth } => Self::Hub {
loc: Loc::bare(loc.trail()),
auth: auth.parent_at(loc.urn().dyn_path().components().auth_depth()),
},
Self::Scope { loc, auth } => Self::Scope { loc: Loc::bare(loc.trail()), auth }, Self::Scope { loc, auth } => Self::Scope { loc: Loc::bare(loc.trail()), auth },
Self::Sftp { loc, auth } => Self::Sftp { loc: Loc::bare(loc.trail()), auth }, Self::Sftp { loc, auth } => Self::Sftp { loc: Loc::bare(loc.trail()), auth },
@ -261,13 +307,16 @@ impl<'a> Url<'a> {
pub fn triple(self) -> (PathDyn<'a>, PathDyn<'a>, PathDyn<'a>) { pub fn triple(self) -> (PathDyn<'a>, PathDyn<'a>, PathDyn<'a>) {
match self { match self {
Self::Regular(loc) | Self::Search { loc, .. } | Self::Mount { loc, .. } => { Self::Regular(loc)
| Self::Search { loc, .. }
| Self::Mount { loc, .. }
| Self::Hub { loc, .. } => {
let (base, rest, urn) = loc.triple(); let (base, rest, urn) = loc.triple();
(base.as_path(), rest.as_path(), urn.as_path()) (base.dyn_path(), rest.dyn_path(), urn.dyn_path())
} }
Self::Scope { loc, .. } | Self::Sftp { loc, .. } => { Self::Scope { loc, .. } | Self::Sftp { loc, .. } => {
let (base, rest, urn) = loc.triple(); let (base, rest, urn) = loc.triple();
(base.as_path(), rest.as_path(), urn.as_path()) (base.dyn_path(), rest.dyn_path(), urn.dyn_path())
} }
} }
} }
@ -279,6 +328,7 @@ impl<'a> Url<'a> {
} }
pub fn try_join(self, path: impl AsStrand) -> Result<UrlBuf, JoinError> { pub fn try_join(self, path: impl AsStrand) -> Result<UrlBuf, JoinError> {
let path = path.as_strand();
let joined = self.loc().try_join(path)?; let joined = self.loc().try_join(path)?;
Ok(match self { Ok(match self {
@ -300,6 +350,11 @@ impl<'a> Url<'a> {
UrlBuf::Mount { loc: LocBuf::<PathBuf>::zeroed(joined.into_os()?), auth: auth.clone() } UrlBuf::Mount { loc: LocBuf::<PathBuf>::zeroed(joined.into_os()?), auth: auth.clone() }
} }
Self::Hub { auth, .. } => UrlBuf::Hub {
loc: joined.into_os()?.into(),
auth: auth.clone().descend(path.as_os_path()?),
},
Self::Scope { auth, .. } => { Self::Scope { auth, .. } => {
UrlBuf::Scope { loc: joined.into_unix()?.into(), auth: auth.clone() } UrlBuf::Scope { loc: joined.into_unix()?.into(), auth: auth.clone() }
} }
@ -310,8 +365,8 @@ impl<'a> Url<'a> {
}) })
} }
pub fn try_replace<'b>(self, take: usize, to: impl AsPathRef<'b>) -> Result<UrlCow<'b>> { pub fn try_replace<'b>(self, take: usize, to: impl DynPathRef<'b>) -> Result<UrlCow<'b>> {
self.try_replace_impl(take, to.as_path_ref()) self.try_replace_impl(take, to.dyn_path_ref())
} }
fn try_replace_impl<'b>(self, take: usize, rep: PathDyn<'b>) -> Result<UrlCow<'b>> { fn try_replace_impl<'b>(self, take: usize, rep: PathDyn<'b>) -> Result<UrlCow<'b>> {
@ -337,6 +392,10 @@ impl<'a> Url<'a> {
loc: LocBuf::<std::path::PathBuf>::new(path.into_os()?, loc.base(), loc.trail()), loc: LocBuf::<std::path::PathBuf>::new(path.into_os()?, loc.base(), loc.trail()),
auth: auth.clone(), auth: auth.clone(),
}, },
Self::Hub { loc, auth } if path.try_starts_with(loc.trail())? => UrlBuf::Hub {
auth: auth.clone().with_parent_depth(path.components().auth_depth()),
loc: LocBuf::<PathBuf>::new(path.into_os()?, loc.base(), loc.trail()),
},
Self::Scope { loc, auth } if path.try_starts_with(loc.trail())? => UrlBuf::Scope { Self::Scope { loc, auth } if path.try_starts_with(loc.trail())? => UrlBuf::Scope {
loc: LocBuf::<typed_path::UnixPathBuf>::new(path.into_unix()?, loc.base(), loc.trail()), loc: LocBuf::<typed_path::UnixPathBuf>::new(path.into_unix()?, loc.base(), loc.trail()),
auth: auth.clone(), auth: auth.clone(),
@ -354,6 +413,10 @@ impl<'a> Url<'a> {
loc: LocBuf::<std::path::PathBuf>::saturated(path.into_os()?, self.kind()), loc: LocBuf::<std::path::PathBuf>::saturated(path.into_os()?, self.kind()),
auth: auth.clone(), auth: auth.clone(),
}, },
Self::Hub { auth, .. } => UrlBuf::Hub {
auth: auth.clone().with_parent_depth(path.components().auth_depth()),
loc: LocBuf::<PathBuf>::saturated(path.into_os()?, self.kind()),
},
Self::Scope { auth, .. } => UrlBuf::Scope { Self::Scope { auth, .. } => UrlBuf::Scope {
loc: LocBuf::<typed_path::UnixPathBuf>::saturated(path.into_unix()?, self.kind()), loc: LocBuf::<typed_path::UnixPathBuf>::saturated(path.into_unix()?, self.kind()),
auth: auth.clone(), auth: auth.clone(),
@ -367,10 +430,12 @@ impl<'a> Url<'a> {
Ok(url.into()) Ok(url.into())
} }
#[inline]
pub fn try_starts_with(self, base: impl AsUrl) -> Result<bool, StartsWithError> { pub fn try_starts_with(self, base: impl AsUrl) -> Result<bool, StartsWithError> {
let base = base.as_url(); let base = base.as_url();
Ok(self.loc().try_starts_with(base.loc())? && self.auth().covariant(base.auth())) Ok(
self.loc().try_starts_with(base.loc())?
&& self.auth_at(base).is_some_and(|a| a == base.auth()),
)
} }
pub fn try_strip_prefix(self, base: impl AsUrl) -> Result<PathDyn<'a>, StripPrefixError> { pub fn try_strip_prefix(self, base: impl AsUrl) -> Result<PathDyn<'a>, StripPrefixError> {
@ -379,7 +444,7 @@ impl<'a> Url<'a> {
let base = base.as_url(); let base = base.as_url();
let prefix = self.loc().try_strip_prefix(base.loc())?; let prefix = self.loc().try_strip_prefix(base.loc())?;
if self.auth().covariant(base.auth()) { if self.auth_at(base).is_some_and(|a| a == base.auth()) {
return Ok(prefix); return Ok(prefix);
} }
@ -420,22 +485,24 @@ impl<'a> Url<'a> {
#[inline] #[inline]
pub fn uri(self) -> PathDyn<'a> { pub fn uri(self) -> PathDyn<'a> {
match self { match self {
Self::Regular(loc) => loc.uri().as_path(), Self::Regular(loc) => loc.uri().dyn_path(),
Self::Search { loc, .. } => loc.uri().as_path(), Self::Search { loc, .. } => loc.uri().dyn_path(),
Self::Mount { loc, .. } => loc.uri().as_path(), Self::Mount { loc, .. } => loc.uri().dyn_path(),
Self::Scope { loc, .. } => loc.uri().as_path(), Self::Hub { loc, .. } => loc.uri().dyn_path(),
Self::Sftp { loc, .. } => loc.uri().as_path(), Self::Scope { loc, .. } => loc.uri().dyn_path(),
Self::Sftp { loc, .. } => loc.uri().dyn_path(),
} }
} }
#[inline] #[inline]
pub fn urn(self) -> PathDyn<'a> { pub fn urn(self) -> PathDyn<'a> {
match self { match self {
Self::Regular(loc) => loc.urn().as_path(), Self::Regular(loc) => loc.urn().dyn_path(),
Self::Search { loc, .. } => loc.urn().as_path(), Self::Search { loc, .. } => loc.urn().dyn_path(),
Self::Mount { loc, .. } => loc.urn().as_path(), Self::Mount { loc, .. } => loc.urn().dyn_path(),
Self::Scope { loc, .. } => loc.urn().as_path(), Self::Hub { loc, .. } => loc.urn().dyn_path(),
Self::Sftp { loc, .. } => loc.urn().as_path(), Self::Scope { loc, .. } => loc.urn().dyn_path(),
Self::Sftp { loc, .. } => loc.urn().dyn_path(),
} }
} }
} }

View file

@ -40,7 +40,7 @@ impl FileBuilder for Demand {
AuthKind::Regular | AuthKind::Search => { AuthKind::Regular | AuthKind::Search => {
self.0.build::<yazi_fs::engine::local::Demand>().open(url).await?.into() self.0.build::<yazi_fs::engine::local::Demand>().open(url).await?.into()
} }
AuthKind::Mount | AuthKind::Scope => { AuthKind::Mount | AuthKind::Hub | AuthKind::Scope => {
self.0.build::<super::lua::Demand>().open(url).await?.into() self.0.build::<super::lua::Demand>().open(url).await?.into()
} }
AuthKind::Sftp => self.0.build::<super::sftp::Demand>().open(url).await?.into(), AuthKind::Sftp => self.0.build::<super::sftp::Demand>().open(url).await?.into(),

View file

@ -55,7 +55,7 @@ where
match (from.kind().is_local(), to.kind().is_local()) { match (from.kind().is_local(), to.kind().is_local()) {
(true, true) => Local::new(from).await?.copy(to.loc(), attrs).await, (true, true) => Local::new(from).await?.copy(to.loc(), attrs).await,
(false, false) if from.auth().covariant(to.auth()) => { (false, false) if from.auth().same_service(to.auth()) => {
Engines::new(from).await?.copy(to.loc(), attrs).await Engines::new(from).await?.copy(to.loc(), attrs).await
} }
(true, false) | (false, true) | (false, false) => super::copy_impl(from, to, attrs).await, (true, false) | (false, true) | (false, false) => super::copy_impl(from, to, attrs).await,
@ -75,7 +75,7 @@ where
let (from, to) = (from.as_url(), to.as_url()); let (from, to) = (from.as_url(), to.as_url());
let attrs = attrs.into(); let attrs = attrs.into();
if from.auth().covariant(to.auth()) { if from.auth().same_service(to.auth()) {
let engine = Engines::new(from).await?; let engine = Engines::new(from).await?;
if engine.capabilities().await?.copy_progressive { if engine.capabilities().await?.copy_progressive {
return engine.copy_progressive(to.loc(), attrs); return engine.copy_progressive(to.loc(), attrs);
@ -119,7 +119,7 @@ where
V: AsUrl, V: AsUrl,
{ {
let (original, link) = (original.as_url(), link.as_url()); let (original, link) = (original.as_url(), link.as_url());
if original.auth().covariant(link.auth()) { if original.auth().same_service(link.auth()) {
Engines::new(original).await?.hard_link(link.loc()).await Engines::new(original).await?.hard_link(link.loc()).await
} else { } else {
Err(io::Error::from(io::ErrorKind::CrossesDevices)) Err(io::Error::from(io::ErrorKind::CrossesDevices))
@ -208,7 +208,7 @@ where
V: AsUrl, V: AsUrl,
{ {
let (from, to) = (from.as_url(), to.as_url()); let (from, to) = (from.as_url(), to.as_url());
if from.auth().covariant(to.auth()) { if from.auth().same_service(to.auth()) {
Engines::new(from).await?.rename(to.loc()).await Engines::new(from).await?.rename(to.loc()).await
} else { } else {
Err(io::Error::from(io::ErrorKind::CrossesDevices)) Err(io::Error::from(io::ErrorKind::CrossesDevices))
@ -268,7 +268,9 @@ where
let url = url.into(); let url = url.into();
match url.as_url() { match url.as_url() {
Url::Regular(_) | Url::Search { .. } => yazi_fs::engine::local::try_absolute(url), Url::Regular(_) | Url::Search { .. } => yazi_fs::engine::local::try_absolute(url),
Url::Mount { .. } | Url::Scope { .. } | Url::Sftp { .. } => super::try_absolute_impl(url), Url::Mount { .. } | Url::Hub { .. } | Url::Scope { .. } | Url::Sftp { .. } => {
super::try_absolute_impl(url)
}
} }
} }

View file

@ -2,7 +2,7 @@ use std::io;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use yazi_fs::{cha::Cha, engine::{Attrs, Capabilities, Engine}}; use yazi_fs::{cha::Cha, engine::{Attrs, Capabilities, Engine}};
use yazi_shared::{path::{AsPath, PathBufDyn}, strand::AsStrand, url::{Url, UrlBuf, UrlCow}}; use yazi_shared::{path::{DynPath, PathBufDyn}, strand::AsStrand, url::{Url, UrlBuf, UrlCow}};
#[derive(Clone)] #[derive(Clone)]
pub(super) enum Engines<'a> { pub(super) enum Engines<'a> {
@ -52,7 +52,7 @@ impl<'a> Engine for Engines<'a> {
async fn copy<P>(&self, to: P, attrs: Attrs) -> io::Result<u64> async fn copy<P>(&self, to: P, attrs: Attrs) -> io::Result<u64>
where where
P: AsPath, P: DynPath,
{ {
match self { match self {
Self::Local(p) => p.copy(to, attrs).await, Self::Local(p) => p.copy(to, attrs).await,
@ -63,7 +63,7 @@ impl<'a> Engine for Engines<'a> {
fn copy_progressive<P, A>(&self, to: P, attrs: A) -> io::Result<mpsc::Receiver<io::Result<u64>>> fn copy_progressive<P, A>(&self, to: P, attrs: A) -> io::Result<mpsc::Receiver<io::Result<u64>>>
where where
P: AsPath, P: DynPath,
A: Into<Attrs>, A: Into<Attrs>,
{ {
match self { match self {
@ -107,7 +107,7 @@ impl<'a> Engine for Engines<'a> {
async fn hard_link<P>(&self, to: P) -> io::Result<()> async fn hard_link<P>(&self, to: P) -> io::Result<()>
where where
P: AsPath, P: DynPath,
{ {
match self { match self {
Self::Local(p) => p.hard_link(to).await, Self::Local(p) => p.hard_link(to).await,
@ -129,7 +129,7 @@ impl<'a> Engine for Engines<'a> {
Ok(match url.kind() { Ok(match url.kind() {
K::Regular | K::Search => Self::Me::Local(yazi_fs::engine::local::Local::new(url).await?), K::Regular | K::Search => Self::Me::Local(yazi_fs::engine::local::Local::new(url).await?),
K::Mount | K::Scope => Self::Me::Lua(super::lua::Lua::new(url).await?), K::Mount | K::Hub | K::Scope => Self::Me::Lua(super::lua::Lua::new(url).await?),
K::Sftp => Self::Me::Sftp(super::sftp::Sftp::new(url).await?), K::Sftp => Self::Me::Sftp(super::sftp::Sftp::new(url).await?),
}) })
} }
@ -184,7 +184,7 @@ impl<'a> Engine for Engines<'a> {
async fn rename<P>(&self, to: P) -> io::Result<()> async fn rename<P>(&self, to: P) -> io::Result<()>
where where
P: AsPath, P: DynPath,
{ {
match self { match self {
Self::Local(p) => p.rename(to).await, Self::Local(p) => p.rename(to).await,

View file

@ -6,7 +6,7 @@ use yazi_binding::MpscTx;
use yazi_config::vfs::{ServiceLua, Vfs}; use yazi_config::vfs::{ServiceLua, Vfs};
use yazi_fs::{cha::Cha, engine::{Attrs, Capabilities, Engine}, file::Files}; use yazi_fs::{cha::Cha, engine::{Attrs, Capabilities, Engine}, file::Files};
use yazi_runner::{RUNNER, provider::{ProvideResult, ProviderJob}}; use yazi_runner::{RUNNER, provider::{ProvideResult, ProviderJob}};
use yazi_shared::{event::Cmd, path::{AsPath, PathBufDyn}, strand::AsStrand, url::{AsUrl, Url, UrlBuf, UrlCow}}; use yazi_shared::{event::Cmd, path::{DynPath, PathBufDyn}, strand::AsStrand, url::{AsUrl, Url, UrlBuf, UrlCow}};
use crate::engine::lua::ReadDir; use crate::engine::lua::ReadDir;
@ -48,23 +48,23 @@ impl<'a> Engine for Lua<'a> {
async fn copy<P>(&self, to: P, attrs: Attrs) -> io::Result<u64> async fn copy<P>(&self, to: P, attrs: Attrs) -> io::Result<u64>
where where
P: AsPath, P: DynPath,
{ {
let from = self.url.to_owned(); let from = self.url.to_owned();
let to = to.as_path().to_owned(); let to = to.dyn_path().to_owned();
Ok(self.call(ProviderJob::Copy { from, to, attrs }).await?.0?) Ok(self.call(ProviderJob::Copy { from, to, attrs }).await?.0?)
} }
fn copy_progressive<P, A>(&self, to: P, attrs: A) -> io::Result<mpsc::Receiver<io::Result<u64>>> fn copy_progressive<P, A>(&self, to: P, attrs: A) -> io::Result<mpsc::Receiver<io::Result<u64>>>
where where
P: AsPath, P: DynPath,
A: Into<Attrs>, A: Into<Attrs>,
{ {
let (tx, rx) = mpsc::channel(20); let (tx, rx) = mpsc::channel(20);
let job = ProviderJob::CopyProgressive { let job = ProviderJob::CopyProgressive {
from: self.url.to_owned(), from: self.url.to_owned(),
to: to.as_path().to_owned(), to: to.dyn_path().to_owned(),
attrs: attrs.into(), attrs: attrs.into(),
tx: MpscTx::map(tx.clone(), Ok), tx: MpscTx::map(tx.clone(), Ok),
}; };
@ -88,10 +88,10 @@ impl<'a> Engine for Lua<'a> {
async fn hard_link<P>(&self, to: P) -> io::Result<()> async fn hard_link<P>(&self, to: P) -> io::Result<()>
where where
P: AsPath, P: DynPath,
{ {
let from = self.url.to_owned(); let from = self.url.to_owned();
let to = to.as_path().to_owned(); let to = to.dyn_path().to_owned();
Ok(self.call(ProviderJob::HardLink { from, to }).await?.ok()?) Ok(self.call(ProviderJob::HardLink { from, to }).await?.ok()?)
} }
@ -103,7 +103,7 @@ impl<'a> Engine for Lua<'a> {
} }
async fn new<'b>(url: Url<'b>) -> io::Result<Self::Me<'b>> { async fn new<'b>(url: Url<'b>) -> io::Result<Self::Me<'b>> {
let (Url::Mount { auth, .. } | Url::Scope { auth, .. }) = url else { let (Url::Mount { auth, .. } | Url::Hub { auth, .. } | Url::Scope { auth, .. }) = url else {
return Err(io::Error::new( return Err(io::Error::new(
io::ErrorKind::InvalidInput, io::ErrorKind::InvalidInput,
format!("Not a custom VFS URL: {url:?}"), format!("Not a custom VFS URL: {url:?}"),
@ -141,10 +141,10 @@ impl<'a> Engine for Lua<'a> {
async fn rename<P>(&self, to: P) -> io::Result<()> async fn rename<P>(&self, to: P) -> io::Result<()>
where where
P: AsPath, P: DynPath,
{ {
let from = self.url.to_owned(); let from = self.url.to_owned();
let to = to.as_path().to_owned(); let to = to.dyn_path().to_owned();
Ok(self.call(ProviderJob::Rename { from, to }).await?.ok()?) Ok(self.call(ProviderJob::Rename { from, to }).await?.ok()?)
} }

View file

@ -4,7 +4,7 @@ use tokio::{io::{AsyncWriteExt, BufReader, BufWriter}, sync::mpsc::Receiver};
use yazi_config::vfs::{ServiceSftp, Vfs}; use yazi_config::vfs::{ServiceSftp, Vfs};
use yazi_fs::engine::{Capabilities, DirReader, Engine, FileHolder}; use yazi_fs::engine::{Capabilities, DirReader, Engine, FileHolder};
use yazi_sftp::fs::{Attrs, Flags}; use yazi_sftp::fs::{Attrs, Flags};
use yazi_shared::{auth::AuthKind, loc::LocBuf, path::{AsPath, PathBufDyn}, strand::AsStrand, url::{Url, UrlBuf, UrlCow, UrlLike}}; use yazi_shared::{auth::AuthKind, loc::LocBuf, path::{DynPath, PathBufDyn}, strand::AsStrand, url::{Url, UrlBuf, UrlCow, UrlLike}};
use super::Cha; use super::Cha;
use crate::engine::sftp::Conn; use crate::engine::sftp::Conn;
@ -83,9 +83,9 @@ impl<'a> Engine for Sftp<'a> {
async fn copy<P>(&self, to: P, attrs: yazi_fs::engine::Attrs) -> io::Result<u64> async fn copy<P>(&self, to: P, attrs: yazi_fs::engine::Attrs) -> io::Result<u64>
where where
P: AsPath, P: DynPath,
{ {
let to = to.as_path().as_unix()?; let to = to.dyn_path().as_unix()?;
let attrs = super::Attrs(attrs).try_into().unwrap_or_default(); let attrs = super::Attrs(attrs).try_into().unwrap_or_default();
let op = self.op().await?; let op = self.op().await?;
@ -107,12 +107,12 @@ impl<'a> Engine for Sftp<'a> {
fn copy_progressive<P, A>(&self, to: P, attrs: A) -> io::Result<Receiver<io::Result<u64>>> fn copy_progressive<P, A>(&self, to: P, attrs: A) -> io::Result<Receiver<io::Result<u64>>>
where where
P: AsPath, P: DynPath,
A: Into<yazi_fs::engine::Attrs>, A: Into<yazi_fs::engine::Attrs>,
{ {
let to = UrlBuf::Sftp { let to = UrlBuf::Sftp {
loc: LocBuf::<typed_path::UnixPathBuf>::saturated( loc: LocBuf::<typed_path::UnixPathBuf>::saturated(
to.as_path().to_unix_owned()?, to.dyn_path().to_unix_owned()?,
AuthKind::Sftp, AuthKind::Sftp,
), ),
auth: self.config.auth.clone(), auth: self.config.auth.clone(),
@ -138,9 +138,9 @@ impl<'a> Engine for Sftp<'a> {
async fn hard_link<P>(&self, to: P) -> io::Result<()> async fn hard_link<P>(&self, to: P) -> io::Result<()>
where where
P: AsPath, P: DynPath,
{ {
let to = to.as_path().as_unix()?; let to = to.dyn_path().as_unix()?;
Ok(self.op().await?.hardlink(self.path, to).await?) Ok(self.op().await?.hardlink(self.path, to).await?)
} }
@ -176,9 +176,9 @@ impl<'a> Engine for Sftp<'a> {
async fn rename<P>(&self, to: P) -> io::Result<()> async fn rename<P>(&self, to: P) -> io::Result<()>
where where
P: AsPath, P: DynPath,
{ {
let to = to.as_path().as_unix()?; let to = to.dyn_path().as_unix()?;
let op = self.op().await?; let op = self.op().await?;
match op.rename_posix(self.path, &to).await { match op.rename_posix(self.path, &to).await {

View file

@ -97,20 +97,20 @@ impl Local {
let mut ops = Vec::with_capacity(urls.len()); let mut ops = Vec::with_capacity(urls.len());
for u in urls { for u in urls {
let Some((parent, urn)) = u.pair() else { continue }; let Some((parent, key)) = u.pair2() else { continue };
let Ok(file) = File::new(&u).await else { let Ok(file) = File::new(&u).await else {
ops.push(FilesOp::Deleting(parent.into(), [urn.into()].into())); ops.push(FilesOp::Deleting(parent.into(), [key.into()].into()));
continue; continue;
}; };
if let Some(p) = file.url.as_local() if let Some(p) = file.url.as_local()
&& !engine::local::match_name_case(p).await && !engine::local::match_name_case(p).await
{ {
ops.push(FilesOp::Deleting(parent.into(), [urn.into()].into())); ops.push(FilesOp::Deleting(parent.into(), [key.into()].into()));
continue; continue;
} }
ops.push(FilesOp::Upserting(parent.into(), [(urn.into(), file)].into())); ops.push(FilesOp::Upserting(parent.into(), [(key.into(), file)].into()));
} }
FilesOp::mutate(ops); FilesOp::mutate(ops);

View file

@ -39,16 +39,16 @@ impl Remote {
let mut ups = Vec::with_capacity(urls.len()); let mut ups = Vec::with_capacity(urls.len());
for (u, upload) in urls { for (u, upload) in urls {
let Some((parent, urn)) = u.pair() else { continue }; let Some((parent, key)) = u.pair2() else { continue };
let Ok(mut file) = File::new(&u).await else { let Ok(mut file) = File::new(&u).await else {
ops.push(FilesOp::Deleting(parent.into(), [urn.into()].into())); ops.push(FilesOp::Deleting(parent.into(), [key.into()].into()));
continue; continue;
}; };
let is_file = file.is_file(); let is_file = file.is_file();
file.cha.ctime = Some(SystemTime::now()); file.cha.ctime = Some(SystemTime::now());
ops.push(FilesOp::Upserting(parent.into(), [(urn.into(), file)].into())); ops.push(FilesOp::Upserting(parent.into(), [(key.into(), file)].into()));
if upload && is_file { if upload && is_file {
ups.push(u); ups.push(u);
} }

View file

@ -21,7 +21,7 @@ impl Reporter {
for url in urls.into_iter().map(Into::into) { for url in urls.into_iter().map(Into::into) {
match url.as_url().kind() { match url.as_url().kind() {
AuthKind::Regular | AuthKind::Search => self.report_local(url), AuthKind::Regular | AuthKind::Search => self.report_local(url),
AuthKind::Mount => {} // TODO: mounted VFS cache invalidation AuthKind::Mount | AuthKind::Hub => {} // TODO: mounted VFS cache invalidation
AuthKind::Scope | AuthKind::Sftp => self.report_remote(url), AuthKind::Scope | AuthKind::Sftp => self.report_remote(url),
} }
} }

View file

@ -59,7 +59,7 @@ impl Watched {
AuthKind::Mount => UrlBuf::Mount { loc: path.into_os().ok()?.into(), auth }, AuthKind::Mount => UrlBuf::Mount { loc: path.into_os().ok()?.into(), auth },
AuthKind::Scope => UrlBuf::Scope { loc: path.into_unix().ok()?.into(), auth }, AuthKind::Scope => UrlBuf::Scope { loc: path.into_unix().ok()?.into(), auth },
AuthKind::Sftp => UrlBuf::Sftp { loc: path.into_unix().ok()?.into(), auth }, AuthKind::Sftp => UrlBuf::Sftp { loc: path.into_unix().ok()?.into(), auth },
AuthKind::Regular | AuthKind::Search => return None, AuthKind::Regular | AuthKind::Search | AuthKind::Hub => return None,
}; };
if self.contains_url(&url) { Some(url) } else { None } if self.contains_url(&url) { Some(url) } else { None }
} }