fix: add preset VFS config file to avoid TOML parsing errors (#3414)

This commit is contained in:
三咲雅 misaki masa 2025-12-08 16:24:19 +08:00 committed by GitHub
parent 9ae8b90d23
commit 396f634bb8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 184 additions and 52 deletions

View file

@ -0,0 +1 @@
[services]

View file

@ -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<Vfs, toml::de::Error> {
toml::from_str(&yazi_macro::config_preset!("vfs"))
}
#[inline]
pub(crate) fn mix<E, A, B, C>(a: A, b: B, c: C) -> impl Iterator<Item = E>
where

View file

@ -1 +1 @@
yazi_macro::mod_flat!(service vfs);
yazi_macro::mod_flat!(service services vfs);

View file

@ -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<String, Service>);
impl Deref for Services {
type Target = HashMap<String, Service>;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl Services {
pub(super) fn reshape(mut self) -> Result<Self> {
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<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let map: HashMap<String, Service> = HashMap::deserialize(deserializer)?;
self.0.extend(map);
Ok(self)
}
}

View file

@ -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<String, Service>,
pub services: Services,
}
impl Vfs {
@ -19,9 +20,10 @@ impl Vfs {
async fn init() -> io::Result<Vfs> {
tokio::task::spawn_blocking(|| {
toml::from_str::<Vfs>(&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<String> {
fn read() -> Result<String> {
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<Self> {
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<Self> { Ok(Self { services: self.services.reshape()? }) }
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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