perf: lock-free hashing for string interns (#3091)

This commit is contained in:
三咲雅 misaki masa 2025-08-21 09:40:52 +08:00 committed by GitHub
parent 84907297a0
commit 0054cf0b87
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 204 additions and 131 deletions

26
Cargo.lock generated
View file

@ -364,9 +364,9 @@ dependencies = [
[[package]]
name = "cfg-if"
version = "1.0.1"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
[[package]]
name = "chrono"
@ -1967,9 +1967,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]]
name = "quick-xml"
version = "0.38.1"
version = "0.38.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9845d9dccf565065824e69f9f235fafba1587031eda353c1f1561cd6a6be78f4"
checksum = "d200a41a7797e6461bd04e4e95c3347053a731c32c87f066f2f0dda22dbdbba8"
dependencies = [
"memchr",
]
@ -2153,7 +2153,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
dependencies = [
"getrandom 0.2.16",
"libredox",
"thiserror 2.0.15",
"thiserror 2.0.16",
]
[[package]]
@ -2574,11 +2574,11 @@ dependencies = [
[[package]]
name = "thiserror"
version = "2.0.15"
version = "2.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80d76d3f064b981389ecb4b6b7f45a0bf9fdac1d5b9204c7bd6714fecc302850"
checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0"
dependencies = [
"thiserror-impl 2.0.15",
"thiserror-impl 2.0.16",
]
[[package]]
@ -2594,9 +2594,9 @@ dependencies = [
[[package]]
name = "thiserror-impl"
version = "2.0.15"
version = "2.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d29feb33e986b6ea906bd9c3559a856983f92371b3eaa5e83782a351623de0"
checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960"
dependencies = [
"proc-macro2",
"quote",
@ -3153,11 +3153,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.9"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.60.2",
]
[[package]]

View file

@ -78,13 +78,13 @@ impl Cd {
Ok(s) => {
let Ok(url) = UrlBuf::try_from(s).map(expand_url) else { return };
let Ok(file) = File::new(url.clone()).await else { return };
let Ok(file) = File::new(&url).await else { return };
if file.is_dir() {
return MgrProxy::cd(&url);
}
if let Some(p) = url.parent_url() {
FilesOp::Upserting(p, [(url.urn_owned(), file)].into()).emit();
FilesOp::Upserting(p.into(), [(url.urn_owned(), file)].into()).emit();
}
MgrProxy::reveal(&url);
}

View file

@ -48,7 +48,7 @@ impl Create {
provider::create_dir_all(&new).await?;
} else if let Some(real) = realname(&new).await {
ok_or_not_found(provider::remove_file(&new).await)?;
FilesOp::Deleting(parent.clone(), [UrnBuf::from(real)].into()).emit();
FilesOp::Deleting(parent.to_owned(), [UrnBuf::from(real)].into()).emit();
provider::create(&new).await?;
} else {
provider::create_dir_all(&parent).await.ok();
@ -56,8 +56,8 @@ impl Create {
provider::create(&new).await?;
}
if let Ok(f) = File::new(new.clone()).await {
FilesOp::Upserting(parent, [(f.urn_owned(), f)].into()).emit();
if let Ok(f) = File::new(&new).await {
FilesOp::Upserting(parent.into(), [(f.urn_owned(), f)].into()).emit();
MgrProxy::reveal(&new)
}
Ok(())

View file

@ -55,7 +55,7 @@ impl Actor for Open {
tokio::spawn(async move {
let mut files = Vec::with_capacity(todo.len());
for i in todo {
if let Ok(f) = File::new(targets[i].0.clone()).await {
if let Ok(f) = File::new(&targets[i].0).await {
files.push(f);
}
}

View file

@ -69,15 +69,15 @@ impl Rename {
if let Some(o) = overwritten {
ok_or_not_found(provider::rename(&p_new.join(&o), &new).await)?;
FilesOp::Deleting(p_new.clone(), [UrnBuf::from(o)].into()).emit();
FilesOp::Deleting(p_new.to_owned(), [UrnBuf::from(o)].into()).emit();
}
let file = File::new(new.clone()).await?;
let file = File::new(&new).await?;
if p_new == p_old {
FilesOp::Upserting(p_old, [(n_old, file)].into()).emit();
FilesOp::Upserting(p_old.into(), [(n_old, file)].into()).emit();
} else {
FilesOp::Deleting(p_old, [n_old].into()).emit();
FilesOp::Upserting(p_new, [(n_new, file)].into()).emit();
FilesOp::Deleting(p_old.into(), [n_old].into()).emit();
FilesOp::Upserting(p_new.into(), [(n_new, file)].into()).emit();
}
MgrProxy::reveal(&new);

View file

@ -26,7 +26,7 @@ impl Actor for Reveal {
// If the child is not hovered, which means it doesn't exist,
// create a dummy file
if !opt.no_dummy && tab.hovered().is_none_or(|f| &child != f.urn()) {
let op = FilesOp::Creating(parent, vec![File::from_dummy(opt.target, None)]);
let op = FilesOp::Creating(parent.into(), vec![File::from_dummy(opt.target, None)]);
tab.current.update_pub(tab.id, op);
}

View file

@ -32,7 +32,7 @@ impl Boot {
};
if provider::metadata(&entry).await.is_ok_and(|m| m.is_file()) {
(parent, child)
(parent.into(), child)
} else {
(entry, UrnBuf::default())
}

View file

@ -17,13 +17,15 @@ impl DerefMut for Linked {
}
impl Linked {
pub fn from_dir<'a, 'b>(&'a self, url: &'b UrlBuf) -> Box<dyn Iterator<Item = &'a UrlBuf> + 'b>
pub fn from_dir<'a, 'b, T>(&'a self, url: T) -> Box<dyn Iterator<Item = &'a UrlBuf> + 'b>
where
'a: 'b,
T: Into<Url<'b>>,
{
let url = url.into();
if url.scheme.is_virtual() {
Box::new(iter::empty())
} else if let Some(to) = self.get(url) {
} else if let Some(to) = self.get(&url) {
Box::new(self.iter().filter(move |(k, v)| *v == to && *k != url).map(|(k, _)| k))
} else {
Box::new(self.iter().filter(move |(_, v)| *v == url).map(|(k, _)| k))
@ -34,7 +36,7 @@ impl Linked {
if url.scheme.is_virtual() {
vec![]
} else if let Some((parent, urn)) = url.pair() {
self.from_dir(&parent).map(|u| u.join(&urn)).collect()
self.from_dir(parent).map(|u| u.join(&urn)).collect()
} else {
vec![]
}

View file

@ -68,9 +68,12 @@ impl Watcher {
if !watched.contains(&p) && !LINKED.read().from_dir(&p).any(|u| watched.contains(u)) {
continue;
}
out_tx.send(u).ok();
if !parents.contains(&p) {
out_tx.send(p.clone()).ok();
if parents.contains(&p) {
out_tx.send(u).ok();
} else {
let p = p.to_owned();
out_tx.send(u).ok();
out_tx.send(p.to_owned()).ok();
parents.insert(p);
}
}
@ -134,8 +137,8 @@ impl Watcher {
for u in urls {
let Some((parent, urn)) = u.pair() else { continue };
let Ok(file) = File::new(u).await else {
ops.push(FilesOp::Deleting(parent, [urn].into()));
let Ok(file) = File::new(&u).await else {
ops.push(FilesOp::Deleting(parent.into(), [urn].into()));
continue;
};
@ -144,11 +147,11 @@ impl Watcher {
|| realname_unchecked(u, &mut cached).await.is_ok_and(|s| urn.as_urn() == s);
if !eq {
ops.push(FilesOp::Deleting(parent, [urn].into()));
ops.push(FilesOp::Deleting(parent.into(), [urn].into()));
continue;
}
ops.push(FilesOp::Upserting(parent, [(urn, file)].into()));
ops.push(FilesOp::Upserting(parent.into(), [(urn, file)].into()));
}
FilesOp::mutate(ops);

View file

@ -72,7 +72,7 @@ impl Selected {
self.inner.extend(urls.iter().enumerate().map(|(i, u)| (u.into(), now + i as u64)));
for u in parents {
*self.parents.entry(UrlBufCov(u)).or_insert(0) += self.inner.len() - len;
*self.parents.entry_ref(&UrlCov::new(u)).or_default() += self.inner.len() - len;
}
urls.len()
}

View file

@ -1,7 +1,7 @@
use std::{ffi::OsStr, fs::{FileType, Metadata}, hash::{BuildHasher, Hash, Hasher}, ops::Deref};
use anyhow::Result;
use yazi_shared::url::{Uri, UrlBuf, Urn, UrnBuf};
use yazi_shared::url::{Uri, UrlBuf, UrlCow, Urn, UrnBuf};
use crate::{cha::Cha, provider};
@ -21,9 +21,10 @@ impl Deref for File {
impl File {
#[inline]
pub async fn new(url: UrlBuf) -> Result<Self> {
pub async fn new(url: impl Into<UrlCow<'_>>) -> Result<Self> {
let url = url.into();
let meta = provider::symlink_metadata(&url).await?;
Ok(Self::from_follow(url, meta).await)
Ok(Self::from_follow(url.into_owned(), meta).await)
}
#[inline]

View file

@ -55,9 +55,9 @@ impl FilesOp {
let Some(o_p) = o.parent_url() else { continue };
let Some(n_p) = n.url.parent_url() else { continue };
if o_p != n_p {
parents.entry(o_p).or_default().0.insert(o.urn_owned());
parents.entry_ref(&o_p).or_default().0.insert(o.urn_owned());
}
parents.entry(n_p).or_default().1.insert(n.urn_owned(), n);
parents.entry_ref(&n_p).or_default().1.insert(n.urn_owned(), n);
}
for (p, (o, n)) in parents {
match (o.is_empty(), n.is_empty()) {
@ -124,7 +124,7 @@ impl FilesOp {
} else if maybe_exists(cwd).await {
Self::IOErr(cwd.clone(), kind).emit();
} else if let Some((p, n)) = cwd.pair() {
Self::Deleting(p, [n].into()).emit();
Self::Deleting(p.into(), [n].into()).emit();
}
}

View file

@ -1,6 +1,6 @@
use mlua::{ExternalError, FromLua, IntoLua, Lua, Value};
use yazi_fs::path::expand_url;
use yazi_shared::{event::CmdCow, url::{UrlBuf, UrlCow}};
use yazi_shared::{event::CmdCow, url::{Url, UrlBuf, UrlCow}};
#[derive(Debug)]
pub struct CdOpt {
@ -33,6 +33,10 @@ impl From<(UrlBuf, CdSource)> for CdOpt {
}
}
impl From<(Url<'_>, CdSource)> for CdOpt {
fn from((target, source): (Url, CdSource)) -> Self { Self::from((target.to_owned(), source)) }
}
impl FromLua for CdOpt {
fn from_lua(_: Value, _: &Lua) -> mlua::Result<Self> { Err("unsupported".into_lua_err()) }
}

View file

@ -92,7 +92,7 @@ impl Prework {
let parent = buf[0].0.parent_url().unwrap();
FilesOp::Size(
parent,
parent.into(),
HashMap::from_iter(buf.into_iter().map(|(u, s)| (u.urn_owned(), s))),
)
.emit();

View file

@ -85,16 +85,19 @@ impl LocBuf {
}
pub fn zeroed(path: impl Into<PathBuf>) -> Self {
let mut loc = Self::from(path.into());
(loc.uri, loc.urn) = (0, 0);
loc
let loc = Self::from(path.into());
let Loc { inner, uri, urn } = Loc::zeroed(&loc.inner);
debug_assert!(inner.as_os_str() == loc.inner.as_os_str());
Self { inner: loc.inner, uri, urn }
}
pub fn floated(path: impl Into<PathBuf>, base: &Path) -> Self {
let mut loc = Self::from(path.into());
loc.uri =
loc.inner.strip_prefix(base).expect("Loc must start with the given base").as_os_str().len();
loc
let loc = Self::from(path.into());
let Loc { inner, uri, urn } = Loc::floated(&loc.inner, base);
debug_assert!(inner.as_os_str() == loc.inner.as_os_str());
Self { inner: loc.inner, uri, urn }
}
#[inline]

View file

@ -110,9 +110,25 @@ impl<'a> Loc<'a> {
Ok(loc)
}
pub fn zeroed<T: AsRef<Path> + ?Sized>(path: &'a T) -> Self {
let mut loc = Self::from(path.as_ref());
(loc.uri, loc.urn) = (0, 0);
loc
}
pub fn floated<T: AsRef<Path> + ?Sized>(path: &'a T, base: &Path) -> Self {
let mut loc = Self::from(path.as_ref());
loc.uri =
loc.inner.strip_prefix(base).expect("Loc must start with the given base").as_os_str().len();
loc
}
#[inline]
pub fn as_loc(self) -> Loc<'a> { self }
#[inline]
pub fn as_path(self) -> &'a Path { self.inner }
#[inline]
pub fn uri(self) -> &'a Uri {
Uri::new(unsafe {
@ -161,6 +177,9 @@ impl<'a> Loc<'a> {
#[inline]
pub fn name(self) -> &'a OsStr { self.inner.file_name().unwrap_or(OsStr::new("")) }
#[inline]
pub fn parent(self) -> Option<&'a Path> { self.inner.parent() }
#[inline]
pub fn triple(self) -> (&'a Path, &'a Path, &'a Path) {
let len = self.bytes().len();

View file

@ -1,6 +1,13 @@
yazi_macro::mod_flat!(pool ptr symbol traits);
static SYMBOLS: crate::RoCell<parking_lot::Mutex<hashbrown::HashMap<SymbolPtr, u64>>> =
crate::RoCell::new();
static SYMBOLS: crate::RoCell<
parking_lot::Mutex<hashbrown::HashMap<SymbolPtr, u64, foldhash::fast::FixedState>>,
> = crate::RoCell::new();
pub(super) fn init() { SYMBOLS.with(<_>::default); }
#[inline]
pub(super) fn compute_hash<T: std::hash::Hash>(value: T) -> u64 {
use core::hash::BuildHasher;
foldhash::fast::FixedState::default().hash_one(value)
}

View file

@ -1,6 +1,8 @@
use std::marker::PhantomData;
use crate::pool::{SYMBOLS, Symbol, SymbolPtr};
use hashbrown::hash_map::RawEntryMut;
use crate::pool::{SYMBOLS, Symbol, SymbolPtr, compute_hash};
pub struct Pool<T: ?Sized> {
_phantom: PhantomData<T>,
@ -8,18 +10,23 @@ pub struct Pool<T: ?Sized> {
impl Pool<[u8]> {
pub fn intern(value: &[u8]) -> Symbol<[u8]> {
let mut lock = SYMBOLS.lock();
let hash = compute_hash(value);
if let Some((ptr, count)) = lock.get_key_value_mut(value) {
*count += 1;
return Symbol::new(ptr.clone());
match SYMBOLS.lock().raw_entry_mut().from_key_hashed_nocheck(hash, value) {
RawEntryMut::Occupied(mut oe) => {
let (ptr, count) = oe.get_key_value_mut();
*count += 1;
Symbol::new(ptr.clone())
}
RawEntryMut::Vacant(ve) => {
let boxed = value.to_vec().into_boxed_slice();
let ptr = SymbolPtr::leaked(Box::leak(boxed));
ve.insert_hashed_nocheck(hash, ptr.clone(), 1);
Symbol::new(ptr)
}
}
let boxed = value.to_vec().into_boxed_slice();
let ptr = SymbolPtr::leaked(Box::leak(boxed));
lock.insert(ptr.clone(), 1);
Symbol::new(ptr)
}
}

View file

@ -1,6 +1,8 @@
use std::{hash::{Hash, Hasher}, marker::PhantomData, mem::ManuallyDrop, ops::Deref, str};
use crate::pool::{Pool, SYMBOLS, SymbolPtr};
use hashbrown::hash_map::RawEntryMut;
use crate::pool::{Pool, SYMBOLS, SymbolPtr, compute_hash};
pub struct Symbol<T: ?Sized> {
ptr: SymbolPtr,
@ -13,22 +15,29 @@ unsafe impl<T: ?Sized> Sync for Symbol<T> {}
impl<T: ?Sized> Clone for Symbol<T> {
fn clone(&self) -> Self {
*SYMBOLS.lock().get_mut(&self.ptr).unwrap() += 1;
let hash = compute_hash(&self.ptr);
match SYMBOLS.lock().raw_entry_mut().from_key_hashed_nocheck(hash, &self.ptr) {
RawEntryMut::Occupied(mut oe) => *oe.get_mut() += 1,
RawEntryMut::Vacant(_) => unreachable!(),
}
Symbol::new(self.ptr.clone())
}
}
impl<T: ?Sized> Drop for Symbol<T> {
fn drop(&mut self) {
let mut lock = SYMBOLS.lock();
let count = lock.get_mut(&self.ptr).unwrap();
let hash = compute_hash(&self.ptr);
match SYMBOLS.lock().raw_entry_mut().from_key_hashed_nocheck(hash, &self.ptr) {
RawEntryMut::Occupied(mut oe) => {
let count = oe.get_mut();
*count -= 1;
*count -= 1;
if *count == 0 {
lock.remove(&self.ptr);
unsafe {
drop(Box::from_raw(self.ptr.as_ptr()));
if *count == 0 {
oe.remove();
drop(unsafe { Box::from_raw(self.ptr.as_ptr()) });
}
}
RawEntryMut::Vacant(_) => unreachable!(),
}
}
}
@ -38,7 +47,7 @@ impl AsRef<[u8]> for Symbol<[u8]> {
}
impl AsRef<str> for Symbol<str> {
fn as_ref(&self) -> &str { unsafe { str::from_utf8_unchecked(self.ptr.as_ref()) } }
fn as_ref(&self) -> &str { unsafe { str::from_utf8_unchecked(self.ptr.bytes()) } }
}
impl Deref for Symbol<[u8]> {
@ -74,19 +83,6 @@ impl<T: ?Sized> Hash for Symbol<T> {
fn hash<H: Hasher>(&self, state: &mut H) { self.ptr.as_ptr().hash(state); }
}
// --- PartialOrd
impl PartialOrd for Symbol<[u8]> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
self.as_ref().partial_cmp(other.as_ref())
}
}
impl PartialOrd for Symbol<str> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
self.as_ref().partial_cmp(other.as_ref())
}
}
// --- Ord
impl Ord for Symbol<[u8]> {
fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.as_ref().cmp(other.as_ref()) }
@ -96,6 +92,15 @@ impl Ord for Symbol<str> {
fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.as_ref().cmp(other.as_ref()) }
}
// --- PartialOrd
impl PartialOrd for Symbol<[u8]> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { Some(self.cmp(other)) }
}
impl PartialOrd for Symbol<str> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { Some(self.cmp(other)) }
}
// --- Debug
impl std::fmt::Debug for Symbol<[u8]> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {

View file

@ -31,7 +31,7 @@ impl From<&Url<'_>> for UrlBuf {
}
impl From<Url<'_>> for UrlBuf {
fn from(value: Url<'_>) -> Self { Self::from(&value) }
fn from(url: Url<'_>) -> Self { Self { loc: url.loc.into(), scheme: url.scheme } }
}
impl From<&UrlBuf> for UrlBuf {
@ -91,35 +91,23 @@ impl PartialEq<Url<'_>> for &UrlBuf {
}
impl UrlBuf {
pub fn join(&self, path: impl AsRef<Path>) -> Self {
use Scheme as S;
let join = self.loc.join(path);
let loc = match self.scheme {
S::Regular => join.into(),
S::Search(_) => LocBuf::new(join, self.loc.base(), self.loc.base()),
S::Archive(_) => LocBuf::floated(join, self.loc.base()),
S::Sftp(_) => join.into(),
};
Self { loc, scheme: self.scheme.clone() }
}
#[inline]
pub fn join(&self, path: impl AsRef<Path>) -> Self { self.as_url().join(path) }
#[inline]
pub fn components(&self) -> Components<'_> { Components::from(self) }
#[inline]
pub fn covariant(&self, other: &Self) -> bool { self.as_url().covariant(other.as_url()) }
pub fn os_str(&self) -> Cow<'_, OsStr> { self.components().os_str() }
#[inline]
pub fn display(&self) -> Display<'_> { Display::new(self) }
#[inline]
pub fn os_str(&self) -> Cow<'_, OsStr> { self.components().os_str() }
pub fn covariant(&self, other: &Self) -> bool { self.as_url().covariant(other.as_url()) }
#[inline]
pub fn parent_url(&self) -> Option<UrlBuf> { self.as_url().parent_url() }
pub fn parent_url(&self) -> Option<Url<'_>> { self.as_url().parent_url() }
pub fn strip_prefix(&self, base: impl AsRef<UrlBuf>) -> Option<&Urn> {
use Scheme as S;
@ -172,8 +160,11 @@ impl UrlBuf {
Self { loc: self.loc.rebase(base), scheme: self.scheme.clone() }
}
// TODO: use Urn instead of UrlBuf
#[inline]
pub fn pair(&self) -> Option<(Self, UrnBuf)> { Some((self.parent_url()?, self.loc.urn_owned())) }
pub fn pair(&self) -> Option<(Url<'_>, UrnBuf)> {
Some((self.parent_url()?, self.loc.urn_owned()))
}
#[inline]
pub fn hash_u64(&self) -> u64 { foldhash::fast::FixedState::default().hash_one(self) }
@ -193,9 +184,7 @@ impl UrlBuf {
pub fn is_regular(&self) -> bool { self.as_url().is_regular() }
#[inline]
pub fn to_regular(&self) -> Self {
Self { loc: self.loc.to_path().into(), scheme: Scheme::Regular }
}
pub fn to_regular(&self) -> Self { self.as_url().into_regular().into() }
#[inline]
pub fn into_regular(mut self) -> Self {
@ -243,9 +232,7 @@ impl UrlBuf {
}
impl Debug for UrlBuf {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}{}", Encode::from(self), self.loc.display())
}
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { self.as_url().fmt(f) }
}
impl Serialize for UrlBuf {

View file

@ -62,6 +62,10 @@ impl From<UrlCow<'_>> for UrlBufCov {
fn from(value: UrlCow<'_>) -> Self { Self(value.into_owned()) }
}
impl From<Url<'_>> for UrlBufCov {
fn from(value: Url<'_>) -> Self { Self(value.to_owned()) }
}
impl From<&UrlCov<'_>> for UrlBufCov {
fn from(value: &UrlCov<'_>) -> Self { Self(UrlBuf::from(&value.0)) }
}
@ -87,5 +91,5 @@ impl UrlBufCov {
pub fn as_url(&self) -> UrlCov<'_> { UrlCov::from(self) }
#[inline]
pub fn parent_url(&self) -> Option<UrlBufCov> { self.0.parent_url().map(UrlBufCov) }
pub fn parent_url(&self) -> Option<UrlBufCov> { self.0.parent_url().map(Into::into) }
}

View file

@ -118,10 +118,10 @@ impl UrlCow<'_> {
pub fn into_owned(self) -> UrlBuf { self.as_url().into() }
#[inline]
pub fn parent_url(&self) -> Option<UrlBuf> { self.as_url().parent_url() }
pub fn parent_url(&self) -> Option<Url<'_>> { self.as_url().parent_url() }
#[inline]
pub fn pair(&self) -> Option<(UrlBuf, UrnBuf)> { self.as_url().pair() }
pub fn pair(&self) -> Option<(Url<'_>, UrnBuf)> { self.as_url().pair() }
pub fn parse(bytes: &[u8]) -> Result<(Scheme, Cow<'_, Path>, Option<(usize, usize)>)> {
let mut skip = 0;

View file

@ -1,10 +1,10 @@
use std::{ops::Deref, path::Path};
use std::{borrow::Cow, ffi::OsStr, fmt::{Debug, Formatter}, ops::Deref, path::Path};
use hashbrown::Equivalent;
use crate::{loc::{Loc, LocBuf}, url::{Components, Scheme, UrlBuf, UrnBuf}};
use crate::{loc::{Loc, LocBuf}, url::{Components, Encode, Scheme, UrlBuf, UrnBuf}};
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[derive(Clone, Eq, Hash, PartialEq)]
pub struct Url<'a> {
pub loc: Loc<'a>,
pub scheme: Scheme,
@ -41,6 +41,13 @@ impl Equivalent<UrlBuf> for Url<'_> {
fn equivalent(&self, key: &UrlBuf) -> bool { self == key }
}
// --- Debug
impl Debug for Url<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}{}", Encode::from(self), self.loc.display())
}
}
impl<'a> Url<'a> {
#[inline]
pub fn regular<T: AsRef<Path> + ?Sized>(path: &'a T) -> Self {
@ -50,12 +57,35 @@ impl<'a> Url<'a> {
#[inline]
pub fn is_regular(&self) -> bool { self.scheme == Scheme::Regular }
#[inline]
pub fn into_regular(self) -> Self {
Self { loc: self.loc.as_path().into(), scheme: Scheme::Regular }
}
#[inline]
pub fn is_search(&self) -> bool { matches!(self.scheme, Scheme::Search(_)) }
#[inline]
pub fn as_url(&'a self) -> Url<'a> { Self::from(self) }
#[inline]
pub fn to_owned(&self) -> UrlBuf { self.into() }
pub fn join(&self, path: impl AsRef<Path>) -> UrlBuf {
use Scheme as S;
let join = self.loc.join(path);
let loc = match self.scheme {
S::Regular => join.into(),
S::Search(_) => LocBuf::new(join, self.loc.base(), self.loc.base()),
S::Archive(_) => LocBuf::floated(join, self.loc.base()),
S::Sftp(_) => join.into(),
};
UrlBuf { loc, scheme: self.scheme.clone() }
}
pub fn base(&self) -> Option<Self> {
use Scheme as S;
@ -72,8 +102,7 @@ impl<'a> Url<'a> {
})
}
// TODO: use Url instead as the return type
pub fn parent_url(&self) -> Option<UrlBuf> {
pub fn parent_url(&self) -> Option<Self> {
use Scheme as S;
let parent = self.loc.parent()?;
@ -81,41 +110,43 @@ impl<'a> Url<'a> {
Some(match self.scheme {
// Regular
S::Regular => UrlBuf { loc: parent.into(), scheme: S::Regular },
S::Regular => Self { loc: parent.into(), scheme: S::Regular },
// Search
S::Search(_) if uri.is_empty() => UrlBuf { loc: parent.into(), scheme: S::Regular },
S::Search(_) => UrlBuf {
loc: LocBuf::new(parent, self.loc.base(), self.loc.base()),
S::Search(_) if uri.is_empty() => Self { loc: parent.into(), scheme: S::Regular },
S::Search(_) => Self {
loc: Loc::new(parent, self.loc.base(), self.loc.base()),
scheme: self.scheme.clone(),
},
// Archive
S::Archive(_) if uri.is_empty() => UrlBuf { loc: parent.into(), scheme: S::Regular },
S::Archive(_) if uri.is_empty() => Self { loc: parent.into(), scheme: S::Regular },
S::Archive(_) if uri.nth(1).is_none() => {
UrlBuf { loc: LocBuf::zeroed(parent), scheme: self.scheme.clone() }
Self { loc: Loc::zeroed(parent), scheme: self.scheme.clone() }
}
S::Archive(_) => {
UrlBuf { loc: LocBuf::floated(parent, self.loc.base()), scheme: self.scheme.clone() }
Self { loc: Loc::floated(parent, self.loc.base()), scheme: self.scheme.clone() }
}
// SFTP
S::Sftp(_) => UrlBuf { loc: parent.into(), scheme: self.scheme.clone() },
S::Sftp(_) => Self { loc: parent.into(), scheme: self.scheme.clone() },
})
}
#[inline]
pub fn components(&self) -> Components<'_> { Components::from(self) }
#[inline]
pub fn os_str(&self) -> Cow<'_, OsStr> { self.components().os_str() }
#[inline]
pub fn covariant(&self, other: impl Into<Self>) -> bool {
let other = other.into();
self.scheme.covariant(&other.scheme) && self.loc == other.loc
}
// TODO: use Urn instead as the return type
#[inline]
pub fn pair(&self) -> Option<(UrlBuf, UrnBuf)> {
pub fn pair(&self) -> Option<(Url<'a>, UrnBuf)> {
Some((self.parent_url()?, self.loc.urn_owned()))
}