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_macro::{render, succ};
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};
@ -30,7 +30,7 @@ impl Actor for Show {
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() {
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_parser::cmp::TriggerForm;
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 crate::{Actor, Ctx};
@ -78,7 +78,7 @@ impl Trigger {
}
// 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
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_config::THEME;
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 crate::lives::{CoreRef, PtrCell};
@ -73,7 +73,11 @@ impl UserData for File {
Ok(yazi_config::THEME.icon.matches(me, me.is_hovered()))
});
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, ()| {
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();
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, ()| {
let core: CoreRef = lua.named_registry_value("cx")?;
@ -122,7 +126,7 @@ impl UserData for File {
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);
};

View file

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

View file

@ -64,10 +64,10 @@ impl Create {
if dir {
engine::create_dir_all(&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);
FilesOp::Deleting(parent.into(), [urn.into()].into()).emit();
FilesOp::Deleting(parent.into(), [key.into()].into()).emit();
engine::create(&new).await?;
} else if let Some(parent) = new.parent() {
engine::create_dir_all(parent).await.ok();
@ -78,10 +78,10 @@ impl Create {
}
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?;
FilesOp::Upserting(parent.into(), [(urn.into(), file)].into()).emit();
FilesOp::Upserting(parent.into(), [(key.into(), file)].into()).emit();
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> {
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);
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:peek, 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);
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| {
if f.stage == FolderStage::Loading {
render!();
@ -43,7 +43,7 @@ impl Actor for Hidden {
{
render!(h.repos(None));
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:watch, cx)?;
}

View file

@ -18,19 +18,19 @@ impl Actor for Hover {
// Parent should always track CWD
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
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
if let (Some(h), Some(u)) = (tab.hovered(), form.urn)
&& h.urn() == u
if let (Some(h), Some(key)) = (tab.hovered(), form.key)
&& h.entry_key() == key
{
// `hover(Some)` occurs after user actions, such as create, rename, reveal, etc.
// At this point, it's intuitive to track the file location regardless.
tab.current.trace = Some(u.clone());
// At this point, it's intuitive to track the entry regardless.
tab.current.trace = Some(key.clone());
cx.tasks.scheduler.behavior.reset();
}

View file

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

View file

@ -14,7 +14,7 @@ impl Actor for Reveal {
const NAME: &str = "reveal";
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
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,
// 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)]);
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);
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| {
if f.stage == FolderStage::Loading {
render!();
@ -50,7 +50,7 @@ impl Actor for Sort {
{
render!(h.repos(None));
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:watch, cx)?;
}

View file

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

View file

@ -19,10 +19,10 @@ impl Actor for UpdateSucceed {
if form.track
&& 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()
{
cx.current_mut().trace = Some(urn.into());
cx.current_mut().trace = Some(key.into());
act!(mgr:hover, cx)?;
}

View file

@ -1,7 +1,7 @@
use serde::Deserialize;
use yazi_binding::position::{Offset, Origin, Position};
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;
#[derive(Deserialize, DeserializeOver, DeserializeOver2)]

View file

@ -1,16 +1,28 @@
use std::sync::Arc;
use hashbrown::HashMap;
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 super::{DomainSeed, Domains, Service};
use super::{DomainSeed, Domains};
use crate::vfs::Service;
pub struct Authorities(HashMap<Scheme, Domains>);
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)
}
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 {
@ -41,7 +53,7 @@ impl<'de> Deserialize<'de> for Authorities {
impl DeserializeOverWith for Authorities {
fn deserialize_over_with<'de, D: Deserializer<'de>>(mut self, de: D) -> Result<Self, D::Error> {
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)
}

View file

@ -1,34 +1,66 @@
use std::{ops::{Deref, DerefMut}, sync::Arc};
use std::sync::Arc;
use hashbrown::HashMap;
use serde::{Deserialize, Deserializer, de::{DeserializeSeed, Error}};
use yazi_shared::{KebabCasedKey, auth::Scheme};
use serde::{Deserialize, Deserializer, de::{self, DeserializeSeed, Error}};
use yazi_shared::auth::{AuthKind, Domain, Scheme};
use super::{Service, ServiceSftp};
#[derive(Default)]
pub struct Domains(pub(super) HashMap<KebabCasedKey, Service>);
impl Deref for Domains {
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 }
pub struct Domains {
exact: HashMap<Domain<'static>, Service>,
catchall: Option<Service>,
}
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) {
for (domain, service) in &mut self.0 {
for (domain, service) in &mut self.exact {
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.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;
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 => {
return Err(D::Error::custom("scheme cannot be configured"));
}
Scheme::Sftp => {
let map = HashMap::<KebabCasedKey, ServiceSftp>::deserialize(deserializer)?;
map.into_iter().map(|(domain, service)| (domain, Service::Sftp(service))).collect()
let map = HashMap::<Domain<'static>, ServiceSftp>::deserialize(deserializer)?;
Domains::from_map(
map.into_iter().map(|(domain, service)| (domain, Service::Sftp(service))).collect(),
)?
}
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(_))) {
return Err(D::Error::custom("SFTP services must use the `sftp` scheme"));
}
map
Domains::from_map(map)?
}
});
};
domains.init(self.0);
Ok(domains)

View file

@ -10,6 +10,7 @@ use super::{ServiceLua, ServiceSftp};
pub enum Service {
Sftp(ServiceSftp),
Mount(ServiceLua),
Hub(ServiceLua),
Scope(ServiceLua),
}
@ -19,7 +20,7 @@ impl TryFrom<&'static Service> for &'static ServiceSftp {
fn try_from(value: &'static Service) -> Result<Self, Self::Error> {
match value {
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")
}
}
@ -32,7 +33,7 @@ impl TryFrom<&'static Service> for &'static ServiceLua {
fn try_from(value: &'static Service) -> Result<Self, Self::Error> {
match value {
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 {
Self::Sftp(_) => AuthKind::Sftp,
Self::Mount(_) => AuthKind::Mount,
Self::Hub(_) => AuthKind::Hub,
Self::Scope(_) => AuthKind::Scope,
}
}
@ -50,6 +52,7 @@ impl Service {
match self {
Self::Sftp(sftp) => &sftp.auth,
Self::Mount(lua) => &lua.auth,
Self::Hub(lua) => &lua.auth,
Self::Scope(lua) => &lua.auth,
}
}
@ -58,6 +61,7 @@ impl Service {
match self {
Self::Sftp(sftp) => &mut sftp.auth,
Self::Mount(lua) => &mut lua.auth,
Self::Hub(lua) => &mut lua.auth,
Self::Scope(lua) => &mut lua.auth,
}
}

View file

@ -21,7 +21,7 @@ impl Vfs {
where
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}")));
};
@ -47,8 +47,6 @@ impl DeserializeOverWith for Vfs {
// --- Inject
inventory::submit! {
AuthInventory {
get: |scheme, domain| {
VFS.authorities.get(scheme, domain).map(|service| service.auth().clone())
},
get: |scheme, domain| VFS.authorities.auth(scheme, domain),
}
}

View file

@ -67,7 +67,7 @@ impl Yanked {
}
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() {
let old = self.files.len();
self.files.retain(|f| !removal.iter().any(|u| f.url.covariant(u)));

View file

@ -1,7 +1,7 @@
use anyhow::Result;
use hashbrown::HashMap;
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;
@ -62,7 +62,7 @@ impl Finder {
continue;
}
self.matched.insert(file.urn().into(), i);
self.matched.insert(file.entry_key().into(), i);
if self.matched.len() > 99 {
break;
}
@ -78,9 +78,9 @@ impl Finder {
impl Finder {
pub fn matched_idx<T>(&self, folder: &Folder, urn: T) -> Option<u8>
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_fs::{Entries, FilesOp, FolderStage, cha::Cha, file::File};
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 crate::MgrProxy;
@ -104,24 +104,26 @@ impl Folder {
b
}
pub fn hover(&mut self, urn: PathDyn) -> bool {
if self.hovered().map(|h| h.urn()) == Some(urn) {
pub fn hover(&mut self, key: PathDyn) -> bool {
if key.is_empty() {
return self.arrow(0);
} else if self.hovered().map(|h| h.entry_key()) == Some(key) {
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);
self.retrace();
b
}
pub fn repos(&mut self, urn: Option<PathDyn>) -> bool {
if let Some(u) = urn {
self.hover(u)
} else if let Some(u) = self.trace.take() {
let b = self.hover(u.as_path());
self.trace = Some(u);
pub fn repos(&mut self, key: Option<PathDyn>) -> bool {
if let Some(k) = key {
self.hover(k)
} else if let Some(k) = self.trace.take() {
let b = self.hover(k.dyn_path());
self.trace = Some(k);
b
} else {
self.arrow(0)
@ -129,7 +131,7 @@ impl Folder {
}
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) {

View file

@ -133,7 +133,7 @@ impl Selected {
}
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() {
self.remove_many(&removal);
}
@ -190,6 +190,12 @@ mod tests {
assert!(!s.remove(Path::new("/a/b")));
assert!(s.inner.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]

View file

@ -85,7 +85,7 @@ impl Tab {
pub fn hovered_mut(&mut self) -> Option<&mut File> { self.current.hovered_mut() }
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;
rect.y = rect.y.saturating_sub(1) + y as u16;

View file

@ -54,7 +54,11 @@ impl Tasks {
targets
.iter()
.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)
.collect()

View file

@ -51,6 +51,6 @@ fn try_absolute_impl<'a>(url: UrlCow<'a>) -> Option<UrlCow<'a>> {
Some(match url.as_url() {
Url::Regular(_) => UrlBuf::Regular(loc).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 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}};
@ -45,19 +45,19 @@ impl<'a> Engine for Local<'a> {
#[inline]
async fn copy<P>(&self, to: P, attrs: Attrs) -> io::Result<u64>
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();
super::copy_impl(from, to, attrs).await
}
fn copy_progressive<P, A>(&self, to: P, attrs: A) -> io::Result<mpsc::Receiver<io::Result<u64>>>
where
P: AsPath,
P: DynPath,
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();
Ok(super::copy_progressive_impl(from, to, attrs.into()))
}
@ -71,9 +71,9 @@ impl<'a> Engine for Local<'a> {
#[inline]
async fn hard_link<P>(&self, to: P) -> io::Result<()>
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
}
@ -87,7 +87,7 @@ impl<'a> Engine for Local<'a> {
async fn new<'b>(url: Url<'b>) -> io::Result<Self::Me<'b>> {
match url {
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:?}")))
}
}
@ -101,7 +101,7 @@ impl<'a> Engine for Local<'a> {
reader: tokio::fs::read_dir(self.path).await?,
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,
format!("Not a local URL: {:?}", self.url),
))?,
@ -125,9 +125,9 @@ impl<'a> Engine for Local<'a> {
#[inline]
async fn rename<P>(&self, to: P) -> io::Result<()>
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
}

View file

@ -2,7 +2,7 @@ use std::io;
use tokio::{io::{AsyncRead, AsyncSeek, AsyncWrite, AsyncWriteExt}, sync::mpsc};
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}};
@ -23,11 +23,11 @@ pub trait Engine: Sized {
fn copy<P>(&self, to: P, attrs: Attrs) -> impl Future<Output = io::Result<u64>>
where
P: AsPath;
P: DynPath;
fn copy_progressive<P, A>(&self, to: P, attrs: A) -> io::Result<mpsc::Receiver<io::Result<u64>>>
where
P: AsPath,
P: DynPath,
A: Into<Attrs>;
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<()>>
where
P: AsPath;
P: DynPath;
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<()>>
where
P: AsPath;
P: DynPath;
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 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 crate::{FILES_TICKET, SortBy, file::File};
@ -69,19 +69,19 @@ impl Entries {
}
}
pub fn update_size(&mut self, mut sizes: HashMap<PathBufDyn, u64>) {
if sizes.len() <= 50 {
sizes.retain(|k, v| self.sizes.get(k) != Some(v));
pub fn update_size(&mut self, sizes: HashMap<PathBufDyn, u64>) {
self.sizes.reserve(if self.sizes.is_empty() { sizes.len() } else { sizes.len().div_ceil(2) });
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() {
return;
}
if self.sorter.by == SortBy::Size {
if changed && self.sorter.by == SortBy::Size {
self.revision += 1;
}
self.sizes.extend(sizes);
}
pub fn update_ioerr(&mut self) {
@ -97,9 +97,10 @@ impl Entries {
macro_rules! go {
($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 {
if todo.remove(&f.urn()).is_some() && todo.is_empty() {
if todo.remove(&f.entry_key()).is_some() && todo.is_empty() {
break;
}
}
@ -119,48 +120,14 @@ impl Entries {
}
}
#[cfg(unix)]
pub fn update_deleting(&mut self, urns: HashSet<PathBufDyn>) -> Vec<usize> {
use yazi_shared::path::PathLike;
if urns.is_empty() {
return vec![];
}
pub fn update_deleting(&mut self, mut keys: HashSet<PathBufDyn>) -> Vec<usize> {
keys.retain(|k| !k.is_empty());
let mut deleted = Vec::with_capacity(keys.len());
let (mut hidden, mut items) = if let Some(filter) = &self.filter {
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() {
if !keys.is_empty() {
let mut i = 0;
self.items.retain(|f| {
let b = items.remove(&f.urn());
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());
let b = keys.remove(&f.entry_key());
if b {
deleted.push(i)
}
@ -168,8 +135,9 @@ impl Entries {
!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;
@ -178,8 +146,9 @@ impl Entries {
pub fn update_updating(
&mut self,
files: HashMap<PathBufDyn, File>,
mut files: 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() {
return Default::default();
}
@ -188,9 +157,9 @@ impl Entries {
($dist:expr, $src:expr, $inc:literal) => {
let mut b = true;
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].urn() == f.urn();
b = b && $dist[i].entry_key() == f.entry_key();
$dist[i] = f;
if $src.is_empty() {
@ -221,13 +190,18 @@ impl Entries {
(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() {
return;
}
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);
@ -255,14 +229,13 @@ impl Entries {
}
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 {
files
.into_iter()
.partition(|f| (f.is_hidden() && !self.show_hidden) || !filter.matches(f.urn()))
files.partition(|f| (f.is_hidden() && !self.show_hidden) || !filter.matches(f.urn()))
} else if self.show_hidden {
(vec![], files.into_iter().collect())
(vec![], files.collect())
} else {
files.into_iter().partition(|f| f.is_hidden())
files.partition(|f| f.is_hidden())
}
}
}
@ -270,7 +243,9 @@ impl Entries {
impl Entries {
// --- Items
#[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
#[inline]

View file

@ -63,6 +63,9 @@ impl File {
#[inline]
pub fn urn(&self) -> PathDyn<'_> { self.url.urn() }
#[inline]
pub fn entry_key(&self) -> PathDyn<'_> { self.url.entry_key() }
#[inline]
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(&[""], "");
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/b"], "/a");

View file

@ -2,7 +2,7 @@ use std::{iter, path::Path};
use hashbrown::{HashMap, HashSet};
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};
@ -42,16 +42,17 @@ impl FilesOp {
pub fn files(&self) -> Box<dyn Iterator<Item = &File> + '_> {
match self {
Self::Full(_, files, _) => Box::new(files.iter()),
Self::Part(_, files, _) => Box::new(files.iter()),
Self::Full(_, files, _) | Self::Part(_, files, _) | Self::Creating(_, files) => {
Box::new(files.iter().filter(|f| !f.entry_key().is_empty()))
}
Self::Done(..) => Box::new(iter::empty()),
Self::Size(..) => Box::new(iter::empty()),
Self::IOErr(..) => Box::new(iter::empty()),
Self::Creating(_, files) => Box::new(files.iter()),
Self::Deleting(..) => Box::new(iter::empty()),
Self::Updating(_, map) => Box::new(map.values()),
Self::Upserting(_, map) => Box::new(map.values()),
Self::Updating(_, map) | Self::Upserting(_, map) => {
Box::new(map.values().filter(|f| !f.entry_key().is_empty()))
}
}
}
@ -68,7 +69,7 @@ impl FilesOp {
pub fn create(files: Vec<File>) {
let mut parents: HashMap<UrlBuf, Vec<_>> = Default::default();
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);
}
}
@ -80,18 +81,18 @@ impl FilesOp {
pub fn rename(map: HashMap<UrlBuf, File>) {
let mut parents: HashMap<UrlBuf, (HashSet<_>, HashMap<_, _>)> = Default::default();
for (o, n) in map {
let Some(o_p) = o.parent() else { continue };
let Some(n_p) = n.url.parent() else { continue };
let Some((o_p, o_k)) = o.pair2() else { continue };
let Some((n_p, n_k)) = n.url.pair2() else { continue };
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 {
parents.get_or_insert_default(o_p).0.insert(o.urn().into());
parents.get_or_insert_default(n_p).1.insert(n.urn().into(), n);
parents.get_or_insert_default(o_p).0.insert(o_k.into());
parents.get_or_insert_default(n_p).1.insert(n_k.into(), n);
}
}
for (p, (o, n)) in parents {
match (o.is_empty(), n.is_empty()) {
(true, true) => unreachable!(),
(true, true) => {}
(true, false) => Self::Upserting(p, n).emit(),
(false, true) => Self::Deleting(p, o).emit(),
(false, false) => {
@ -106,14 +107,20 @@ impl FilesOp {
let mut parents: HashMap<_, (HashMap<_, _>, HashSet<_>)> = Default::default();
for op in ops {
match op {
Self::Upserting(p, map) => parents.entry(p).or_default().0.extend(map),
Self::Deleting(p, urns) => parents.entry(p).or_default().1.extend(urns),
Self::Upserting(p, map) => parents
.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!(),
}
}
for (p, (u, d)) in parents {
match (u.is_empty(), d.is_empty()) {
(true, true) => unreachable!(),
(true, true) => {}
(true, false) => Self::Deleting(p, d).emit(),
(false, true) => Self::Upserting(p, u).emit(),
(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 {
Self::Deleting(cwd, urns) => {
(urns.iter().filter_map(|u| cwd.try_join(u).ok()).collect(), vec![])
Self::Deleting(_, keys) => {
(it.filter(|u| keys.contains(&u.entry_key())).cloned().collect(), vec![])
}
Self::Updating(cwd, urns) | Self::Upserting(cwd, urns) => urns
.iter()
.filter(|&(u, f)| u != f.urn())
.filter_map(|(u, f)| cwd.try_join(u).ok().map(|u| (u, f)))
.filter(|(u, _)| contains(u))
.map(|(u, f)| (u, f.clone()))
Self::Updating(_, files) | Self::Upserting(_, files) => it
.filter_map(|u| files.get(&u.entry_key()).map(|f| (u, f)))
.filter(|(_, f)| !f.entry_key().is_empty())
.filter(|&(u, f)| u != f.url)
.map(|(u, f)| (u.clone(), f.clone()))
.unzip(),
_ => (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 {
let cow: UrlCow = url.into();
let (path, uri, urn) = clean_path_impl(
cow.loc(),
cow.base().components().count() - 1,
cow.trail().components().count() - 1,
let depth = cow.loc().components().count();
let base = depth - cow.uri().components().count();
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, path).try_into().expect("UrlBuf from cleaned path")
spec.auth = auth.unwrap_or(spec.auth);
(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::*;
let mut auths: Vec<_> = iter::successors(auth, |auth| auth.parent.clone()).collect();
let root = auths.pop();
let mut out = vec![];
let mut uri_count = 0;
let mut urn_count = 0;
macro_rules! push {
($i:ident, $c:ident) => {{
out.push(($i, $c));
($i:ident, $c:ident, $auth:ident) => {{
out.push(($i, $c, $auth));
if $i >= base {
uri_count += 1;
}
@ -33,7 +50,7 @@ fn clean_path_impl(path: PathDyn, base: usize, trail: usize) -> (PathBufDyn, usi
macro_rules! pop {
() => {{
if let Some((i, _)) = out.pop() {
if let Some((i, ..)) = out.pop() {
if i >= base {
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() {
let auth = (root.is_some() && c.has_auth()).then(|| auths.pop().unwrap());
match c {
CurDir => {}
ParentDir => match out.last().map(|(_, c)| c) {
ParentDir => match out.last().map(|(_, c, _)| c) {
Some(RootDir) => {}
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() {
PathBufDyn::with_str(kind, ".")
} 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")
};
(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)]
@ -106,4 +131,24 @@ mod tests {
}
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(),
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 {
loc: LocBuf::<typed_path::UnixPathBuf>::with(path.into_unix().unwrap(), uri, urn).unwrap(),
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: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-hub://a1/@root/$BAR_BAZ", "test-hub://a1:2:2/@,root/bar/baz"),
// -1 component
("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}"),
@ -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/${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
("test-mount://7z//${EM/PT/Y}", "test-mount://7z//"),
("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 yazi_shared::path::{PathCow, PathDyn, PathKind};
const SET: &AsciiSet =
&CONTROLS.add(b'"').add(b'*').add(b':').add(b'<').add(b'>').add(b'?').add(b'\\').add(b'|');
const SET: &AsciiSet = &CONTROLS
.add(b'"')
.add(b'*')
.add(b':')
.add(b'<')
.add(b'>')
.add(b'?')
.add(b'\\')
.add(b'|')
.add(b'%');
pub trait PercentEncoding<'a> {
fn percent_encode(self) -> Cow<'a, Path>;

View file

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

View file

@ -1,6 +1,6 @@
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 crate::Xdg;
@ -13,12 +13,14 @@ impl FsSpec for Auth {
fn cache(&self) -> Option<PathBuf> {
match self.kind {
AuthKind::Regular | AuthKind::Search => None,
AuthKind::Mount | AuthKind::Scope | AuthKind::Sftp => Some(Xdg::temp_dir().join(format!(
"{}_{}_{}",
self.kind.into_str(),
self.scheme,
Encode::domain(&self.domain)
))),
AuthKind::Mount | AuthKind::Hub | AuthKind::Scope | AuthKind::Sftp => {
Some(Xdg::temp_dir().join(format!(
"{}_{}_{}",
self.kind.into_str(),
self.scheme,
EncodeAuth::domain(&self.domain)
)))
}
}
}
}

View file

@ -1,7 +1,7 @@
use std::{borrow::Cow, ffi::OsStr, path::{Path, PathBuf}};
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 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 {
let mut it = loc.components();
if it.next() == Some(yazi_shared::path::Component::RootDir) {
root.push(it.as_path().percent_encode());
root.push(it.dyn_path().percent_encode());
} else {
root.push(".%2F");
root.push(loc.percent_encode());
@ -51,7 +51,7 @@ impl<'a> FsUrl<'a> for Url<'a> {
fn unified_path(self) -> Cow<'a, Path> {
match self {
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()
}
}
@ -66,7 +66,7 @@ impl FsUrl<'_> for UrlBuf {
fn unified_path(self) -> Cow<'static, Path> {
match self {
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()
}
}
@ -81,7 +81,7 @@ impl<'a> FsUrl<'a> for UrlCow<'a> {
fn unified_path(self) -> Cow<'a, Path> {
match self {
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()
}
}

View file

@ -3,11 +3,11 @@ use yazi_shared::path::PathBufDyn;
#[derive(Debug, Default)]
pub struct HoverForm {
pub urn: Option<PathBufDyn>,
pub key: Option<PathBufDyn>,
}
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 {

View file

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

View file

@ -78,7 +78,7 @@ impl Scheduler {
}
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 };
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 };
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
.ops
.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();
// FIXME: use PathBufDyn instead of UrlBuf in SizeIn
FilesOp::Size(
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();
});

View file

@ -1,19 +1,37 @@
use std::{fmt, sync::Arc};
use std::{fmt, hash::{Hash, Hasher}, sync::Arc};
use anyhow::{Result, anyhow};
use percent_encoding::percent_decode_str;
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();
#[derive(Debug, Deserialize, Eq, Hash, PartialEq)]
#[derive(Clone, Debug, Deserialize)]
pub struct Auth {
pub kind: AuthKind,
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 {
@ -21,20 +39,28 @@ impl Default 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 {
pub const DEFAULT: Self =
Self { kind: AuthKind::Regular, scheme: Scheme::Regular, domain: SStr::Borrowed("") };
pub const DEFAULT: Self = Self {
kind: AuthKind::Regular,
scheme: Scheme::Regular,
domain: Domain::EMPTY,
parent: None,
};
pub fn default_arc() -> Arc<Self> { DEFAULT_ARC.clone() }
pub fn new(kind: AuthKind, scheme: Scheme, domain: impl Into<SStr>) -> Arc<Self> {
Arc::new(Self { kind, scheme, domain: domain.into() })
pub fn new<'a>(kind: AuthKind, scheme: Scheme, domain: impl Into<Domain<'a>>) -> Arc<Self> {
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 {
Scheme::Regular => Some(Self::default_arc()),
Scheme::Search => Some(Self::search(domain)),
@ -42,22 +68,88 @@ impl Auth {
}
}
pub fn search(query: impl Into<String>) -> Arc<Self> {
Self::new(AuthKind::Search, Scheme::Search, query.into())
pub fn child(self: Arc<Self>) -> Arc<Self> {
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 {
!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>> {
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}"))?;
Ok(Arc::new(Self {
kind: kind.parse()?,
scheme: scheme.parse()?,
domain: percent_decode_str(domain).decode_utf8()?.into_owned().into(),
}))
Ok(Self::new(kind.parse()?, scheme.parse()?, percent_decode_str(domain).decode_utf8()?))
}
}

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 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<'_> {
pub fn domain(s: &str) -> PercentEncode<'_> {
const SET: &AsciiSet = &CONTROLS.add(b'/').add(b':');
impl EncodeAuth<'_> {
pub fn domain<'a>(s: &'a Domain<'_>) -> PercentEncode<'a> {
const SET: &AsciiSet = &CONTROLS.add(b'/').add(b':').add(b'%');
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 {
write!(
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 crate::auth::{Auth, Scheme};
use crate::auth::{Auth, Domain, Scheme};
pub struct AuthInventory {
pub get: fn(&Scheme, &str) -> Option<Arc<Auth>>,
pub get: fn(&Scheme, &Domain<'_>) -> Option<Arc<Auth>>,
}
inventory::collect!(AuthInventory);

View file

@ -11,6 +11,7 @@ pub enum AuthKind {
Regular,
Search,
Mount,
Hub,
Scope,
Sftp,
}
@ -20,7 +21,7 @@ impl AuthKind {
pub fn is_local(self) -> bool {
match self {
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 {
match self {
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 {
match self {
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); }

View file

@ -1,6 +1,6 @@
use std::{ffi::{OsStr, OsString}, fmt::Debug, hash::Hash};
use crate::{path::{AsPath, AsPathView}, strand::AsStrandView};
use crate::{path::{DynPath, PathView}, strand::AsStrandView};
// --- LocAble
pub trait LocAble<'p>
@ -27,14 +27,10 @@ impl<'p> LocAble<'p> for &'p typed_path::UnixPath {
// --- LocBufAble
pub trait LocBufAble
where
Self: 'static + AsPath + Default,
Self: 'static + DynPath + Default,
{
type Strand<'a>: StrandAble<'a>;
type Borrowed<'a>: LocAble<'a>
+ LocAbleImpl<'a>
+ AsPathView<'a, Self::Borrowed<'a>>
+ Debug
+ Hash;
type Borrowed<'a>: LocAble<'a> + LocAbleImpl<'a> + PathView<'a, Self::Borrowed<'a>> + Debug + Hash;
}
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 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)]
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() }
}
impl<T> AsPath for LocBuf<T>
impl<T> DynPath for LocBuf<T>
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
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>
@ -59,7 +59,7 @@ where
impl<P> Hash for LocBuf<P>
where
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) }
}
@ -67,7 +67,7 @@ where
impl<P> Debug for LocBuf<P>
where
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 {
f.debug_struct("LocBuf")
@ -81,7 +81,7 @@ where
impl<P> From<P> for LocBuf<P>
where
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 {
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>
where
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
where
@ -157,7 +157,7 @@ where
#[inline]
pub fn as_loc<'a>(&'a self) -> Loc<'a, P::Borrowed<'a>> {
Loc {
inner: self.inner.as_path_view(),
inner: self.inner.path_view(),
uri: self.uri,
urn: self.urn,
_phantom: PhantomData,
@ -223,7 +223,7 @@ where
impl<P> LocBuf<P>
where
P: LocBufAble + LocBufAbleImpl,
for<'a> &'a P: AsPathView<'a, P::Borrowed<'a>>,
for<'a> &'a P: PathView<'a, P::Borrowed<'a>>,
{
#[inline]
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 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)]
pub struct Loc<'p, P = &'p std::path::Path> {
@ -29,11 +29,11 @@ where
fn deref(&self) -> &Self::Target { &self.inner }
}
impl<'p, P> AsPath for Loc<'p, P>
impl<'p, P> DynPath for Loc<'p, P>
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
@ -81,9 +81,9 @@ where
pub fn bare<T>(path: T) -> Self
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 p = path.strip_prefix(P::empty()).unwrap();
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
where
T: AsPathView<'p, P>,
T: PathView<'p, P>,
S: AsStrandView<'a, P::Strand<'a>>,
{
let mut loc = Self::bare(path);
@ -133,7 +133,7 @@ where
pub fn new<'a, T, S>(path: T, base: S, trail: S) -> Self
where
T: AsPathView<'p, P>,
T: PathView<'p, P>,
S: AsStrandView<'a, P::Strand<'a>>,
{
let mut loc = Self::bare(path);
@ -143,18 +143,17 @@ where
}
#[inline]
pub fn parent(self) -> Option<P> {
self.inner.parent().filter(|p| !p.as_encoded_bytes().is_empty())
}
pub fn parent(self) -> Option<P> { self.inner.parent() }
pub fn saturated<'a, T>(path: T, kind: AuthKind) -> Self
where
T: AsPathView<'p, P>,
T: PathView<'p, P>,
{
match kind {
AuthKind::Regular => Self::bare(path),
AuthKind::Search => Self::zeroed(path),
AuthKind::Mount => Self::zeroed(path),
AuthKind::Hub => Self::bare(path),
AuthKind::Scope => 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>
where
T: AsPathView<'p, P>,
T: PathView<'p, P>,
{
if urn > uri {
bail!("URN cannot be longer than URI");
@ -238,7 +237,7 @@ where
pub fn zeroed<T>(path: T) -> Self
where
T: AsPathView<'p, P>,
T: PathView<'p, P>,
{
let mut loc = Self::bare(path);
(loc.uri, loc.urn) = (0, 0);
@ -248,8 +247,21 @@ where
#[cfg(test)]
mod tests {
use std::path::Path;
use typed_path::UnixPath;
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]
fn test_with() -> Result<()> {
let cases = [
@ -268,11 +280,11 @@ mod tests {
];
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.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.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_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
#[derive(Clone, Debug, Eq, FromLuaOwned)]
@ -48,24 +48,24 @@ impl TryFrom<PathBufDyn> for typed_path::UnixPathBuf {
// --- Hash
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
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 {
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 {
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 {
fn equivalent(&self, key: &PathDyn<'_>) -> bool { self.as_path() == *key }
fn equivalent(&self, key: &PathDyn<'_>) -> bool { self.dyn_path() == *key }
}
impl PathBufDyn {
@ -126,7 +126,7 @@ impl PathBufDyn {
pub fn try_extend<T>(&mut self, paths: T) -> Result<(), PathDynError>
where
T: IntoIterator,
T::Item: AsPath,
T::Item: DynPath,
{
for p in paths {
self.try_push(p)?;
@ -136,9 +136,9 @@ impl PathBufDyn {
pub fn try_push<T>(&mut self, path: T) -> Result<(), PathDynError>
where
T: AsPath,
T: DynPath,
{
let path = path.as_path();
let path = path.dyn_path();
Ok(match self {
Self::Os(p) => p.push(path.as_os()?),
Self::Unix(p) => p.push(path.encoded_bytes()),
@ -199,7 +199,7 @@ impl Serialize for PathBufDyn {
path: &'a [u8],
}
let path = self.as_path();
let path = self.dyn_path();
Shadow { kind: path.kind(), path: path.encoded_bytes() }.serialize(serializer)
}
}

View file

@ -18,6 +18,9 @@ impl<'a> Component<'a> {
_ => None,
}
}
#[inline]
pub fn has_auth(&self) -> bool { matches!(self, Self::ParentDir | Self::Normal(_)) }
}
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>),
}
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> {
pub fn auth_depth(self) -> usize { self.filter(Component::has_auth).count() }
pub fn path(&self) -> PathDyn<'a> {
match self {
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 crate::path::{AsPath, PathBufDyn, PathDyn, PathDynError, PathKind};
use crate::path::{DynPath, PathBufDyn, PathDyn, PathDynError, PathKind};
#[derive(Debug)]
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> {
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 {
@ -31,14 +31,14 @@ impl From<PathCow<'_>> for PathBufDyn {
}
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<'_> {
fn eq(&self, other: &&str) -> bool {
match self {
Self::Borrowed(s) => s.as_path() == *other,
Self::Owned(s) => s.as_path() == *other,
Self::Borrowed(s) => s.dyn_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::Search => Self::Os,
AuthKind::Mount => Self::Os,
AuthKind::Hub => Self::Os,
AuthKind::Scope => Self::Unix,
AuthKind::Sftp => Self::Unix,
}

View file

@ -2,95 +2,97 @@ use std::borrow::Cow;
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 {
fn as_os(&self) -> Result<&std::path::Path, PathDynError> { self.as_path().as_os() }
pub trait PathLike: DynPath {
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)]
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<'_>)>
where
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>
where
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>
where
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>
where
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>
where
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>
where
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>
where
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 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)]
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> {
fn from(value: &'a PathBufDyn) -> Self { value.as_path() }
fn from(value: &'a PathBufDyn) -> Self { value.dyn_path() }
}
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 {
@ -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) }
}
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<'_> {
fn eq(&self, other: &&str) -> bool {
match *self {
@ -46,7 +52,7 @@ impl PartialEq<&str> 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> {
@ -139,8 +145,8 @@ impl<'p> PathDyn<'p> {
pub fn parent(self) -> Option<Self> {
Some(match self {
Self::Os(p) => Self::Os(p.parent().filter(|p| !p.as_os_str().is_empty())?),
Self::Unix(p) => Self::Unix(p.parent().filter(|p| !p.as_bytes().is_empty())?),
Self::Os(p) => Self::Os(p.parent()?),
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
pub trait AsPathView<'a, T> {
fn as_path_view(self) -> T;
// --- PathView
pub trait PathView<'a, T> {
fn path_view(self) -> T;
}
impl<'a> AsPathView<'a, &'a std::path::Path> for &'a std::path::Path {
fn as_path_view(self) -> &'a std::path::Path { self }
impl<'a> PathView<'a, &'a std::path::Path> for &'a std::path::Path {
fn path_view(self) -> &'a std::path::Path { self }
}
impl<'a> AsPathView<'a, &'a std::path::Path> for &'a std::path::PathBuf {
fn as_path_view(self) -> &'a std::path::Path { self }
impl<'a> PathView<'a, &'a std::path::Path> for &'a std::path::PathBuf {
fn path_view(self) -> &'a std::path::Path { self }
}
impl<'a> AsPathView<'a, &'a typed_path::UnixPath> for &'a typed_path::UnixPath {
fn as_path_view(self) -> &'a typed_path::UnixPath { self }
impl<'a> PathView<'a, &'a typed_path::UnixPath> for &'a typed_path::UnixPath {
fn path_view(self) -> &'a typed_path::UnixPath { self }
}
impl<'a> AsPathView<'a, &'a typed_path::UnixPath> for &'a typed_path::UnixPathBuf {
fn as_path_view(self) -> &'a typed_path::UnixPath { self }
impl<'a> PathView<'a, &'a typed_path::UnixPath> for &'a typed_path::UnixPathBuf {
fn path_view(self) -> &'a typed_path::UnixPath { self }
}

View file

@ -1,17 +1,17 @@
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)]
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) }
}
impl<'a> Encode<'a> {
impl<'a> EncodeSpec<'a> {
pub(crate) fn ports(self) -> impl Display {
struct D<'a>(Encode<'a>);
struct D<'a>(EncodeSpec<'a>);
impl Display for D<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@ -30,7 +30,7 @@ impl<'a> Encode<'a> {
match self.0.0.kind() {
AuthKind::Regular => Ok(()),
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)
}
}
@ -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 {
match self.0 {
Url::Regular(_) => write!(f, "regular://"),
Url::Search { auth, .. }
| Url::Mount { auth, .. }
| Url::Hub { auth, .. }
| Url::Scope { 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 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)]
pub struct Spec {
@ -28,18 +28,23 @@ impl Spec {
// Decode domain and ports
let mut skip = 0;
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)?,
};
// Resolve authority
let auth = Auth::get(&parsed.scheme, &domain)
.ok_or_else(|| anyhow!("unknown VFS authority: {parsed}://{}", Encode::domain(&domain)))?;
let auth = Auth::get(&parsed.scheme, &domain).ok_or_else(|| {
anyhow!("unknown VFS authority: {parsed}://{}", EncodeAuth::domain(&domain))
})?;
// Decode path
let path = Self::decode_path(auth.kind, parsed.tilde, &rest[skip..])?;
let (uri, urn) = Self::normalize_ports(auth.kind, uri, urn, &path)?;
let (path, auth) = if auth.kind == AuthKind::Hub {
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))
}
@ -54,16 +59,13 @@ impl Spec {
fn decode_param<'a>(
bytes: &'a [u8],
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 slash = bytes.get(len).is_some_and(|&b| b == b'/');
*skip += len + slash as usize;
let (uri, urn) = Self::decode_ports(&bytes[..len], &mut len)?;
let domain = match Cow::from(percent_decode(&bytes[..len])) {
Cow::Borrowed(b) => str::from_utf8(b)?.into(),
Cow::Owned(b) => String::from_utf8(b)?.into(),
};
let domain = percent_decode(&bytes[..len]).decode_utf8()?.into();
Ok((domain, uri, urn))
}
@ -90,6 +92,41 @@ impl Spec {
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(
kind: AuthKind,
uri: Option<usize>,
@ -107,7 +144,7 @@ impl Spec {
(uri, urn)
}
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 urn = urn.unwrap_or(path.name().is_some() as usize);
(uri, urn)
@ -118,7 +155,7 @@ impl Spec {
pub fn retrieve_ports(url: Url) -> (usize, usize) {
match url {
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())
}
Url::Scope { loc, .. } | Url::Sftp { loc, .. } => {

View file

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

View file

@ -10,8 +10,9 @@ pub fn init_tests() {
inventory::submit! {
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-hub", _) => Some(Auth::new(AuthKind::Hub, scheme.clone(), domain.clone())),
("test-scope", "aws") => Some(Auth::new(AuthKind::Scope, scheme.clone(), "aws")),
("sftp", "vps") => Some(Auth::new(AuthKind::Sftp, scheme.clone(), "vps")),
_ => None,

View file

@ -5,13 +5,14 @@ use serde::{Deserialize, Serialize, de::{self, IntoDeserializer}};
use yazi_codegen::FromLuaOwned;
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)]
pub enum UrlBuf {
Regular(LocBuf),
Search { 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> },
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::Search { loc, auth } => Self::Search { 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::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::Search { 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::Sftp { loc, .. } => loc.into_inner().into(),
}
@ -156,6 +159,7 @@ impl UrlBuf {
Self::Regular(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::Hub { loc, .. } => loc.try_set_name(name.as_os()?)?,
Self::Scope { 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::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::Hub { .. } => todo!(),
Self::Scope { .. } => todo!(),
Self::Sftp { .. } => todo!(),
}
@ -188,6 +193,18 @@ impl UrlBuf {
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 {
@ -265,10 +282,14 @@ impl<'de> IntoDeserializer<'de, de::value::Error> for &'de UrlBuf {
// --- Tests
#[cfg(test)]
mod tests {
use std::fmt::Debug;
use anyhow::Result;
use super::*;
use crate::url::UrlLike;
use crate::{path::PathKind, url::UrlLike};
fn debug(value: impl Debug) -> String { format!("{value:?}").replace(r"\", "/") }
#[test]
fn test_join() -> anyhow::Result<()> {
@ -293,13 +314,7 @@ mod tests {
for (base, path, expected) in cases {
let base: UrlBuf = base.parse()?;
#[cfg(unix)]
assert_eq!(format!("{:?}", base.try_join(path)?), expected);
#[cfg(windows)]
assert_eq!(
format!("{:?}", base.try_join(path)?).replace(r"\", "/"),
expected.replace(r"\", "/")
);
assert_eq!(debug(base.try_join(path)?), expected);
}
Ok(())
@ -312,6 +327,8 @@ mod tests {
// Regular
("/a", Some("/")),
("/", None),
("a", Some("")),
("", None),
// Search
("search://kw:2:2//a/b/c", Some("search://kw:1:1//a/b")),
("search://kw:1:1//a/b", Some("search://kw//a")),
@ -329,13 +346,16 @@ mod tests {
("sftp://vps//", None),
// Relative
("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),
("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 {
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(())
@ -344,29 +364,166 @@ mod tests {
#[test]
fn test_into_search() -> Result<()> {
crate::init_tests();
const S: char = std::path::MAIN_SEPARATOR;
let u: UrlBuf = "/root".parse()?;
assert_eq!(format!("{u:?}"), "/root");
assert_eq!(debug(&u), "/root");
let u = u.into_search("kw")?;
assert_eq!(format!("{u:?}"), "search://kw//root");
assert_eq!(format!("{:?}", u.parent().unwrap()), "/");
assert_eq!(debug(&u), "search://kw//root");
assert_eq!(debug(u.parent().unwrap()), "/");
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")?;
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();
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();
assert_eq!(format!("{u:?}"), "search://kw//root");
assert_eq!(debug(u), "search://kw//root");
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(())
}

View file

@ -1,11 +1,12 @@
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)]
pub struct Components<'a> {
inner: path::Components<'a>,
url: Url<'a>,
auth_yields: usize,
back_yields: usize,
scheme_yielded: bool,
}
@ -15,6 +16,7 @@ impl<'a> From<Url<'a>> for Components<'a> {
Self {
inner: value.loc().components(),
url: value,
auth_yields: 0,
back_yields: 0,
scheme_yielded: false,
}
@ -83,6 +85,10 @@ impl<'a> Components<'a> {
Url::Mount { 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 { 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> {
fn next_back(&mut self) -> Option<Self::Item> {
if let Some(c) = self.inner.next_back() {
self.auth_yields += c.has_auth() as usize;
self.back_yields += 1;
Some(c.into())
} else if !self.scheme_yielded {

View file

@ -1,16 +1,17 @@
use std::{borrow::Cow, hash::{Hash, Hasher}, path::PathBuf, sync::Arc};
use anyhow::Result;
use anyhow::{Result, ensure};
use serde::{Deserialize, Deserializer, Serialize};
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)]
pub enum UrlCow<'a> {
Regular(LocCow<'a>),
Search { 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> },
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::Search { loc, auth } => Self::Search { 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::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::Search { loc, auth } => Self::Search { 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::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> {
let Spec { auth, uri, urn } = spec;
validate_auth_depth(&auth, path)?;
Ok(match auth.kind {
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::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::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> {
let Spec { auth, uri, urn } = spec;
validate_auth_depth(&auth, path.dyn_path())?;
Ok(match auth.kind {
AuthKind::Regular => {
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(),
auth,
},
AuthKind::Hub => {
Self::Hub { loc: LocBuf::<PathBuf>::with(path.try_into()?, uri, urn)?.into(), auth }
}
AuthKind::Scope => {
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::Search { loc, .. } => loc.is_owned(),
Self::Mount { loc, .. } => loc.is_owned(),
Self::Hub { loc, .. } => loc.is_owned(),
Self::Scope { 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::Search { loc, auth } => UrlBuf::Search { 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::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 (auth, path) = match self {
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()),
};
(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)]
mod tests {
use super::*;

View file

@ -2,7 +2,7 @@ use std::borrow::Cow;
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>);
@ -33,7 +33,7 @@ impl<'de, 'a: 'de> Deserializer<'de> for UrlDeserializer<'a> {
struct MapDeserializer<'a> {
kind: Option<&'static str>,
scheme: Option<Symbol<str>>,
domain: Option<Symbol<str>>,
domain: Option<Domain<'static>>,
uri: Option<usize>,
urn: Option<usize>,
path: Option<Cow<'a, [u8]>>,
@ -47,7 +47,7 @@ impl<'a> MapDeserializer<'a> {
Self {
kind: Some(spec.kind.into()),
scheme: Some(spec.scheme.intern()),
domain: Some(spec.domain.intern()),
domain: Some(spec.domain.clone()),
uri: Some(uri),
urn: Some(urn),
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>);
@ -6,7 +6,7 @@ impl std::fmt::Display for Display<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let (kind, loc) = (self.0.kind(), self.0.loc());
if kind.is_virtual() {
Encode(self.0).fmt(f)?;
EncodeSpec(self.0).fmt(f)?;
}
loc.display().fmt(f)
}

View file

@ -1,24 +1,36 @@
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)]
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<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use crate::spec::Encode as E;
let loc = percent_encode(self.0.loc().encoded_bytes(), CONTROLS);
let loc = Self::loc(self.0.loc().encoded_bytes());
match self.0 {
Url::Regular(_) => write!(f, "regular~://{loc}"),
Url::Search { auth, .. }
| Url::Mount { auth, .. }
| Url::Hub { auth, .. }
| Url::Scope { 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 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
where
@ -22,6 +22,8 @@ where
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_root(&self) -> bool { self.as_url().has_root() }
@ -44,6 +46,8 @@ where
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 spec(&self) -> Spec { self.as_url().spec() }
@ -62,8 +66,8 @@ where
self.as_url().try_join(path)
}
fn try_replace<'a>(&self, take: usize, path: impl AsPathRef<'a>) -> Result<UrlCow<'a>> {
self.as_url().try_replace(take, path)
fn try_replace<'a>(&self, take: usize, to: impl DynPathRef<'a>) -> Result<UrlCow<'a>> {
self.as_url().try_replace(take, to)
}
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| {
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::ToString, |lua, me, ()| {

View file

@ -42,6 +42,7 @@ impl AsUrl for UrlBuf {
Self::Regular(loc) => Url::Regular(loc.as_loc()),
Self::Search { loc, auth } => Url::Search { 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::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::Search { loc, auth } => Url::Search { 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::Sftp { loc, auth } => Url::Sftp { loc: loc.as_loc(), auth },
}

View file

@ -5,13 +5,14 @@ use hashbrown::Equivalent;
use serde::Serialize;
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)]
pub enum Url<'a> {
Regular(Loc<'a>),
Search { 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> },
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::Search { auth, .. }
| Self::Mount { auth, .. }
| Self::Hub { auth, .. }
| Self::Scope { 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]
pub fn as_regular(self) -> Result<Self, PathDynError> {
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::Search { loc, auth } => Self::Search { 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::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::Search { 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::Sftp { loc, .. } => loc.extension()?.as_strand(),
})
@ -106,6 +123,7 @@ impl<'a> Url<'a> {
Self::Regular(loc) => loc.has_base(),
Self::Search { loc, .. } => loc.has_base(),
Self::Mount { loc, .. } => loc.has_base(),
Self::Hub { loc, .. } => loc.has_base(),
Self::Scope { 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::Search { loc, .. } => loc.has_trail(),
Self::Mount { loc, .. } => loc.has_trail(),
Self::Hub { loc, .. } => loc.has_trail(),
Self::Scope { loc, .. } => loc.has_trail(),
Self::Sftp { loc, .. } => loc.has_trail(),
}
@ -140,11 +159,12 @@ impl<'a> Url<'a> {
#[inline]
pub fn loc(self) -> PathDyn<'a> {
match self {
Self::Regular(loc) => loc.as_path(),
Self::Search { loc, .. } => loc.as_path(),
Self::Mount { loc, .. } => loc.as_path(),
Self::Scope { loc, .. } => loc.as_path(),
Self::Sftp { loc, .. } => loc.as_path(),
Self::Regular(loc) => loc.dyn_path(),
Self::Search { loc, .. } => loc.dyn_path(),
Self::Mount { loc, .. } => loc.dyn_path(),
Self::Hub { loc, .. } => loc.dyn_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::Search { 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::Sftp { loc, .. } => loc.file_name()?.as_strand(),
})
@ -165,6 +186,19 @@ impl<'a> Url<'a> {
#[inline]
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> {
let uri = self.uri();
@ -187,6 +221,11 @@ impl<'a> Url<'a> {
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
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::Search { auth, .. }
| Self::Mount { auth, .. }
| Self::Hub { auth, .. }
| Self::Scope { auth, .. }
| Self::Sftp { auth, .. } => auth.clone(),
};
@ -219,6 +259,7 @@ impl<'a> Url<'a> {
Self::Regular(loc) => loc.file_stem()?.as_strand(),
Self::Search { 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::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::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::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>) {
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();
(base.as_path(), rest.as_path(), urn.as_path())
(base.dyn_path(), rest.dyn_path(), urn.dyn_path())
}
Self::Scope { loc, .. } | Self::Sftp { loc, .. } => {
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> {
let path = path.as_strand();
let joined = self.loc().try_join(path)?;
Ok(match self {
@ -300,6 +350,11 @@ impl<'a> Url<'a> {
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, .. } => {
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>> {
self.try_replace_impl(take, to.as_path_ref())
pub fn try_replace<'b>(self, take: usize, to: impl DynPathRef<'b>) -> Result<UrlCow<'b>> {
self.try_replace_impl(take, to.dyn_path_ref())
}
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()),
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 {
loc: LocBuf::<typed_path::UnixPathBuf>::new(path.into_unix()?, loc.base(), loc.trail()),
auth: auth.clone(),
@ -354,6 +413,10 @@ impl<'a> Url<'a> {
loc: LocBuf::<std::path::PathBuf>::saturated(path.into_os()?, self.kind()),
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 {
loc: LocBuf::<typed_path::UnixPathBuf>::saturated(path.into_unix()?, self.kind()),
auth: auth.clone(),
@ -367,10 +430,12 @@ impl<'a> Url<'a> {
Ok(url.into())
}
#[inline]
pub fn try_starts_with(self, base: impl AsUrl) -> Result<bool, StartsWithError> {
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> {
@ -379,7 +444,7 @@ impl<'a> Url<'a> {
let base = base.as_url();
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);
}
@ -420,22 +485,24 @@ impl<'a> Url<'a> {
#[inline]
pub fn uri(self) -> PathDyn<'a> {
match self {
Self::Regular(loc) => loc.uri().as_path(),
Self::Search { loc, .. } => loc.uri().as_path(),
Self::Mount { loc, .. } => loc.uri().as_path(),
Self::Scope { loc, .. } => loc.uri().as_path(),
Self::Sftp { loc, .. } => loc.uri().as_path(),
Self::Regular(loc) => loc.uri().dyn_path(),
Self::Search { loc, .. } => loc.uri().dyn_path(),
Self::Mount { loc, .. } => loc.uri().dyn_path(),
Self::Hub { loc, .. } => loc.uri().dyn_path(),
Self::Scope { loc, .. } => loc.uri().dyn_path(),
Self::Sftp { loc, .. } => loc.uri().dyn_path(),
}
}
#[inline]
pub fn urn(self) -> PathDyn<'a> {
match self {
Self::Regular(loc) => loc.urn().as_path(),
Self::Search { loc, .. } => loc.urn().as_path(),
Self::Mount { loc, .. } => loc.urn().as_path(),
Self::Scope { loc, .. } => loc.urn().as_path(),
Self::Sftp { loc, .. } => loc.urn().as_path(),
Self::Regular(loc) => loc.urn().dyn_path(),
Self::Search { loc, .. } => loc.urn().dyn_path(),
Self::Mount { loc, .. } => loc.urn().dyn_path(),
Self::Hub { loc, .. } => loc.urn().dyn_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 => {
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()
}
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()) {
(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
}
(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 attrs = attrs.into();
if from.auth().covariant(to.auth()) {
if from.auth().same_service(to.auth()) {
let engine = Engines::new(from).await?;
if engine.capabilities().await?.copy_progressive {
return engine.copy_progressive(to.loc(), attrs);
@ -119,7 +119,7 @@ where
V: AsUrl,
{
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
} else {
Err(io::Error::from(io::ErrorKind::CrossesDevices))
@ -208,7 +208,7 @@ where
V: AsUrl,
{
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
} else {
Err(io::Error::from(io::ErrorKind::CrossesDevices))
@ -268,7 +268,9 @@ where
let url = url.into();
match url.as_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 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)]
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>
where
P: AsPath,
P: DynPath,
{
match self {
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>>>
where
P: AsPath,
P: DynPath,
A: Into<Attrs>,
{
match self {
@ -107,7 +107,7 @@ impl<'a> Engine for Engines<'a> {
async fn hard_link<P>(&self, to: P) -> io::Result<()>
where
P: AsPath,
P: DynPath,
{
match self {
Self::Local(p) => p.hard_link(to).await,
@ -129,7 +129,7 @@ impl<'a> Engine for Engines<'a> {
Ok(match url.kind() {
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?),
})
}
@ -184,7 +184,7 @@ impl<'a> Engine for Engines<'a> {
async fn rename<P>(&self, to: P) -> io::Result<()>
where
P: AsPath,
P: DynPath,
{
match self {
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_fs::{cha::Cha, engine::{Attrs, Capabilities, Engine}, file::Files};
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;
@ -48,23 +48,23 @@ impl<'a> Engine for Lua<'a> {
async fn copy<P>(&self, to: P, attrs: Attrs) -> io::Result<u64>
where
P: AsPath,
P: DynPath,
{
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?)
}
fn copy_progressive<P, A>(&self, to: P, attrs: A) -> io::Result<mpsc::Receiver<io::Result<u64>>>
where
P: AsPath,
P: DynPath,
A: Into<Attrs>,
{
let (tx, rx) = mpsc::channel(20);
let job = ProviderJob::CopyProgressive {
from: self.url.to_owned(),
to: to.as_path().to_owned(),
to: to.dyn_path().to_owned(),
attrs: attrs.into(),
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<()>
where
P: AsPath,
P: DynPath,
{
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()?)
}
@ -103,7 +103,7 @@ impl<'a> Engine for Lua<'a> {
}
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(
io::ErrorKind::InvalidInput,
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<()>
where
P: AsPath,
P: DynPath,
{
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()?)
}

View file

@ -4,7 +4,7 @@ use tokio::{io::{AsyncWriteExt, BufReader, BufWriter}, sync::mpsc::Receiver};
use yazi_config::vfs::{ServiceSftp, Vfs};
use yazi_fs::engine::{Capabilities, DirReader, Engine, FileHolder};
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 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>
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 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>>>
where
P: AsPath,
P: DynPath,
A: Into<yazi_fs::engine::Attrs>,
{
let to = UrlBuf::Sftp {
loc: LocBuf::<typed_path::UnixPathBuf>::saturated(
to.as_path().to_unix_owned()?,
to.dyn_path().to_unix_owned()?,
AuthKind::Sftp,
),
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<()>
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?)
}
@ -176,9 +176,9 @@ impl<'a> Engine for Sftp<'a> {
async fn rename<P>(&self, to: P) -> io::Result<()>
where
P: AsPath,
P: DynPath,
{
let to = to.as_path().as_unix()?;
let to = to.dyn_path().as_unix()?;
let op = self.op().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());
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 {
ops.push(FilesOp::Deleting(parent.into(), [urn.into()].into()));
ops.push(FilesOp::Deleting(parent.into(), [key.into()].into()));
continue;
};
if let Some(p) = file.url.as_local()
&& !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;
}
ops.push(FilesOp::Upserting(parent.into(), [(urn.into(), file)].into()));
ops.push(FilesOp::Upserting(parent.into(), [(key.into(), file)].into()));
}
FilesOp::mutate(ops);

View file

@ -39,16 +39,16 @@ impl Remote {
let mut ups = Vec::with_capacity(urls.len());
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 {
ops.push(FilesOp::Deleting(parent.into(), [urn.into()].into()));
ops.push(FilesOp::Deleting(parent.into(), [key.into()].into()));
continue;
};
let is_file = file.is_file();
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 {
ups.push(u);
}

View file

@ -21,7 +21,7 @@ impl Reporter {
for url in urls.into_iter().map(Into::into) {
match url.as_url().kind() {
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),
}
}

View file

@ -59,7 +59,7 @@ impl Watched {
AuthKind::Mount => UrlBuf::Mount { loc: path.into_os().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::Regular | AuthKind::Search => return None,
AuthKind::Regular | AuthKind::Search | AuthKind::Hub => return None,
};
if self.contains_url(&url) { Some(url) } else { None }
}