feat: WTF-8 validator (#3374)

This commit is contained in:
三咲雅 misaki masa 2025-11-27 14:22:14 +08:00 committed by sxyazi
parent 329c80cbee
commit 3561867b99
No known key found for this signature in database
26 changed files with 308 additions and 166 deletions

20
Cargo.lock generated
View file

@ -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",

View file

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

View file

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

View file

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

View file

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

View file

@ -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::<PathBuf>::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::<std::path::PathBuf>::with(path.into_os().unwrap(), uri, urn).unwrap(),
),
Url::Search { domain, .. } => UrlBuf::Search {
loc: LocBuf::<std::path::PathBuf>::with(path.into_os().unwrap(), uri, urn).unwrap(),
domain: domain.intern(),
},
Url::Archive { domain, .. } => UrlBuf::Archive {
loc: LocBuf::<std::path::PathBuf>::with(path.into_os().unwrap(), uri, urn).unwrap(),
domain: domain.intern(),
},
Url::Sftp { domain, .. } => UrlBuf::Sftp {
loc: LocBuf::<typed_path::UnixPathBuf>::with(path.into_unix().unwrap(), uri, urn).unwrap(),
domain: domain.intern(),
},
};
absolute_url(expanded)

View file

@ -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<K>(self, kind: K) -> Result<PathCow<'a>>
where
K: Into<PathKind>;
}
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<K>(self, kind: K) -> Result<PathCow<'a>>
where
K: Into<PathKind>,
{
PathCow::with(kind, percent_decode(self.encoded_bytes()))
}
}

View file

@ -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<PathBuf>;
@ -26,13 +26,12 @@ impl<'a> FsUrl<'a> for Url<'a> {
fn cache(&self) -> Option<PathBuf> {
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
}

View file

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

View file

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

View file

@ -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<PathBufDyn> for typed_path::UnixPathBuf {
fn try_from(value: PathBufDyn) -> Result<Self, Self::Error> { value.into_unix() }
}
// --- Hash
impl Hash for PathBufDyn {
fn hash<H: Hasher>(&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<PathDyn<'_>> 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()),
})
}

View file

@ -11,6 +11,15 @@ pub enum Component<'a> {
Normal(Strand<'a>),
}
impl<'a> Component<'a> {
pub fn as_normal(&self) -> Option<Strand<'a>> {
match self {
Self::Normal(s) => Some(*s),
_ => None,
}
}
}
impl<'a> From<std::path::Component<'a>> for Component<'a> {
fn from(value: std::path::Component<'a>) -> Self {
match value {

View file

@ -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<PathDynError> 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<StrandError> 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<StrandError> for StripPrefixError {
fn from(err: StrandError) -> Self {
match err {
StrandError::AsOs | StrandError::AsUtf8 => Self::WrongEncoding,
}
}
}
impl From<std::path::StripPrefixError> for StripPrefixError {
fn from(_: std::path::StripPrefixError) -> Self { Self::NotPrefix }
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<str> },
@ -92,6 +92,10 @@ impl From<Cow<'_, Self>> for UrlBuf {
}
// --- Eq
impl PartialEq for UrlBuf {
fn eq(&self, other: &Self) -> bool { self.as_url() == other.as_url() }
}
impl PartialEq<Url<'_>> for UrlBuf {
fn eq(&self, other: &Url) -> bool { self.as_url() == *other }
}
@ -100,6 +104,11 @@ impl PartialEq<Url<'_>> for &UrlBuf {
fn eq(&self, other: &Url) -> bool { self.as_url() == *other }
}
// --- Hash
impl Hash for UrlBuf {
fn hash<H: Hasher>(&self, state: &mut H) { self.as_url().hash(state) }
}
impl UrlBuf {
#[inline]
pub fn new() -> &'static Self {

View file

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

View file

@ -0,0 +1 @@
yazi_macro::mod_flat!(validator wtf8);

View file

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

View file

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

View file

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

View file

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

View file

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