From 396f634bb85d20b31971f4fb77fd938101bce494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Mon, 8 Dec 2025 16:24:19 +0800 Subject: [PATCH] fix: add preset VFS config file to avoid TOML parsing errors (#3414) --- yazi-config/preset/vfs-default.toml | 1 + yazi-config/src/preset.rs | 6 ++- yazi-config/src/vfs/mod.rs | 2 +- yazi-config/src/vfs/services.rs | 42 +++++++++++++++ yazi-config/src/vfs/vfs.rs | 35 +++++------- yazi-scheduler/src/file/file.rs | 8 ++- yazi-scheduler/src/file/in.rs | 6 +-- yazi-shared/src/loc/buf.rs | 12 ++--- yazi-shared/src/loc/loc.rs | 4 +- yazi-shared/src/scheme/cow.rs | 11 ++-- yazi-shared/src/scheme/encode.rs | 2 +- yazi-shared/src/url/cow.rs | 84 +++++++++++++++++++++++++++++ yazi-shared/src/url/url.rs | 23 ++++++-- 13 files changed, 184 insertions(+), 52 deletions(-) create mode 100644 yazi-config/preset/vfs-default.toml create mode 100644 yazi-config/src/vfs/services.rs diff --git a/yazi-config/preset/vfs-default.toml b/yazi-config/preset/vfs-default.toml new file mode 100644 index 00000000..48be1dab --- /dev/null +++ b/yazi-config/preset/vfs-default.toml @@ -0,0 +1 @@ +[services] diff --git a/yazi-config/src/preset.rs b/yazi-config/src/preset.rs index 7c7762ef..7030e5f4 100644 --- a/yazi-config/src/preset.rs +++ b/yazi-config/src/preset.rs @@ -1,4 +1,4 @@ -use crate::{Yazi, keymap::Keymap, theme::Theme}; +use crate::{Yazi, keymap::Keymap, theme::Theme, vfs::Vfs}; pub(crate) struct Preset; @@ -19,6 +19,10 @@ impl Preset { }) } + pub(super) fn vfs() -> Result { + toml::from_str(&yazi_macro::config_preset!("vfs")) + } + #[inline] pub(crate) fn mix(a: A, b: B, c: C) -> impl Iterator where diff --git a/yazi-config/src/vfs/mod.rs b/yazi-config/src/vfs/mod.rs index e52b3881..363908c7 100644 --- a/yazi-config/src/vfs/mod.rs +++ b/yazi-config/src/vfs/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(service vfs); +yazi_macro::mod_flat!(service services vfs); diff --git a/yazi-config/src/vfs/services.rs b/yazi-config/src/vfs/services.rs new file mode 100644 index 00000000..0e8814a2 --- /dev/null +++ b/yazi-config/src/vfs/services.rs @@ -0,0 +1,42 @@ +use std::ops::Deref; + +use anyhow::{Result, bail}; +use hashbrown::HashMap; +use serde::{Deserialize, Serialize}; + +use crate::vfs::Service; + +#[derive(Deserialize, Serialize)] +pub struct Services(HashMap); + +impl Deref for Services { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { &self.0 } +} + +impl Services { + pub(super) fn reshape(mut self) -> Result { + for (name, service) in &mut self.0 { + if name.is_empty() || name.len() > 20 { + bail!("VFS name `{name}` must be between 1 and 20 characters"); + } else if !name.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'z' | b'-')) { + bail!("VFS name `{name}` must be in kebab-case"); + } + + service.reshape()?; + } + + Ok(self) + } + + pub(super) fn deserialize_over<'de, D>(mut self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let map: HashMap = HashMap::deserialize(deserializer)?; + self.0.extend(map); + + Ok(self) + } +} diff --git a/yazi-config/src/vfs/vfs.rs b/yazi-config/src/vfs/vfs.rs index c5039ac8..b035889f 100644 --- a/yazi-config/src/vfs/vfs.rs +++ b/yazi-config/src/vfs/vfs.rs @@ -1,16 +1,17 @@ use std::io; -use hashbrown::HashMap; +use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; use tokio::sync::OnceCell; -use yazi_fs::Xdg; -use yazi_macro::ok_or_not_found; +use yazi_codegen::DeserializeOver1; +use yazi_fs::{Xdg, ok_or_not_found}; use super::Service; +use crate::{Preset, vfs::Services}; -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, DeserializeOver1)] pub struct Vfs { - pub services: HashMap, + pub services: Services, } impl Vfs { @@ -19,9 +20,10 @@ impl Vfs { async fn init() -> io::Result { tokio::task::spawn_blocking(|| { - toml::from_str::(&Vfs::read()?).map_err(io::Error::other)?.reshape() + Preset::vfs()?.deserialize_over(toml::Deserializer::parse(&Vfs::read()?)?)?.reshape() }) .await? + .map_err(|e| io::Error::new(io::ErrorKind::Other, e)) } LOADED.get_or_try_init(init).await @@ -40,24 +42,11 @@ impl Vfs { } } - fn read() -> io::Result { + fn read() -> Result { let p = Xdg::config_dir().join("vfs.toml"); - Ok(ok_or_not_found!(std::fs::read_to_string(&p).map_err(|e| { - std::io::Error::new(e.kind(), format!("Failed to read VFS config {p:?}: {e}")) - }))) + ok_or_not_found(std::fs::read_to_string(&p)) + .with_context(|| format!("Failed to read config {p:?}")) } - fn reshape(mut self) -> io::Result { - for (name, service) in &mut self.services { - if name.is_empty() || name.len() > 20 { - Err(io::Error::other(format!("VFS name `{name}` must be between 1 and 20 characters")))?; - } else if !name.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'z' | b'-')) { - Err(io::Error::other(format!("VFS name `{name}` must be in kebab-case")))?; - } - - service.reshape()?; - } - - Ok(self) - } + fn reshape(self) -> Result { Ok(Self { services: self.services.reshape()? }) } } diff --git a/yazi-scheduler/src/file/file.rs b/yazi-scheduler/src/file/file.rs index 8144eea5..8976a40a 100644 --- a/yazi-scheduler/src/file/file.rs +++ b/yazi-scheduler/src/file/file.rs @@ -119,14 +119,12 @@ impl File { .await?; if !links.is_empty() { - let len = links.len(); - let (tx, mut rx) = mpsc::channel(len); + let (tx, mut rx) = mpsc::channel(1); for task in links { self.queue(task.with_drop(&tx), LOW); } - for _ in 0..len { - rx.recv().await; - } + drop(tx); + while rx.recv().await.is_some() {} } for task in files { diff --git a/yazi-scheduler/src/file/in.rs b/yazi-scheduler/src/file/in.rs index 91483054..bee53877 100644 --- a/yazi-scheduler/src/file/in.rs +++ b/yazi-scheduler/src/file/in.rs @@ -42,11 +42,7 @@ pub(crate) struct FileInCut { } impl Drop for FileInCut { - fn drop(&mut self) { - if let Some(tx) = self.drop.take() { - tx.try_send(()).ok(); - } - } + fn drop(&mut self) { _ = self.drop.take(); } } impl FileInCut { diff --git a/yazi-shared/src/loc/buf.rs b/yazi-shared/src/loc/buf.rs index 115ef646..59db3862 100644 --- a/yazi-shared/src/loc/buf.rs +++ b/yazi-shared/src/loc/buf.rs @@ -266,24 +266,24 @@ mod tests { #[test] fn test_new() { let loc: LocBuf = Path::new("/").into(); - assert_eq!(loc.uri().as_os_str(), OsStr::new("/")); + assert_eq!(loc.uri().as_os_str(), OsStr::new("")); assert_eq!(loc.urn().as_os_str(), OsStr::new("")); assert_eq!(loc.file_name(), None); - assert_eq!(loc.base().as_os_str(), OsStr::new("")); + assert_eq!(loc.base().as_os_str(), OsStr::new("/")); assert_eq!(loc.trail().as_os_str(), OsStr::new("/")); let loc: LocBuf = Path::new("/root").into(); - assert_eq!(loc.uri().as_os_str(), OsStr::new("/root")); + assert_eq!(loc.uri().as_os_str(), OsStr::new("root")); assert_eq!(loc.urn().as_os_str(), OsStr::new("root")); assert_eq!(loc.file_name().unwrap(), OsStr::new("root")); - assert_eq!(loc.base().as_os_str(), OsStr::new("")); + assert_eq!(loc.base().as_os_str(), OsStr::new("/")); assert_eq!(loc.trail().as_os_str(), OsStr::new("/")); let loc: LocBuf = Path::new("/root/code/foo/").into(); - assert_eq!(loc.uri().as_os_str(), OsStr::new("/root/code/foo")); + assert_eq!(loc.uri().as_os_str(), OsStr::new("foo")); assert_eq!(loc.urn().as_os_str(), OsStr::new("foo")); assert_eq!(loc.file_name().unwrap(), OsStr::new("foo")); - assert_eq!(loc.base().as_os_str(), OsStr::new("")); + assert_eq!(loc.base().as_os_str(), OsStr::new("/root/code/")); assert_eq!(loc.trail().as_os_str(), OsStr::new("/root/code/")); } diff --git a/yazi-shared/src/loc/loc.rs b/yazi-shared/src/loc/loc.rs index d377b396..a1824ee8 100644 --- a/yazi-shared/src/loc/loc.rs +++ b/yazi-shared/src/loc/loc.rs @@ -86,7 +86,7 @@ where let path = path.as_path_view(); let Some(name) = path.file_name() else { let p = path.strip_prefix(P::empty()).unwrap(); - return Self { inner: p, uri: p.len(), urn: 0, _phantom: PhantomData }; + return Self { inner: p, uri: 0, urn: 0, _phantom: PhantomData }; }; let name_len = name.len(); @@ -97,7 +97,7 @@ where let bytes = &path.as_encoded_bytes()[..prefix_len + name_len]; Self { inner: unsafe { P::from_encoded_bytes_unchecked(bytes) }, - uri: bytes.len(), + uri: name_len, urn: name_len, _phantom: PhantomData, } diff --git a/yazi-shared/src/scheme/cow.rs b/yazi-shared/src/scheme/cow.rs index 1ec93849..30a4eea0 100644 --- a/yazi-shared/src/scheme/cow.rs +++ b/yazi-shared/src/scheme/cow.rs @@ -146,7 +146,7 @@ impl<'a> SchemeCow<'a> { Ok(match kind { SchemeKind::Regular => { ensure!(uri.is_none() && urn.is_none(), "Regular scheme cannot have ports"); - (path.components().count(), path.name().is_some() as usize) + (path.name().is_some() as usize, path.name().is_some() as usize) } SchemeKind::Search => { let (uri, urn) = (uri.unwrap_or(0), urn.unwrap_or(0)); @@ -155,7 +155,7 @@ impl<'a> SchemeCow<'a> { } SchemeKind::Archive => (uri.unwrap_or(0), urn.unwrap_or(0)), SchemeKind::Sftp => { - let uri = uri.unwrap_or_else(|| path.components().count()); + let uri = uri.unwrap_or(path.name().is_some() as usize); let urn = urn.unwrap_or(path.name().is_some() as usize); (uri, urn) } @@ -163,7 +163,12 @@ impl<'a> SchemeCow<'a> { } pub fn retrieve_ports(url: Url) -> (usize, usize) { - (url.uri().components().count(), url.urn().components().count()) + match url { + Url::Regular(loc) => (loc.file_name().is_some() as usize, loc.file_name().is_some() as usize), + Url::Search { loc, .. } => (loc.uri().components().count(), loc.urn().components().count()), + Url::Archive { loc, .. } => (loc.uri().components().count(), loc.urn().components().count()), + Url::Sftp { loc, .. } => (loc.uri().components().count(), loc.urn().components().count()), + } } } diff --git a/yazi-shared/src/scheme/encode.rs b/yazi-shared/src/scheme/encode.rs index f191d2d5..c51e28ea 100644 --- a/yazi-shared/src/scheme/encode.rs +++ b/yazi-shared/src/scheme/encode.rs @@ -39,7 +39,7 @@ impl<'a> Encode<'a> { SchemeKind::Regular => Ok(()), SchemeKind::Search | SchemeKind::Archive => w!(0, 0), SchemeKind::Sftp => { - w!(self.0.0.loc().components().count(), self.0.0.loc().name().is_some() as usize) + w!(self.0.0.loc().name().is_some() as usize, self.0.0.loc().name().is_some() as usize) } } } diff --git a/yazi-shared/src/url/cow.rs b/yazi-shared/src/url/cow.rs index 7d9a0c5a..d6bb9569 100644 --- a/yazi-shared/src/url/cow.rs +++ b/yazi-shared/src/url/cow.rs @@ -266,3 +266,87 @@ impl<'de> Deserialize<'de> for UrlCow<'_> { UrlBuf::deserialize(deserializer).map(UrlCow::from) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::url::UrlLike; + + #[test] + fn test_parse() -> Result<()> { + struct Case { + url: &'static str, + urn: &'static str, + uri: &'static str, + trail: &'static str, + base: &'static str, + } + + let cases = [ + // Regular + Case { + url: "/root/music/rock/song.mp3", + urn: "song.mp3", + uri: "song.mp3", + trail: "/root/music/rock/", + base: "/root/music/rock/", + }, + // Search portal + Case { + url: "search://keyword//root/Documents/reports", + urn: "", + uri: "", + trail: "search://keyword//root/Documents/reports", + base: "search://keyword//root/Documents/reports", + }, + // Search item + Case { + url: "search://keyword:2:2//root/Documents/reports/2023/summary.docx", + urn: "2023/summary.docx", + uri: "2023/summary.docx", + trail: "search://keyword//root/Documents/reports/", + base: "search://keyword//root/Documents/reports/", + }, + // Archive portal + Case { + url: "archive://domain//root/Downloads/images.zip", + urn: "", + uri: "", + trail: "archive://domain//root/Downloads/images.zip", + base: "archive://domain//root/Downloads/images.zip", + }, + // Archive item + Case { + url: "archive://domain:2:1//root/Downloads/images.zip/2025/city.jpg", + urn: "city.jpg", + uri: "2025/city.jpg", + trail: "archive://domain:1:1//root/Downloads/images.zip/2025/", + base: "archive://domain//root/Downloads/images.zip/", + }, + // SFTP + Case { + url: "sftp://my-server//root/docs/report.pdf", + urn: "report.pdf", + uri: "report.pdf", + trail: "sftp://my-server//root/docs/", + base: "sftp://my-server//root/docs/", + }, + ]; + + for case in cases { + let url = UrlCow::try_from(case.url)?; + assert_eq!(url.urn().to_str()?, case.urn); + assert_eq!(url.uri().to_str()?, case.uri); + assert_eq!( + format!("{:?}", url.trail()), + format!("{:?}", UrlCow::try_from(case.trail)?.as_url()) + ); + assert_eq!( + format!("{:?}", url.base()), + format!("{:?}", UrlCow::try_from(case.base)?.as_url()) + ); + } + + Ok(()) + } +} diff --git a/yazi-shared/src/url/url.rs b/yazi-shared/src/url/url.rs index a815670a..de27bbe5 100644 --- a/yazi-shared/src/url/url.rs +++ b/yazi-shared/src/url/url.rs @@ -61,8 +61,8 @@ impl<'a> Url<'a> { pub fn base(self) -> Self { match self { Self::Regular(loc) => Self::Regular(Loc::bare(loc.base())), - Self::Search { loc, domain } => Self::Search { loc: Loc::bare(loc.base()), domain }, - Self::Archive { loc, domain } => Self::Archive { loc: Loc::bare(loc.base()), domain }, + Self::Search { loc, domain } => Self::Search { loc: Loc::zeroed(loc.base()), domain }, + Self::Archive { loc, domain } => Self::Archive { loc: Loc::zeroed(loc.base()), domain }, Self::Sftp { loc, domain } => Self::Sftp { loc: Loc::bare(loc.base()), domain }, } } @@ -222,12 +222,25 @@ impl<'a> Url<'a> { #[inline] pub fn to_owned(self) -> UrlBuf { self.into() } - #[inline] pub fn trail(self) -> Self { + let uri = self.uri(); match self { Self::Regular(loc) => Self::Regular(Loc::bare(loc.trail())), - Self::Search { loc, domain } => Self::Search { loc: Loc::bare(loc.trail()), domain }, - Self::Archive { loc, domain } => Self::Archive { loc: Loc::bare(loc.trail()), domain }, + + Self::Search { loc, domain } if uri.is_empty() => { + Self::Search { loc: Loc::zeroed(loc.trail()), domain } + } + Self::Search { loc, domain } => { + Self::Search { loc: Loc::new(loc.trail(), loc.base(), loc.base()), domain } + } + + Self::Archive { loc, domain } if uri.is_empty() => { + Self::Archive { loc: Loc::zeroed(loc.trail()), domain } + } + Self::Archive { loc, domain } => { + Self::Archive { loc: Loc::new(loc.trail(), loc.base(), loc.base()), domain } + } + Self::Sftp { loc, domain } => Self::Sftp { loc: Loc::bare(loc.trail()), domain }, } }