diff --git a/Cargo.lock b/Cargo.lock index ec94c648..6ce50e62 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3876,21 +3876,21 @@ dependencies = [ [[package]] name = "tracing-appender" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf" dependencies = [ "crossbeam-channel", - "thiserror 1.0.69", + "thiserror 2.0.17", "time", "tracing-subscriber", ] [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", @@ -3899,9 +3899,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" dependencies = [ "once_cell", "valuable", @@ -4615,9 +4615,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" [[package]] name = "winsafe" @@ -4732,7 +4732,6 @@ dependencies = [ "futures", "hashbrown 0.16.1", "regex", - "serde", "vergen-gitcl", "yazi-adapter", "yazi-config", @@ -4933,6 +4932,7 @@ dependencies = [ "tokio", "tracing", "trash", + "typed-path", "uzers", "windows-sys 0.61.2", "yazi-ffi", diff --git a/yazi-actor/src/mgr/refresh.rs b/yazi-actor/src/mgr/refresh.rs index b1580cda..b174d9a8 100644 --- a/yazi-actor/src/mgr/refresh.rs +++ b/yazi-actor/src/mgr/refresh.rs @@ -48,12 +48,12 @@ impl Refresh { // TODO: performance improvement fn trigger_dirs(folders: &[&Folder]) { - async fn go(cwd: UrlBuf, cha: Cha) { - let Some(cha) = Files::assert_stale(&cwd, cha).await else { return }; + async fn go(dir: UrlBuf, cha: Cha) { + let Some(cha) = Files::assert_stale(&dir, cha).await else { return }; - match Files::from_dir_bulk(&cwd).await { - Ok(files) => FilesOp::Full(cwd, files, cha).emit(), - Err(e) => FilesOp::issue_error(&cwd, e).await, + match Files::from_dir_bulk(&dir).await { + Ok(files) => FilesOp::Full(dir, files, cha).emit(), + Err(e) => FilesOp::issue_error(&dir, e).await, } } diff --git a/yazi-boot/Cargo.toml b/yazi-boot/Cargo.toml index 724e99a3..1130dcc7 100644 --- a/yazi-boot/Cargo.toml +++ b/yazi-boot/Cargo.toml @@ -21,7 +21,6 @@ clap = { workspace = true } futures = { workspace = true } hashbrown = { workspace = true } regex = { workspace = true } -serde = { workspace = true } [build-dependencies] yazi-shared = { path = "../yazi-shared", version = "25.9.15" } diff --git a/yazi-fm/Cargo.toml b/yazi-fm/Cargo.toml index 177a063b..d5a7cb36 100644 --- a/yazi-fm/Cargo.toml +++ b/yazi-fm/Cargo.toml @@ -59,7 +59,7 @@ tokio-stream = { workspace = true } # Logging tracing = { workspace = true } -tracing-appender = "0.2.3" +tracing-appender = "0.2.4" tracing-subscriber = { version = "0.3.20", features = [ "env-filter" ] } [target."cfg(unix)".dependencies] diff --git a/yazi-fs/Cargo.toml b/yazi-fs/Cargo.toml index 2fb51961..00633926 100644 --- a/yazi-fs/Cargo.toml +++ b/yazi-fs/Cargo.toml @@ -32,6 +32,7 @@ scopeguard = { workspace = true } serde = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } +typed-path = { workspace = true } [target."cfg(unix)".dependencies] libc = { workspace = true } diff --git a/yazi-fs/src/path/expand.rs b/yazi-fs/src/path/expand.rs index cec4b1fd..200f1665 100644 --- a/yazi-fs/src/path/expand.rs +++ b/yazi-fs/src/path/expand.rs @@ -1,6 +1,6 @@ use std::{borrow::Cow, path::PathBuf}; -use yazi_shared::{FromWtf8Vec, loc::LocBuf, path::{PathBufDyn, PathCow, PathDyn, PathKind, PathLike}, pool::InternStr, url::{AsUrl, Url, UrlBuf, UrlCow, UrlLike}}; +use yazi_shared::{loc::LocBuf, path::{PathBufDyn, PathCow, PathDyn, PathKind, PathLike}, pool::InternStr, url::{AsUrl, Url, UrlBuf, UrlCow, UrlLike}, wtf8::FromWtf8Vec}; use crate::{CWD, path::clean_url}; @@ -25,21 +25,25 @@ fn expand_url_impl<'a>(url: Url<'a>) -> UrlCow<'a> { let mut path = PathBufDyn::with_capacity(url.kind(), n_base.len() + n_rest.len() + n_urn.len()); path.try_extend([n_base, n_rest, n_urn]).expect("extend original parts should not fail"); - let loc = LocBuf::::with( - path.into_os().expect("Failed to convert PathBufDyn to PathBuf"), - (uri_count + rest_diff + urn_diff) as usize, - (urn_count + urn_diff) as usize, - ) - .expect("Failed to create Loc from expanded path"); + let uri = (uri_count + rest_diff + urn_diff) as usize; + let urn = (urn_count + urn_diff) as usize; let expanded = match url { - Url::Regular(_) => UrlBuf::Regular(loc), - Url::Search { domain, .. } => UrlBuf::Search { loc, domain: domain.intern() }, - Url::Archive { domain, .. } => UrlBuf::Archive { loc, domain: domain.intern() }, - Url::Sftp { domain, .. } => { - todo!(); - // UrlBuf::Sftp { loc, domain: domain.intern() } - } + Url::Regular(_) => UrlBuf::Regular( + LocBuf::::with(path.into_os().unwrap(), uri, urn).unwrap(), + ), + Url::Search { domain, .. } => UrlBuf::Search { + loc: LocBuf::::with(path.into_os().unwrap(), uri, urn).unwrap(), + domain: domain.intern(), + }, + Url::Archive { domain, .. } => UrlBuf::Archive { + loc: LocBuf::::with(path.into_os().unwrap(), uri, urn).unwrap(), + domain: domain.intern(), + }, + Url::Sftp { domain, .. } => UrlBuf::Sftp { + loc: LocBuf::::with(path.into_unix().unwrap(), uri, urn).unwrap(), + domain: domain.intern(), + }, }; absolute_url(expanded) diff --git a/yazi-fs/src/path/percent.rs b/yazi-fs/src/path/percent.rs index 6728d086..0656a347 100644 --- a/yazi-fs/src/path/percent.rs +++ b/yazi-fs/src/path/percent.rs @@ -1,45 +1,32 @@ use std::{borrow::Cow, path::{Path, PathBuf}}; +use anyhow::Result; use percent_encoding::{AsciiSet, CONTROLS, percent_decode, percent_encode}; -use yazi_shared::path::PathDyn; +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'|'); -pub trait PercentEncoding { - fn percent_encode(&self) -> Cow<'_, Path>; +pub trait PercentEncoding<'a> { + fn percent_encode(self) -> Cow<'a, Path>; - fn percent_decode(&self) -> Cow<'_, [u8]>; + fn percent_decode(self, kind: K) -> Result> + where + K: Into; } -impl PercentEncoding for Path { - fn percent_encode(&self) -> Cow<'_, Path> { - match percent_encode(self.as_os_str().as_encoded_bytes(), SET).into() { - Cow::Borrowed(_) => self.into(), +impl<'a> PercentEncoding<'a> for PathDyn<'a> { + fn percent_encode(self) -> Cow<'a, Path> { + match percent_encode(self.encoded_bytes(), SET).into() { + Cow::Borrowed(s) => Path::new(s).into(), Cow::Owned(s) => PathBuf::from(s).into(), } } - fn percent_decode(&self) -> Cow<'_, [u8]> { - match percent_decode(self.as_os_str().as_encoded_bytes()).into() { - Cow::Borrowed(_) => self.as_os_str().as_encoded_bytes().into(), - Cow::Owned(s) => s.into(), - } - } -} - -impl PercentEncoding for PathDyn<'_> { - fn percent_encode(&self) -> Cow<'_, Path> { - match self { - PathDyn::Os(p) => p.percent_encode(), - PathDyn::Unix(_) => todo!(), - } - } - - fn percent_decode(&self) -> Cow<'_, [u8]> { - match self { - PathDyn::Os(p) => p.percent_decode(), - PathDyn::Unix(_) => todo!(), - } + fn percent_decode(self, kind: K) -> Result> + where + K: Into, + { + PathCow::with(kind, percent_decode(self.encoded_bytes())) } } diff --git a/yazi-fs/src/url.rs b/yazi-fs/src/url.rs index b8ff40da..ab23e9fc 100644 --- a/yazi-fs/src/url.rs +++ b/yazi-fs/src/url.rs @@ -1,8 +1,8 @@ use std::{borrow::Cow, ffi::OsStr, path::{Path, PathBuf}}; -use yazi_shared::{path::PathDyn, url::{AsUrl, Url, UrlBuf, UrlCow}}; +use yazi_shared::{path::{AsPath, PathDyn}, url::{AsUrl, Url, UrlBuf, UrlCow}}; -use crate::{FsHash128, FsScheme}; +use crate::{FsHash128, FsScheme, path::PercentEncoding}; pub trait FsUrl<'a> { fn cache(&self) -> Option; @@ -26,13 +26,12 @@ impl<'a> FsUrl<'a> for Url<'a> { fn cache(&self) -> Option { fn with_loc(loc: PathDyn, mut root: PathBuf) -> PathBuf { let mut it = loc.components(); - todo!(); - // if it.next() == Some(std::path::Component::RootDir) { - // root.push(it.as_path().percent_encode()); - // } else { - // root.push(".%2F"); - // root.push(loc.percent_encode()); - // } + if it.next() == Some(yazi_shared::path::Component::RootDir) { + root.push(it.as_path().percent_encode()); + } else { + root.push(".%2F"); + root.push(loc.percent_encode()); + } root } diff --git a/yazi-plugin/src/process/command.rs b/yazi-plugin/src/process/command.rs index 72a41303..93fb9a48 100644 --- a/yazi-plugin/src/process/command.rs +++ b/yazi-plugin/src/process/command.rs @@ -3,7 +3,7 @@ use std::{any::TypeId, ffi::OsStr, io, process::Stdio}; use mlua::{AnyUserData, ExternalError, IntoLuaMulti, Lua, MetaMethod, Table, UserData, Value}; use tokio::process::{ChildStderr, ChildStdin, ChildStdout}; use yazi_binding::Error; -use yazi_shared::FromWtf8; +use yazi_shared::wtf8::FromWtf8; use super::{Child, output::Output}; use crate::process::Status; diff --git a/yazi-shared/src/lib.rs b/yazi-shared/src/lib.rs index aff60ffa..6c3406f6 100644 --- a/yazi-shared/src/lib.rs +++ b/yazi-shared/src/lib.rs @@ -1,8 +1,8 @@ #![allow(clippy::option_map_unit_fn)] -yazi_macro::mod_pub!(data errors event loc path pool scheme shell strand translit url); +yazi_macro::mod_pub!(data errors event loc path pool scheme shell strand translit url wtf8); -yazi_macro::mod_flat!(alias bytes chars condition debounce either env id layer natsort os predictor ro_cell source sync_cell terminal tests throttle time utf8 wtf8); +yazi_macro::mod_flat!(alias bytes chars condition debounce either env id layer natsort os predictor ro_cell source sync_cell terminal tests throttle time utf8); pub fn init() { pool::init(); diff --git a/yazi-shared/src/path/buf.rs b/yazi-shared/src/path/buf.rs index 4f282cf8..807c65a9 100644 --- a/yazi-shared/src/path/buf.rs +++ b/yazi-shared/src/path/buf.rs @@ -1,11 +1,11 @@ -use std::ffi::{OsStr, OsString}; +use std::{ffi::OsString, hash::{Hash, Hasher}}; use hashbrown::Equivalent; -use crate::{FromWtf8, FromWtf8Vec, path::{AsPath, Component, PathDyn, PathDynError, PathKind, SetNameError}, strand::{AsStrand, Strand}}; +use crate::{path::{AsPath, Component, PathDyn, PathDynError, PathKind, SetNameError}, strand::AsStrand, wtf8::FromWtf8Vec}; // --- PathBufDyn -#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[derive(Clone, Debug, Eq)] pub enum PathBufDyn { Os(std::path::PathBuf), Unix(typed_path::UnixPathBuf), @@ -35,6 +35,16 @@ impl TryFrom for typed_path::UnixPathBuf { fn try_from(value: PathBufDyn) -> Result { value.into_unix() } } +// --- Hash +impl Hash for PathBufDyn { + fn hash(&self, state: &mut H) { self.as_path().hash(state) } +} + +// --- PartialEq +impl PartialEq for PathBufDyn { + fn eq(&self, other: &PathBufDyn) -> bool { self.as_path() == other.as_path() } +} + impl PartialEq> for PathBufDyn { fn eq(&self, other: &PathDyn<'_>) -> bool { self.as_path() == *other } } @@ -128,14 +138,10 @@ impl PathBufDyn { where T: AsStrand, { - Ok(match (self, name.as_strand()) { - (Self::Os(p), Strand::Os(s)) => p.set_file_name(s), - (Self::Os(p), Strand::Utf8(s)) => p.set_file_name(s), - (Self::Os(p), Strand::Bytes(b)) => { - p.set_file_name(OsStr::from_wtf8(b).map_err(|_| SetNameError::FromWtf8)?) - } - - (Self::Unix(p), s) => p.set_file_name(s.encoded_bytes()), + let s = name.as_strand(); + Ok(match self { + Self::Os(p) => p.set_file_name(s.as_os()?), + Self::Unix(p) => p.set_file_name(s.encoded_bytes()), }) } diff --git a/yazi-shared/src/path/component.rs b/yazi-shared/src/path/component.rs index 98b73bdb..ec60377b 100644 --- a/yazi-shared/src/path/component.rs +++ b/yazi-shared/src/path/component.rs @@ -11,6 +11,15 @@ pub enum Component<'a> { Normal(Strand<'a>), } +impl<'a> Component<'a> { + pub fn as_normal(&self) -> Option> { + match self { + Self::Normal(s) => Some(*s), + _ => None, + } + } +} + impl<'a> From> for Component<'a> { fn from(value: std::path::Component<'a>) -> Self { match value { diff --git a/yazi-shared/src/path/error.rs b/yazi-shared/src/path/error.rs index 772d7250..175ec67a 100644 --- a/yazi-shared/src/path/error.rs +++ b/yazi-shared/src/path/error.rs @@ -5,13 +5,15 @@ use crate::strand::StrandError; // --- EndsWithError #[derive(Debug, Error)] #[error("calling ends_with on paths with different encodings")] -pub struct EndsWithError; +pub enum EndsWithError { + FromStrand(#[from] StrandError), +} // --- JoinError #[derive(Debug, Error)] #[error("calling join on paths with different encodings")] pub enum JoinError { - FromWtf8, + FromStrand(#[from] StrandError), FromPathDyn(#[from] PathDynError), } @@ -47,7 +49,6 @@ impl From for std::io::Error { #[derive(Debug, Error)] #[error("calling set_name on paths with different encodings")] pub enum SetNameError { - FromWtf8, FromStrand(#[from] StrandError), } @@ -79,7 +80,9 @@ impl From for RsplitOnceError { // --- StartsWithError #[derive(Error, Debug)] #[error("calling starts_with on paths with different encodings")] -pub struct StartsWithError; +pub enum StartsWithError { + FromStrand(#[from] StrandError), +} // --- StripPrefixError #[derive(Debug, Error)] @@ -92,6 +95,14 @@ pub enum StripPrefixError { WrongEncoding, } +impl From for StripPrefixError { + fn from(err: StrandError) -> Self { + match err { + StrandError::AsOs | StrandError::AsUtf8 => Self::WrongEncoding, + } + } +} + impl From for StripPrefixError { fn from(_: std::path::StripPrefixError) -> Self { Self::NotPrefix } } diff --git a/yazi-shared/src/path/path.rs b/yazi-shared/src/path/path.rs index 6ed324d7..01c3cca6 100644 --- a/yazi-shared/src/path/path.rs +++ b/yazi-shared/src/path/path.rs @@ -4,9 +4,9 @@ use anyhow::Result; use hashbrown::Equivalent; use super::{RsplitOnceError, StartsWithError}; -use crate::{BytesExt, FromWtf8, Utf8BytePredictor, path::{AsPath, Components, Display, EndsWithError, JoinError, PathBufDyn, PathDynError, PathKind, StripPrefixError}, strand::{AsStrand, Strand, StrandError}}; +use crate::{BytesExt, Utf8BytePredictor, path::{AsPath, Components, Display, EndsWithError, JoinError, PathBufDyn, PathDynError, PathKind, StripPrefixError}, strand::{AsStrand, Strand, StrandError}}; -#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum PathDyn<'p> { Os(&'p std::path::Path), Unix(&'p typed_path::UnixPath), @@ -186,14 +186,10 @@ impl<'p> PathDyn<'p> { where T: AsStrand, { - Ok(match (self, child.as_strand()) { - (Self::Os(p), Strand::Os(q)) => p.ends_with(q), - (Self::Os(p), Strand::Utf8(q)) => p.ends_with(q), - (Self::Os(p), Strand::Bytes(b)) => { - p.ends_with(OsStr::from_wtf8(b).map_err(|_| EndsWithError)?) - } - - (Self::Unix(p), s) => p.ends_with(s.encoded_bytes()), + let s = child.as_strand(); + Ok(match self { + Self::Os(p) => p.ends_with(s.as_os()?), + Self::Unix(p) => p.ends_with(s.encoded_bytes()), }) } @@ -201,14 +197,10 @@ impl<'p> PathDyn<'p> { where T: AsStrand, { - Ok(match (self, path.as_strand()) { - (Self::Os(p), Strand::Os(q)) => PathBufDyn::Os(p.join(q)), - (Self::Os(p), Strand::Utf8(q)) => PathBufDyn::Os(p.join(q)), - (Self::Os(p), Strand::Bytes(b)) => { - PathBufDyn::Os(p.join(OsStr::from_wtf8(b).map_err(|_| JoinError::FromWtf8)?)) - } - - (Self::Unix(p), s) => PathBufDyn::Unix(p.join(s.encoded_bytes())), + let s = path.as_strand(); + Ok(match self { + Self::Os(p) => PathBufDyn::Os(p.join(s.as_os()?)), + Self::Unix(p) => PathBufDyn::Unix(p.join(s.encoded_bytes())), }) } @@ -235,14 +227,10 @@ impl<'p> PathDyn<'p> { where T: AsStrand, { - Ok(match (self, base.as_strand()) { - (Self::Os(p), Strand::Os(s)) => p.starts_with(s), - (Self::Os(p), Strand::Utf8(s)) => p.starts_with(s), - (Self::Os(p), Strand::Bytes(b)) => { - p.starts_with(OsStr::from_wtf8(b).map_err(|_| StartsWithError)?) - } - - (Self::Unix(p), s) => p.starts_with(s.encoded_bytes()), + let s = base.as_strand(); + Ok(match self { + Self::Os(p) => p.starts_with(s.as_os()?), + Self::Unix(p) => p.starts_with(s.encoded_bytes()), }) } @@ -250,14 +238,10 @@ impl<'p> PathDyn<'p> { where T: AsStrand, { - Ok(match (self, base.as_strand()) { - (Self::Os(p), Strand::Os(s)) => Self::Os(p.strip_prefix(s)?), - (Self::Os(p), Strand::Utf8(s)) => Self::Os(p.strip_prefix(s)?), - (Self::Os(p), Strand::Bytes(b)) => { - Self::Os(p.strip_prefix(OsStr::from_wtf8(b).map_err(|_| StripPrefixError::WrongEncoding)?)?) - } - - (Self::Unix(p), s) => Self::Unix(p.strip_prefix(s.encoded_bytes())?), + let s = base.as_strand(); + Ok(match self { + Self::Os(p) => Self::Os(p.strip_prefix(s.as_os()?)?), + Self::Unix(p) => Self::Unix(p.strip_prefix(s.encoded_bytes())?), }) } diff --git a/yazi-shared/src/shell/windows.rs b/yazi-shared/src/shell/windows.rs index 29ddc83b..ad25beee 100644 --- a/yazi-shared/src/shell/windows.rs +++ b/yazi-shared/src/shell/windows.rs @@ -40,7 +40,7 @@ pub fn escape_os_bytes(b: &[u8]) -> Cow<'_, [u8]> { #[cfg(windows)] pub fn escape_os_str(s: &std::ffi::OsStr) -> Cow<'_, std::ffi::OsStr> { - use crate::FromWtf8Vec; + use crate::wtf8::FromWtf8Vec; match escape_os_bytes(s.as_encoded_bytes()) { Cow::Borrowed(_) => Cow::Borrowed(s), diff --git a/yazi-shared/src/strand/buf.rs b/yazi-shared/src/strand/buf.rs index e5a28479..7b513bf0 100644 --- a/yazi-shared/src/strand/buf.rs +++ b/yazi-shared/src/strand/buf.rs @@ -2,7 +2,7 @@ use std::{borrow::Cow, ffi::OsString, hash::{Hash, Hasher}}; use anyhow::Result; -use crate::{FromWtf8Vec, path::PathDyn, strand::{AsStrand, Strand, StrandCow, StrandError, StrandKind}}; +use crate::{path::PathDyn, strand::{AsStrand, Strand, StrandCow, StrandError, StrandKind}, wtf8::FromWtf8Vec}; // --- StrandBuf #[derive(Clone, Debug, Eq)] diff --git a/yazi-shared/src/strand/conversion.rs b/yazi-shared/src/strand/conversion.rs index b527c5c7..846f139c 100644 --- a/yazi-shared/src/strand/conversion.rs +++ b/yazi-shared/src/strand/conversion.rs @@ -47,6 +47,10 @@ impl AsStrand for &std::path::Path { fn as_strand(&self) -> Strand<'_> { Strand::Os(self.as_os_str()) } } +impl AsStrand for &std::path::PathBuf { + fn as_strand(&self) -> Strand<'_> { Strand::Os(self.as_os_str()) } +} + impl AsStrand for &typed_path::UnixPath { fn as_strand(&self) -> Strand<'_> { Strand::Bytes(self.as_bytes()) } } @@ -55,6 +59,10 @@ impl AsStrand for crate::path::Components<'_> { fn as_strand(&self) -> Strand<'_> { self.strand() } } +impl AsStrand for Cow<'_, [u8]> { + fn as_strand(&self) -> Strand<'_> { Strand::Bytes(self) } +} + impl AsStrand for Cow<'_, OsStr> { fn as_strand(&self) -> Strand<'_> { Strand::Os(self) } } diff --git a/yazi-shared/src/strand/strand.rs b/yazi-shared/src/strand/strand.rs index 92dc418f..a1cc5711 100644 --- a/yazi-shared/src/strand/strand.rs +++ b/yazi-shared/src/strand/strand.rs @@ -2,7 +2,7 @@ use std::{borrow::Cow, ffi::OsStr, fmt::Display}; use anyhow::Result; -use crate::{BytesExt, FromWtf8, strand::{AsStrand, StrandBuf, StrandError, StrandKind}}; +use crate::{BytesExt, strand::{AsStrand, StrandBuf, StrandError, StrandKind}, wtf8::FromWtf8}; // --- Strand #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialOrd)] diff --git a/yazi-shared/src/url/buf.rs b/yazi-shared/src/url/buf.rs index 4811d866..fbfabae9 100644 --- a/yazi-shared/src/url/buf.rs +++ b/yazi-shared/src/url/buf.rs @@ -1,11 +1,11 @@ -use std::{borrow::Cow, fmt::{Debug, Formatter}, path::{Path, PathBuf}, str::FromStr}; +use std::{borrow::Cow, fmt::{Debug, Formatter}, hash::{Hash, Hasher}, path::{Path, PathBuf}, str::FromStr}; use anyhow::Result; use serde::{Deserialize, Serialize}; use crate::{loc::LocBuf, path::{PathBufDyn, PathDynError, SetNameError}, pool::{InternStr, Pool, Symbol}, scheme::{Scheme, SchemeKind}, strand::AsStrand, url::{AsUrl, Url, UrlCow, UrlLike}}; -#[derive(Clone, Eq, Hash, PartialEq)] +#[derive(Clone, Eq)] pub enum UrlBuf { Regular(LocBuf), Search { loc: LocBuf, domain: Symbol }, @@ -92,6 +92,10 @@ impl From> for UrlBuf { } // --- Eq +impl PartialEq for UrlBuf { + fn eq(&self, other: &Self) -> bool { self.as_url() == other.as_url() } +} + impl PartialEq> for UrlBuf { fn eq(&self, other: &Url) -> bool { self.as_url() == *other } } @@ -100,6 +104,11 @@ impl PartialEq> for &UrlBuf { fn eq(&self, other: &Url) -> bool { self.as_url() == *other } } +// --- Hash +impl Hash for UrlBuf { + fn hash(&self, state: &mut H) { self.as_url().hash(state) } +} + impl UrlBuf { #[inline] pub fn new() -> &'static Self { diff --git a/yazi-shared/src/url/components.rs b/yazi-shared/src/url/components.rs index 3c164c56..50778f81 100644 --- a/yazi-shared/src/url/components.rs +++ b/yazi-shared/src/url/components.rs @@ -32,17 +32,17 @@ impl<'a> Components<'a> { } pub fn os_str(&self) -> Cow<'a, OsStr> { - let Ok(path) = self.inner.path().as_os() else { + let Ok(os) = self.inner.strand().as_os() else { return OsString::from(EncodeUrl(self.url()).to_string()).into(); }; if self.url.is_regular() || self.scheme_yielded { - return path.as_os_str().into(); + return os.into(); } let mut s = OsString::from(EncodeScheme(self.url()).to_string()); - s.reserve_exact(path.as_os_str().len()); - s.push(path); + s.reserve_exact(os.len()); + s.push(os); s.into() } diff --git a/yazi-shared/src/wtf8/mod.rs b/yazi-shared/src/wtf8/mod.rs new file mode 100644 index 00000000..3a7d6a45 --- /dev/null +++ b/yazi-shared/src/wtf8/mod.rs @@ -0,0 +1 @@ +yazi_macro::mod_flat!(validator wtf8); diff --git a/yazi-shared/src/wtf8/validator.rs b/yazi-shared/src/wtf8/validator.rs new file mode 100644 index 00000000..247589e2 --- /dev/null +++ b/yazi-shared/src/wtf8/validator.rs @@ -0,0 +1,116 @@ +#[cfg(windows)] +pub(super) fn valid_wtf8(bytes: &[u8]) -> bool { + let mut i = 0; + while i < bytes.len() { + let b = bytes[i]; + if b < 0b1000_0000 { + // ASCII + i += 1; + continue; + } + + // 2-byte: 110x_xxxx 10xx_xxxx + // first byte must be >= 0b1100_0010 to forbid overlongs + if (b & 0b1110_0000) == 0b1100_0000 { + if b < 0b1100_0010 { + return false; + } + if i + 1 >= bytes.len() { + return false; + } + if (bytes[i + 1] & 0b1100_0000) != 0b1000_0000 { + return false; + } + i += 2; + continue; + } + + // 3-byte: 1110_xxxx 10xx_xxxx 10xx_xxxx + if (b & 0b1111_0000) == 0b1110_0000 { + if i + 2 >= bytes.len() { + return false; + } + let (b1, b2) = (bytes[i + 1], bytes[i + 2]); + if (b1 & 0b1100_0000) != 0b1000_0000 || (b2 & 0b1100_0000) != 0b1000_0000 { + return false; + } + if b == 0b1110_0000 && b1 < 0b1010_0000 { + // to forbid overlongs, second byte must be >= 0xA0 + return false; + } + i += 3; + continue; + } + + // 4-byte: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + if (b & 0b1111_1000) == 0b1111_0000 { + if b > 0b1111_0100 { + return false; + } + if i + 3 >= bytes.len() { + return false; + } + let (b1, b2, b3) = (bytes[i + 1], bytes[i + 2], bytes[i + 3]); + if (b1 & 0b1100_0000) != 0b1000_0000 + || (b2 & 0b1100_0000) != 0b1000_0000 + || (b3 & 0b1100_0000) != 0b1000_0000 + { + return false; + } + if b == 0b1111_0000 && b1 < 0b1001_0000 { + // to forbid overlongs for > U+FFFF, second byte must be >= 0x90 + return false; + } else if b == 0b1111_0100 && b1 > 0b1000_1111 { + // to stay <= U+10FFFF, second byte must be <= 0x8F + return false; + } + i += 4; + continue; + } + + return false; + } + true +} + +#[cfg(windows)] +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_valid_wtf8() { + let cases: &[(&[u8], bool)] = &[ + // Valid ASCII + (b"hello", true), + // Valid 2-byte UTF-8 + (&[0xc2, 0xa0], true), // U+00A0 + // Invalid 2-byte: overlong encoding + (&[0xc0, 0x80], false), // overlong for U+0000 + (&[0xc1, 0xbf], false), // overlong for U+007F + // Valid 3-byte UTF-8 + (&[0xe0, 0xa0, 0x80], true), // U+0800 + // Invalid 3-byte: overlong encoding + (&[0xe0, 0x9f, 0xbf], false), // overlong for U+07FF + // WTF-8 specific: unpaired surrogates (should be valid in WTF-8) + ((&[0xed, 0xa0, 0x80]), true), // U+D800 = ED A0 80 (high surrogate) + ((&[0xed, 0xbf, 0xbf]), true), // U+DFFF = ED BF BF (low surrogate) + // Valid 4-byte UTF-8 + ((&[0xf0, 0x90, 0x80, 0x80]), true), // U+10000 + // Invalid 4-byte: overlong + ((&[0xf0, 0x8f, 0xbf, 0xbf]), false), // overlong for U+FFFF + // Invalid 4-byte: beyond U+10FFFF + ((&[0xf4, 0x90, 0x80, 0x80]), false), // U+110000 + // Invalid continuation byte + ((&[0xc2, 0x00]), false), + // Incomplete sequence + ((&[0xc2]), false), + ((&[0xe0, 0xa0]), false), + ((&[0xf0, 0x90, 0x80]), false), + ]; + + for &(input, expected) in cases { + assert_eq!(valid_wtf8(input), expected, "input: {:?}", input); + } + } +} diff --git a/yazi-shared/src/wtf8.rs b/yazi-shared/src/wtf8/wtf8.rs similarity index 78% rename from yazi-shared/src/wtf8.rs rename to yazi-shared/src/wtf8/wtf8.rs index cf785aba..5ab86e7e 100644 --- a/yazi-shared/src/wtf8.rs +++ b/yazi-shared/src/wtf8/wtf8.rs @@ -35,8 +35,11 @@ impl FromWtf8 for OsStr { } #[cfg(windows)] { - // FIXME: validate WTF-8 - Ok(unsafe { Self::from_encoded_bytes_unchecked(wtf8) }) + if super::valid_wtf8(wtf8) { + Ok(unsafe { Self::from_encoded_bytes_unchecked(wtf8) }) + } else { + Err(anyhow::anyhow!("Invalid WTF-8 sequence")) + } } } } @@ -61,8 +64,11 @@ impl FromWtf8Vec for OsString { } #[cfg(windows)] { - // FIXME: validate WTF-8 - Ok(unsafe { Self::from_encoded_bytes_unchecked(wtf8) }) + if super::valid_wtf8(&wtf8) { + Ok(unsafe { Self::from_encoded_bytes_unchecked(wtf8) }) + } else { + Err(anyhow::anyhow!("Invalid WTF-8 sequence")) + } } } } diff --git a/yazi-vfs/src/provider/sftp/sftp.rs b/yazi-vfs/src/provider/sftp/sftp.rs index e31fbc76..9cc6143b 100644 --- a/yazi-vfs/src/provider/sftp/sftp.rs +++ b/yazi-vfs/src/provider/sftp/sftp.rs @@ -1,4 +1,4 @@ -use std::{io, path::Path, sync::Arc}; +use std::{io, sync::Arc}; use tokio::io::{AsyncWriteExt, BufReader, BufWriter}; use yazi_config::vfs::{ProviderSftp, Vfs}; diff --git a/yazi-watcher/src/reporter.rs b/yazi-watcher/src/reporter.rs index 5b2f4ac0..abda3763 100644 --- a/yazi-watcher/src/reporter.rs +++ b/yazi-watcher/src/reporter.rs @@ -1,3 +1,6 @@ +use std::borrow::Cow; + +use percent_encoding::percent_decode; use tokio::sync::mpsc; use yazi_shared::{scheme::SchemeKind, url::{AsUrl, Url, UrlBuf, UrlCow, UrlLike}}; @@ -24,8 +27,8 @@ impl Reporter { } } - fn report_local<'a>(&self, url: UrlCow<'a>) { - let Some((parent, name)) = url.pair() else { return }; + fn report_local(&self, url: UrlCow) { + let Some((parent, urn)) = url.pair() else { return }; // FIXME: LINKED should return Url instead of Path let linked = LINKED.read(); @@ -37,19 +40,22 @@ impl Reporter { self.local_tx.send(url.to_owned()).ok(); self.local_tx.send(parent.to_owned()).ok(); } - if name.ext().is_some_and(|e| e == "%tmp") { + + if urn.ext().is_some_and(|e| e == "%tmp") { continue; } - // SFTP caches - // todo!(); - // if let Some(dir) = watched.find_by_cache(&parent.loc()) { - // self.remote_tx.send(dir.join(name)).ok(); - // self.remote_tx.send(dir.to_owned()).ok(); - // } + + // Virtual caches + let Some(dir) = watched.find_by_cache(parent.loc()) else { continue }; + let Some(name) = url.name() else { continue }; + if let Ok(u) = dir.try_join(Cow::from(percent_decode(name.encoded_bytes()))) { + self.remote_tx.send(u).ok(); + } + self.remote_tx.send(dir).ok(); } } - fn report_remote<'a>(&self, url: UrlCow<'a>) { + fn report_remote(&self, url: UrlCow) { let Some(parent) = url.parent() else { return }; if !WATCHED.read().contains(parent) { return; diff --git a/yazi-watcher/src/watched.rs b/yazi-watcher/src/watched.rs index bfe322c6..cdb69a34 100644 --- a/yazi-watcher/src/watched.rs +++ b/yazi-watcher/src/watched.rs @@ -3,7 +3,7 @@ use std::path::Path; use hashbrown::HashSet; use percent_encoding::percent_decode_str; use yazi_fs::{Xdg, path::PercentEncoding}; -use yazi_shared::url::{AsUrl, UrlBuf, UrlLike}; +use yazi_shared::{path::{Component, PathBufDyn, PathDyn, PathLike}, pool::InternStr, scheme::SchemeKind, url::{AsUrl, UrlBuf, UrlLike}}; #[derive(Debug, Default)] pub struct Watched(HashSet); @@ -28,28 +28,24 @@ impl Watched { #[inline] pub(crate) fn remove(&mut self, url: impl AsUrl) { self.0.remove(&url.as_url()); } - pub(super) fn find_by_cache(&self, cache: &Path) -> Option<&UrlBuf> { - let mut it = cache.strip_prefix(Xdg::cache_dir()).ok()?.components(); + pub(super) fn find_by_cache(&self, cache: PathDyn) -> Option { + let mut it = cache.try_strip_prefix(Xdg::cache_dir()).ok()?.components(); - let l1 = it.next()?.as_os_str().to_str()?; - let (l2, rel) = - if let Ok(p) = it.as_path().strip_prefix(".%2F") { (p, true) } else { (it.as_path(), false) }; + // Parse domain + let domain = it.next()?.as_normal()?.to_str().ok()?; + let domain = percent_decode_str(domain.strip_prefix("sftp-")?).decode_utf8().ok()?.intern(); - let domain = percent_decode_str(l1.strip_prefix("sftp-")?).decode_utf8().ok()?; - let loc = l2.percent_decode(); + // Parse path + let (path, abs) = + if let Ok(p) = it.path().try_strip_prefix(".%2F") { (p, false) } else { (it.path(), true) }; + let path = path.percent_decode(SchemeKind::Sftp).ok()?; + let path = PathBufDyn::from_components( + SchemeKind::Sftp, + Some(Component::RootDir).filter(|_| abs).into_iter().chain(path.components()), + ) + .ok()?; - self.0.iter().find(|u| { - todo!() - // if u.scheme != SchemeRef::Sftp(&domain) { - // return false; - // } - - // let mut it = u.loc.components(); - // if it.next() == Some(std::path::Component::RootDir) { - // !rel && it.as_path().as_os_str().as_encoded_bytes() == loc.as_ref() - // } else { - // rel && u.loc.as_os_str().as_encoded_bytes() == loc.as_ref() - // } - }) + let url = UrlBuf::Sftp { loc: path.into_unix().ok()?.into(), domain }; + if self.contains(&url) { Some(url) } else { None } } }