diff --git a/Cargo.lock b/Cargo.lock index ecd39549..d8013395 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5072,6 +5072,7 @@ dependencies = [ "parking_lot", "percent-encoding", "serde", + "thiserror 2.0.17", "tokio", "uzers", "windows-sys 0.61.2", diff --git a/Cargo.toml b/Cargo.toml index 55fb0ce3..d9e4b1e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ scopeguard = "1.2.0" serde = { version = "1.0.228", features = [ "derive" ] } serde_json = "1.0.145" syntect = { version = "5.3.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] } +thiserror = "2.0.17" tokio = { version = "1.48.0", features = [ "full" ] } tokio-stream = "0.1.17" tokio-util = "0.7.17" @@ -61,4 +62,5 @@ uzers = "0.12.1" format_push_string = "warn" implicit_clone = "warn" module_inception = "allow" +unit_arg = "allow" use_self = "warn" diff --git a/cspell.json b/cspell.json index c55d3a1a..5631c351 100644 --- a/cspell.json +++ b/cspell.json @@ -1 +1 @@ -{"language":"en","flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","peekable","ratatui","syntect","pbpaste","pbcopy","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","Konsole","Überzug","pkgs","pdftoppm","poppler","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","hardlink","hardlinking","nlink","nlink","linemodes","SIGSTOP","sevenzip","rsplitn","replacen","DECSET","DECRQM","repeek","cwds","tcsi","Hyprland","Wayfire","SWAYSOCK","btime","nsec","codegen","gethostname","fchmod","fdfind","Rustc","rustc","ffprobe","vframes","luma","obase","outln","errln","tmtheme","twox","cfgs","fstype","objc","rdev","runloop","exfat","rclone","DECRQSS","DECSCUSR","libvterm","Uninit","lockin","rposition","resvg","foldhash","tilded","futs","chdir","hashbrown","JEMALLOC","RUSTFLAGS","RDONLY","GETPATH","fcntl","casefold","inodes","Splatable","casefied"],"version":"0.2"} \ No newline at end of file +{"version":"0.2","flagWords":[],"language":"en","words":["Punct","KEYMAP","splitn","crossterm","YAZI","peekable","ratatui","syntect","pbpaste","pbcopy","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","Konsole","Überzug","pkgs","pdftoppm","poppler","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","hardlink","hardlinking","nlink","nlink","linemodes","SIGSTOP","sevenzip","rsplitn","replacen","DECSET","DECRQM","repeek","cwds","tcsi","Hyprland","Wayfire","SWAYSOCK","btime","nsec","codegen","gethostname","fchmod","fdfind","Rustc","rustc","ffprobe","vframes","luma","obase","outln","errln","tmtheme","twox","cfgs","fstype","objc","rdev","runloop","exfat","rclone","DECRQSS","DECSCUSR","libvterm","Uninit","lockin","rposition","resvg","foldhash","tilded","futs","chdir","hashbrown","JEMALLOC","RUSTFLAGS","RDONLY","GETPATH","fcntl","casefold","inodes","Splatable","casefied","thiserror","memchr","memmem","russh","deadpool","keepalive","nodelay","publickey","deadpool"]} \ No newline at end of file diff --git a/yazi-actor/src/cmp/show.rs b/yazi-actor/src/cmp/show.rs index 99645d87..3e151c1c 100644 --- a/yazi-actor/src/cmp/show.rs +++ b/yazi-actor/src/cmp/show.rs @@ -1,9 +1,9 @@ -use std::{ffi::OsStr, mem, ops::ControlFlow}; +use std::{mem, ops::ControlFlow}; use anyhow::Result; use yazi_macro::{render, succ}; use yazi_parser::cmp::{CmpItem, ShowOpt}; -use yazi_shared::{data::Data, osstr_contains, osstr_starts_with}; +use yazi_shared::{data::Data, path::{AsPathDyn, PathDyn, PathLike}, strand::{AsStrand, StrandLike}}; use crate::{Actor, Ctx}; @@ -30,7 +30,7 @@ impl Actor for Show { }; cmp.ticket = opt.ticket; - cmp.cands = Self::match_candidates(opt.word.as_os_str(), cache); + cmp.cands = Self::match_candidates(opt.word.as_path_dyn(), cache); if cmp.cands.is_empty() { succ!(render!(mem::replace(&mut cmp.visible, false))); } @@ -43,16 +43,20 @@ impl Actor for Show { } impl Show { - fn match_candidates(word: &OsStr, cache: &[CmpItem]) -> Vec { - let smart = !word.as_encoded_bytes().iter().any(|c| c.is_ascii_uppercase()); + fn match_candidates(word: PathDyn, cache: &[CmpItem]) -> Vec { + let smart = !word.encoded_bytes().iter().any(|&b| b.is_ascii_uppercase()); let flow = cache.iter().try_fold((Vec::new(), Vec::new()), |(mut exact, mut fuzzy), item| { - if osstr_starts_with(&item.name, word, smart) { + let name = item.name.as_strand(); + let starts_with = + if smart { name.eq_ignore_ascii_case(word) } else { name.starts_with(word) }; + + if starts_with { exact.push(item); if exact.len() >= LIMIT { return ControlFlow::Break((exact, fuzzy)); } - } else if fuzzy.len() < LIMIT - exact.len() && osstr_contains(&item.name, word) { + } else if fuzzy.len() < LIMIT - exact.len() && name.contains(word) { // Here we don't break the control flow, since we want more exact matching. fuzzy.push(item) } diff --git a/yazi-actor/src/cmp/trigger.rs b/yazi-actor/src/cmp/trigger.rs index a5ac077d..3cc13427 100644 --- a/yazi-actor/src/cmp/trigger.rs +++ b/yazi-actor/src/cmp/trigger.rs @@ -1,11 +1,11 @@ -use std::{ffi::OsString, mem, path::{MAIN_SEPARATOR_STR, PathBuf}}; +use std::{mem, path::MAIN_SEPARATOR_STR}; use anyhow::Result; use yazi_fs::{CWD, path::expand_url, provider::{DirReader, FileHolder}}; use yazi_macro::{act, render, succ}; use yazi_parser::cmp::{CmpItem, ShowOpt, TriggerOpt}; use yazi_proxy::CmpProxy; -use yazi_shared::{OsStrSplit, data::Data, natsort, scheme::SchemeLike, url::{UrlBuf, UrlCow}}; +use yazi_shared::{AnyAsciiChar, data::Data, natsort, path::{AsPath, PathBufDyn, PathLike}, scheme::{SchemeCow, SchemeLike}, strand::StrandBufLike, url::{UrlBuf, UrlCow, UrlLike}}; use yazi_vfs::provider; use crate::{Actor, Ctx}; @@ -32,7 +32,7 @@ impl Actor for Trigger { if cmp.caches.contains_key(&parent) { let ticket = cmp.ticket; - return act!(cmp:show, cx, ShowOpt { cache_name: parent, word, ticket, ..Default::default() }); + return act!(cmp:show, cx, ShowOpt { cache: vec![], cache_name: parent, word, ticket }); } let ticket = cmp.ticket; @@ -42,8 +42,8 @@ impl Actor for Trigger { // "/" is both a directory separator and the root directory per se // As there's no parent directory for the FS root, it is a special case - if parent.loc.as_os_str() == "/" { - cache.push(CmpItem { name: OsString::new(), is_dir: true }); + if parent.loc() == "/" { + cache.push(CmpItem { name: Default::default(), is_dir: true }); } while let Ok(Some(ent)) = dir.next().await { @@ -53,9 +53,8 @@ impl Actor for Trigger { } if !cache.is_empty() { - cache.sort_unstable_by(|a, b| { - natsort(a.name.as_encoded_bytes(), b.name.as_encoded_bytes(), false) - }); + cache + .sort_unstable_by(|a, b| natsort(a.name.encoded_bytes(), b.name.encoded_bytes(), false)); CmpProxy::show(ShowOpt { cache, cache_name: parent, word, ticket }); } @@ -67,23 +66,28 @@ impl Actor for Trigger { } impl Trigger { - fn split_url(s: &str) -> Option<(UrlBuf, PathBuf)> { - let (scheme, path, ..) = UrlCow::parse(s.as_bytes()).ok()?; + fn split_url(s: &str) -> Option<(UrlBuf, PathBufDyn)> { + let (scheme, path) = SchemeCow::parse(s.as_bytes()).ok()?; - if scheme.is_local() && path.as_os_str() == "~" { + if scheme.is_local() && path == "~" { return None; // We don't autocomplete a `~`, but `~/` } - #[cfg(windows)] - const SEP: &[char] = &['/', '\\']; - #[cfg(not(windows))] - const SEP: char = std::path::MAIN_SEPARATOR; + let sep = if cfg!(windows) { + AnyAsciiChar::new(&[b'/', b'\\']).unwrap() + } else { + AnyAsciiChar::new(&[b'/']).unwrap() + }; - Some(match path.as_os_str().rsplit_once(SEP) { + Some(match path.as_path().rsplit_pred(sep) { Some((p, c)) if p.is_empty() => { - (UrlBuf { loc: MAIN_SEPARATOR_STR.into(), scheme: scheme.into() }, c.into()) + let root = PathBufDyn::with(scheme.kind(), MAIN_SEPARATOR_STR).expect("valid root"); + (UrlCow::try_from((scheme, root)).ok()?.into_owned(), c.into()) + } + Some((p, c)) => { + let parent = PathBufDyn::with(scheme.kind(), p.as_dyn()).expect("valid parent"); + (expand_url(UrlCow::try_from((scheme, parent)).ok()?), c.into()) } - Some((p, c)) => (expand_url(UrlBuf { loc: p.into(), scheme: scheme.into() }), c.into()), None => (CWD.load().as_ref().clone(), path.into()), }) } @@ -91,13 +95,13 @@ impl Trigger { #[cfg(test)] mod tests { - use yazi_shared::url::UrlLike; + use yazi_shared::{path::PathBufLike, url::UrlLike}; use super::*; fn compare(s: &str, parent: &str, child: &str) { let (mut p, c) = Trigger::split_url(s).unwrap(); - if let Some(u) = p.strip_prefix(yazi_fs::CWD.load().as_ref()) { + if let Ok(u) = p.try_strip_prefix(yazi_fs::CWD.load().as_ref()) { p = UrlBuf::from(u); } assert_eq!((p, c.to_str().unwrap()), (parent.parse().unwrap(), child)); diff --git a/yazi-actor/src/lives/file.rs b/yazi-actor/src/lives/file.rs index aeca4b56..fc6aa3b7 100644 --- a/yazi-actor/src/lives/file.rs +++ b/yazi-actor/src/lives/file.rs @@ -4,7 +4,7 @@ use mlua::{AnyUserData, IntoLua, UserData, UserDataFields, UserDataMethods, Valu use yazi_binding::Style; use yazi_config::THEME; use yazi_plugin::bindings::Range; -use yazi_shared::url::UrlLike; +use yazi_shared::{path::PathLike, url::UrlLike}; use super::Lives; use crate::lives::PtrCell; @@ -88,11 +88,8 @@ impl UserData for File { if !me.url.has_trail() { return Ok(None); } - let Some(path) = me.url.as_path() else { - return Ok(None); - }; - let mut comp = path.strip_prefix(me.url.loc.trail()).unwrap_or(path).components(); + let mut comp = me.url.try_strip_prefix(me.url.trail()).unwrap_or(me.url.loc()).components(); comp.next_back(); Some(lua.create_string(comp.as_path().as_os_str().as_encoded_bytes())).transpose() }); diff --git a/yazi-actor/src/lives/tab.rs b/yazi-actor/src/lives/tab.rs index 6c97fbb3..311af5b9 100644 --- a/yazi-actor/src/lives/tab.rs +++ b/yazi-actor/src/lives/tab.rs @@ -2,7 +2,7 @@ use std::ops::Deref; use mlua::{AnyUserData, UserData, UserDataFields, UserDataMethods, Value}; use yazi_binding::{Id, UrlRef, cached_field}; -use yazi_shared::url::UrlLike; +use yazi_shared::{path::PathLike, strand::StrandLike, url::UrlLike}; use super::{Finder, Folder, Lives, Mode, Preference, Preview, PtrCell, Selected}; @@ -47,7 +47,7 @@ impl UserData for Tab { fields.add_field_method_get("id", |_, me| Ok(Id(me.id))); cached_field!(fields, name, |lua, me| { let url = &me.current.url; - lua.create_string(url.name().unwrap_or(url.loc.as_os_str()).as_encoded_bytes()) + lua.create_string(url.name().map_or_else(|| url.loc().encoded_bytes(), |n| n.encoded_bytes())) }); cached_field!(fields, mode, |_, me| Mode::make(&me.mode)); diff --git a/yazi-actor/src/mgr/bulk_rename.rs b/yazi-actor/src/mgr/bulk_rename.rs index bf178b5e..2742c867 100644 --- a/yazi-actor/src/mgr/bulk_rename.rs +++ b/yazi-actor/src/mgr/bulk_rename.rs @@ -11,7 +11,7 @@ use yazi_fs::{File, FilesOp, Splatter, max_common_root, path::skip_url, provider use yazi_macro::{err, succ}; use yazi_parser::VoidOpt; use yazi_proxy::{AppProxy, HIDER, TasksProxy}; -use yazi_shared::{OsStrJoin, data::Data, terminal_clear, url::{AsUrl, Component, UrlBuf, UrlCow, UrlLike}}; +use yazi_shared::{OsStrJoin, data::Data, path::PathDyn, terminal_clear, url::{AsUrl, UrlBuf, UrlCow, UrlLike}}; use yazi_term::tty::TTY; use yazi_vfs::{VfsFile, maybe_exists, provider}; use yazi_watcher::WATCHER; @@ -50,7 +50,12 @@ impl Actor for BulkRename { .write_all(old.join(OsStr::new("\n")).as_encoded_bytes()) .await?; - defer! { tokio::spawn(Local.remove_file(tmp.clone())); } + defer! { + let tmp = tmp.clone(); + tokio::spawn(async move { + Local::regular(&tmp).remove_file().await + }); + } TasksProxy::process_exec( cwd.into(), Splatter::new(&[UrlCow::default(), tmp.as_url().into()]).splat(&opener.run), @@ -64,8 +69,8 @@ impl Actor for BulkRename { defer!(AppProxy::resume()); AppProxy::stop().await; - let new: Vec<_> = Local - .read_to_string(&tmp) + let new: Vec<_> = Local::regular(&tmp) + .read_to_string() .await? .lines() .take(old.len()) @@ -120,10 +125,12 @@ impl BulkRename { let permit = WATCHER.acquire().await.unwrap(); let (mut failed, mut succeeded) = (Vec::new(), HashMap::with_capacity(todo.len())); for (o, n) in todo { - let (old, new): (UrlBuf, UrlBuf) = ( - selected[o.0].components().take(root).chain([Component::Normal(&o)]).collect(), - selected[n.0].components().take(root).chain([Component::Normal(&n)]).collect(), - ); + let (Ok(old), Ok(new)) = + (Self::replace_url(&selected[o.0], root, &o), Self::replace_url(&selected[n.0], root, &n)) + else { + failed.push((o, n, anyhow!("Invalid new or old file name"))); + continue; + }; if maybe_exists(&new).await && !provider::must_identical(&old, &new).await { failed.push((o, n, anyhow!("Destination already exists"))); @@ -137,7 +144,7 @@ impl BulkRename { } if !succeeded.is_empty() { - let it = succeeded.iter().map(|(o, n)| (o, &n.url)); + let it = succeeded.iter().map(|(o, n)| (o.as_url(), n.url.as_url())); err!(Pubsub::pub_after_bulk(it)); FilesOp::rename(succeeded); } @@ -153,6 +160,10 @@ impl BulkRename { YAZI.opener.block(YAZI.open.all(Path::new("bulk-rename.txt"), "text/plain")) } + fn replace_url(url: &UrlBuf, take: usize, rep: &OsStr) -> Result { + Ok(url.try_replace(take, PathDyn::with(url.kind(), rep)?)?.into_owned()) + } + async fn output_failed(failed: Vec<(Tuple, Tuple, anyhow::Error)>) -> Result<()> { let mut stdout = TTY.lockout(); terminal_clear(&mut *stdout)?; diff --git a/yazi-actor/src/mgr/copy.rs b/yazi-actor/src/mgr/copy.rs index 152a5565..88402c93 100644 --- a/yazi-actor/src/mgr/copy.rs +++ b/yazi-actor/src/mgr/copy.rs @@ -1,5 +1,3 @@ -use std::ffi::OsString; - use anyhow::{Result, bail}; use yazi_macro::{act, succ}; use yazi_parser::mgr::CopyOpt; @@ -18,7 +16,7 @@ impl Actor for Copy { fn act(cx: &mut Ctx, opt: Self::Options) -> Result { act!(mgr:escape_visual, cx)?; - let mut s = OsString::new(); + let mut s = Vec::::new(); let mut it = if opt.hovered { Box::new(cx.hovered().map(|h| &h.url).into_iter()) } else { @@ -30,29 +28,29 @@ impl Actor for Copy { match opt.r#type.as_ref() { // TODO: rename to "url" "path" => { - s.push(opt.separator.transform(&u.os_str())); + s.extend_from_slice(&opt.separator.transform(&u.os_str())); } "dirname" => { if let Some(p) = u.parent() { - s.push(opt.separator.transform(&p.os_str())); + s.extend_from_slice(&opt.separator.transform(&p.os_str())); } } "filename" => { - s.push(opt.separator.transform(u.name().unwrap_or_default())); + s.extend_from_slice(&opt.separator.transform(&u.name().unwrap_or_default())); } "name_without_ext" => { - s.push(opt.separator.transform(u.stem().unwrap_or_default())); + s.extend_from_slice(&opt.separator.transform(&u.stem().unwrap_or_default())); } _ => bail!("Unknown copy type: {}", opt.r#type), }; if it.peek().is_some() { - s.push("\n"); + s.push(b'\n'); } } // Copy the CWD path regardless even if the directory is empty if s.is_empty() && opt.r#type == "dirname" { - s.push(opt.separator.transform(&cx.cwd().os_str())); + s.extend_from_slice(&opt.separator.transform(&cx.cwd().os_str())); } futures::executor::block_on(CLIPBOARD.set(s)); diff --git a/yazi-actor/src/mgr/create.rs b/yazi-actor/src/mgr/create.rs index 0b73d6c9..733617f1 100644 --- a/yazi-actor/src/mgr/create.rs +++ b/yazi-actor/src/mgr/create.rs @@ -27,7 +27,10 @@ impl Actor for Create { return; } - let new = cwd.join(&name); + let Ok(new) = cwd.try_join(&name) else { + return; + }; + if !opt.force && maybe_exists(&new).await && !ConfirmProxy::show(ConfirmCfg::overwrite(&new)).await diff --git a/yazi-actor/src/mgr/download.rs b/yazi-actor/src/mgr/download.rs index a0c1090b..cbeb0086 100644 --- a/yazi-actor/src/mgr/download.rs +++ b/yazi-actor/src/mgr/download.rs @@ -8,7 +8,7 @@ use yazi_fs::{File, FsScheme, provider::{Provider, local::Local}}; use yazi_macro::succ; use yazi_parser::mgr::{DownloadOpt, OpenOpt}; use yazi_proxy::MgrProxy; -use yazi_shared::{data::Data, url::UrlCow}; +use yazi_shared::{data::Data, url::{UrlCow, UrlLike}}; use yazi_vfs::VfsFile; use crate::{Actor, Ctx}; @@ -78,7 +78,7 @@ impl Download { let roots: HashSet<_> = urls.iter().filter_map(|u| u.scheme().cache()).collect(); for mut root in roots { root.push("%lock"); - Local.create_dir_all(root).await.ok(); + Local::regular(&root).create_dir_all().await.ok(); } } } diff --git a/yazi-actor/src/mgr/enter.rs b/yazi-actor/src/mgr/enter.rs index efece673..4ec6ef85 100644 --- a/yazi-actor/src/mgr/enter.rs +++ b/yazi-actor/src/mgr/enter.rs @@ -15,7 +15,7 @@ impl Actor for Enter { fn act(cx: &mut Ctx, _: Self::Options) -> Result { let Some(h) = cx.hovered().filter(|h| h.is_dir()) else { succ!() }; - let url = if h.url.is_search() { h.url.to_regular() } else { h.url.clone() }; + let url = if h.url.is_search() { h.url.to_regular()? } else { h.url.clone() }; act!(mgr:cd, cx, (url, CdSource::Enter)) } diff --git a/yazi-actor/src/mgr/follow.rs b/yazi-actor/src/mgr/follow.rs index 00e5f4fc..c79a3bf9 100644 --- a/yazi-actor/src/mgr/follow.rs +++ b/yazi-actor/src/mgr/follow.rs @@ -16,11 +16,8 @@ impl Actor for Follow { fn act(cx: &mut Ctx, _: Self::Options) -> Result { let Some(file) = cx.hovered() else { succ!() }; let Some(link_to) = &file.link_to else { succ!() }; - - if let Some(p) = file.url.parent() { - act!(mgr:reveal, cx, clean_url(p.join(link_to))) - } else { - succ!() - } + let Some(parent) = file.url.parent() else { succ!() }; + let Ok(joined) = parent.try_join(link_to) else { succ!() }; + act!(mgr:reveal, cx, clean_url(joined)) } } diff --git a/yazi-actor/src/mgr/hover.rs b/yazi-actor/src/mgr/hover.rs index 75e8afdb..51c3b379 100644 --- a/yazi-actor/src/mgr/hover.rs +++ b/yazi-actor/src/mgr/hover.rs @@ -18,7 +18,7 @@ impl Actor for Hover { // Parent should always track CWD if let Some(p) = &mut tab.parent { - render!(p.repos(tab.current.url.strip_prefix(&p.url))); + render!(p.repos(tab.current.url.try_strip_prefix(&p.url).ok())); } // Repos CWD diff --git a/yazi-actor/src/mgr/leave.rs b/yazi-actor/src/mgr/leave.rs index 8de02c97..c83d2f23 100644 --- a/yazi-actor/src/mgr/leave.rs +++ b/yazi-actor/src/mgr/leave.rs @@ -21,7 +21,7 @@ impl Actor for Leave { let Some(mut url) = url else { succ!() }; if url.is_search() { - url = url.into_regular(); + url = url.as_regular()?; } act!(mgr:cd, cx, (url, CdSource::Leave)) diff --git a/yazi-actor/src/mgr/refresh.rs b/yazi-actor/src/mgr/refresh.rs index ed9b231c..b1580cda 100644 --- a/yazi-actor/src/mgr/refresh.rs +++ b/yazi-actor/src/mgr/refresh.rs @@ -6,7 +6,7 @@ use yazi_fs::{CWD, Files, FilesOp, cha::Cha}; use yazi_macro::{act, succ}; use yazi_parser::VoidOpt; use yazi_proxy::MgrProxy; -use yazi_shared::{data::Data, scheme::SchemeLike, url::UrlBuf}; +use yazi_shared::{data::Data, url::{UrlBuf, UrlLike}}; use yazi_term::tty::TTY; use yazi_vfs::{VfsFiles, VfsFilesOp}; @@ -41,7 +41,7 @@ impl Actor for Refresh { impl Refresh { fn cwd_changed() { - if CWD.load().scheme.is_virtual() { + if CWD.load().kind().is_virtual() { MgrProxy::watch(); } } diff --git a/yazi-actor/src/mgr/rename.rs b/yazi-actor/src/mgr/rename.rs index 068f7e47..ef60c7a2 100644 --- a/yazi-actor/src/mgr/rename.rs +++ b/yazi-actor/src/mgr/rename.rs @@ -5,7 +5,7 @@ use yazi_fs::{File, FilesOp}; use yazi_macro::{act, err, ok_or_not_found, succ}; use yazi_parser::mgr::RenameOpt; use yazi_proxy::{ConfirmProxy, InputProxy, MgrProxy}; -use yazi_shared::{Id, data::Data, path::PathLike, url::{UrlBuf, UrlLike}}; +use yazi_shared::{Id, data::Data, path::PathLike, strand::StrandLike, url::{UrlBuf, UrlLike}}; use yazi_vfs::{VfsFile, maybe_exists, provider}; use yazi_watcher::WATCHER; @@ -49,7 +49,10 @@ impl Actor for Rename { return; } - let new = old.parent().unwrap().join(name); + let Some(Ok(new)) = old.parent().map(|u| u.try_join(name)) else { + return; + }; + if opt.force || !maybe_exists(&new).await || provider::must_identical(&old, &new).await { Self::r#do(tab, old, new).await.ok(); } else if ConfirmProxy::show(ConfirmCfg::overwrite(&new)).await { diff --git a/yazi-actor/src/mgr/search.rs b/yazi-actor/src/mgr/search.rs index 224531d6..44b7571b 100644 --- a/yazi-actor/src/mgr/search.rs +++ b/yazi-actor/src/mgr/search.rs @@ -52,7 +52,7 @@ impl Actor for SearchDo { handle.abort(); } - let cwd = tab.cwd().to_search(opt.subject.as_ref()); + let cwd = tab.cwd().to_search(opt.subject.as_ref())?; let hidden = tab.pref.show_hidden; tab.search = Some(tokio::spawn(async move { @@ -111,7 +111,7 @@ impl Actor for SearchStop { succ!(); } - let rep = tab.history.remove_or(tab.cwd().to_regular()); + let rep = tab.history.remove_or(tab.cwd().to_regular()?); drop(mem::replace(&mut tab.current, rep)); act!(mgr:hidden, cx)?; diff --git a/yazi-actor/src/mgr/tab_create.rs b/yazi-actor/src/mgr/tab_create.rs index 9ddbcea9..2461f6e2 100644 --- a/yazi-actor/src/mgr/tab_create.rs +++ b/yazi-actor/src/mgr/tab_create.rs @@ -29,10 +29,13 @@ impl Actor for TabCreate { (true, wd.into_owned()) } else if let Some(h) = cx.hovered() { tab.pref = cx.tab().pref.clone(); - (false, h.url.to_regular()) + (false, h.url.clone()) + } else if cx.cwd().is_search() { + tab.pref = cx.tab().pref.clone(); + (true, cx.cwd().to_regular()?) } else { tab.pref = cx.tab().pref.clone(); - (true, cx.cwd().to_regular()) + (true, cx.cwd().clone()) }; let tabs = &mut cx.mgr.tabs; diff --git a/yazi-actor/src/mgr/update_files.rs b/yazi-actor/src/mgr/update_files.rs index b785e38a..e2c1479e 100644 --- a/yazi-actor/src/mgr/update_files.rs +++ b/yazi-actor/src/mgr/update_files.rs @@ -17,7 +17,9 @@ impl Actor for UpdateFiles { fn act(cx: &mut Ctx, opt: Self::Options) -> Result { let revision = cx.current().files.revision; - let linked: Vec<_> = LINKED.read().from_dir(opt.op.cwd()).map(|u| opt.op.chdir(u)).collect(); + let linked: Vec<_> = + LINKED.read().from_dir(opt.op.cwd()).filter_map(|u| opt.op.chdir(u).ok()).collect(); + for op in [opt.op].into_iter().chain(linked) { cx.mgr.yanked.apply_op(&op); Self::update_tab(cx, op).ok(); diff --git a/yazi-adapter/src/image.rs b/yazi-adapter/src/image.rs index 7a058f6f..0eabe67e 100644 --- a/yazi-adapter/src/image.rs +++ b/yazi-adapter/src/image.rs @@ -39,7 +39,7 @@ impl Image { }) .await??; - Ok(Local.write(cache, buf).await?) + Ok(Local::regular(&cache).write(buf).await?) } pub(super) async fn downscale(path: PathBuf, rect: Rect) -> Result { diff --git a/yazi-adapter/src/lib.rs b/yazi-adapter/src/lib.rs index 3decaa14..cd0d3303 100644 --- a/yazi-adapter/src/lib.rs +++ b/yazi-adapter/src/lib.rs @@ -1,4 +1,4 @@ -#![allow(clippy::option_map_unit_fn, clippy::unit_arg)] +#![allow(clippy::option_map_unit_fn)] yazi_macro::mod_pub!(drivers); diff --git a/yazi-binding/src/lib.rs b/yazi-binding/src/lib.rs index 2d1ffdc5..aa051c39 100644 --- a/yazi-binding/src/lib.rs +++ b/yazi-binding/src/lib.rs @@ -1,7 +1,5 @@ -#![allow(clippy::unit_arg)] - mod macros; yazi_macro::mod_pub!(elements); -yazi_macro::mod_flat!(cha color composer error file icon id iter permit runtime scheme stage style url urn utils); +yazi_macro::mod_flat!(cha color composer error file icon id iter path permit runtime scheme stage style url utils); diff --git a/yazi-binding/src/macros.rs b/yazi-binding/src/macros.rs index 6bc50e69..7e5c69b4 100644 --- a/yazi-binding/src/macros.rs +++ b/yazi-binding/src/macros.rs @@ -151,10 +151,11 @@ macro_rules! impl_file_fields { ($fields:ident) => { $crate::cached_field!($fields, cha, |_, me| Ok($crate::Cha(me.cha))); $crate::cached_field!($fields, url, |_, me| Ok($crate::Url::new(me.url_owned()))); - $crate::cached_field!($fields, link_to, |_, me| Ok(me.link_to.clone().map($crate::Url::new))); + $crate::cached_field!($fields, link_to, |_, me| Ok(me.link_to_url().map($crate::Url::new))); $crate::cached_field!($fields, name, |lua, me| { - me.name().map(|s| lua.create_string(s.as_encoded_bytes())).transpose() + use yazi_shared::strand::StrandLike; + me.name().map(|s| lua.create_string(s.encoded_bytes())).transpose() }); $crate::cached_field!($fields, cache, |_, me| { use yazi_fs::FsUrl; diff --git a/yazi-binding/src/urn.rs b/yazi-binding/src/path.rs similarity index 56% rename from yazi-binding/src/urn.rs rename to yazi-binding/src/path.rs index 6463ca87..edcce546 100644 --- a/yazi-binding/src/urn.rs +++ b/yazi-binding/src/path.rs @@ -1,7 +1,7 @@ use std::ops::Deref; -use mlua::{ExternalError, FromLua, Lua, UserData, Value}; -use yazi_shared::path::PathBufDyn; +use mlua::{ExternalError, FromLua, Lua, MetaMethod, UserData, UserDataMethods, Value}; +use yazi_shared::path::{PathBufDyn, PathBufLike}; pub struct Path(pub PathBufDyn); @@ -24,4 +24,9 @@ impl FromLua for Path { } } -impl UserData for Path {} +impl UserData for Path { + fn add_methods>(methods: &mut M) { + methods + .add_meta_method(MetaMethod::ToString, |lua, me, ()| lua.create_string(me.encoded_bytes())); + } +} diff --git a/yazi-binding/src/scheme.rs b/yazi-binding/src/scheme.rs index defec5fc..8ceecc57 100644 --- a/yazi-binding/src/scheme.rs +++ b/yazi-binding/src/scheme.rs @@ -20,14 +20,14 @@ impl Deref for Scheme { } impl Scheme { - pub fn new(scheme: &yazi_shared::scheme::Scheme) -> Self { - Self { inner: scheme.clone(), v_kind: None, v_cache: None } + pub fn new(scheme: impl Into) -> Self { + Self { inner: scheme.into(), v_kind: None, v_cache: None } } } impl UserData for Scheme { fn add_fields>(fields: &mut F) { - cached_field!(fields, kind, |_, me| Ok(me.kind())); + cached_field!(fields, kind, |_, me| Ok(me.kind().as_str())); cached_field!(fields, cache, |_, me| Ok(me.cache().map(Url::new))); fields.add_field_method_get("is_virtual", |_, me| Ok(me.is_virtual())); diff --git a/yazi-binding/src/url.rs b/yazi-binding/src/url.rs index 4c75f7ea..c86d34f2 100644 --- a/yazi-binding/src/url.rs +++ b/yazi-binding/src/url.rs @@ -1,8 +1,8 @@ use std::ops::Deref; -use mlua::{AnyUserData, ExternalError, FromLua, Lua, MetaMethod, UserData, UserDataFields, UserDataMethods, UserDataRef, Value}; +use mlua::{AnyUserData, ExternalError, ExternalResult, FromLua, Lua, MetaMethod, UserData, UserDataFields, UserDataMethods, UserDataRef, Value}; use yazi_fs::{FsHash64, FsHash128}; -use yazi_shared::{IntoOsStr, scheme::SchemeLike, url::{AsUrl, UrlCow, UrlLike}}; +use yazi_shared::{path::{PathLike, StripPrefixError}, strand::{StrandCow, StrandLike}, url::{AsUrl, UrlCow, UrlLike}}; use crate::{Scheme, cached_field, deprecate}; @@ -99,26 +99,28 @@ impl FromLua for Url { impl UserData for Url { fn add_fields>(fields: &mut F) { cached_field!(fields, name, |lua, me| { - me.name().map(|s| lua.create_string(s.as_encoded_bytes())).transpose() + me.name().map(|s| lua.create_string(s.encoded_bytes())).transpose() }); cached_field!(fields, stem, |lua, me| { - me.stem().map(|s| lua.create_string(s.as_encoded_bytes())).transpose() + me.stem().map(|s| lua.create_string(s.encoded_bytes())).transpose() }); cached_field!(fields, ext, |lua, me| { - me.ext().map(|s| lua.create_string(s.as_encoded_bytes())).transpose() + me.ext().map(|s| lua.create_string(s.encoded_bytes())).transpose() }); cached_field!(fields, parent, |_, me| Ok(me.parent().map(Self::new))); cached_field!(fields, urn, |_, me| Ok(super::Path::new(me.urn()))); - cached_field!(fields, base, |_, me| Ok(me.base().map(Self::new))); + cached_field!(fields, base, |_, me| { + Ok(Some(me.base()).filter(|u| !u.loc().is_empty()).map(Self::new)) + }); - cached_field!(fields, scheme, |_, me| Ok(Scheme::new(&me.scheme))); + cached_field!(fields, scheme, |_, me| Ok(Scheme::new(me.scheme()))); cached_field!(fields, domain, |lua, me| { - me.scheme.domain().map(|s| lua.create_string(s)).transpose() + me.scheme().domain().map(|s| lua.create_string(s)).transpose() }); fields.add_field_method_get("frag", |lua, me| { deprecate!(lua, "`frag` property of Url is deprecated and renamed to `domain`, please use the new name instead, in your {}"); - me.scheme.domain().map(|s| lua.create_string(s)).transpose() + me.scheme().domain().map(|s| lua.create_string(s)).transpose() }); fields.add_field_method_get("is_regular", |_, me| Ok(me.is_regular())); @@ -138,42 +140,60 @@ impl UserData for Url { }); methods.add_method("join", |_, me, other: Value| { Ok(Self::new(match other { - Value::String(s) => me.join(s.as_bytes().into_os_str()?), + Value::String(s) => { + let b = s.as_bytes(); + me.try_join(StrandCow::with(me.kind(), &*b).into_lua_err()?).into_lua_err()? + } Value::UserData(ud) => { let url = ud.borrow::()?; - if !me.scheme.covariant(&url.scheme) { + if !me.scheme().covariant(url.scheme()) { return Err("cannot join Urls with different schemes".into_lua_err()); } - me.join(&url.loc) + me.try_join(url.loc()).into_lua_err()? } _ => Err("must be a string or Url".into_lua_err())?, })) }); methods.add_method("starts_with", |_, me, base: Value| { Ok(match base { - Value::String(s) => me.loc.starts_with(s.as_bytes().into_os_str()?), - Value::UserData(ud) => me.starts_with(&*ud.borrow::()?), + Value::String(s) => { + let b = s.as_bytes(); + me.loc().try_starts_with(&StrandCow::with(me.kind(), &*b).into_lua_err()?).into_lua_err() + } + Value::UserData(ud) => me.try_starts_with(&*ud.borrow::()?).into_lua_err(), _ => Err("must be a string or Url".into_lua_err())?, }) }); methods.add_method("ends_with", |_, me, child: Value| { Ok(match child { - Value::String(s) => me.loc.ends_with(s.as_bytes().into_os_str()?), - Value::UserData(ud) => me.ends_with(&*ud.borrow::()?), + Value::String(s) => { + let b = s.as_bytes(); + me.loc().try_ends_with(&StrandCow::with(me.kind(), &*b).into_lua_err()?).into_lua_err() + } + Value::UserData(ud) => me.try_ends_with(&*ud.borrow::()?).into_lua_err(), _ => Err("must be a string or Url".into_lua_err())?, }) }); methods.add_method("strip_prefix", |_, me, base: Value| { - let path = match base { - Value::String(s) => me.loc().strip_prefix(&s.as_bytes().into_os_str()?).map(Into::into), - Value::UserData(ud) => me.strip_prefix(&*ud.borrow::()?), + let strip = match base { + Value::String(s) => { + let b = s.as_bytes(); + me.loc().try_strip_prefix(&StrandCow::with(me.kind(), &*b).into_lua_err()?) + } + Value::UserData(ud) => me.try_strip_prefix(&*ud.borrow::()?), _ => Err("must be a string or Url".into_lua_err())?, }; - Ok(path.map(Self::new)) // TODO: return `Path` instead of `Url` + + Ok(match strip { + Ok(p) => Some(Self::new(p)), // TODO: return `Path` instead of `Url` + Err(StripPrefixError::Exotic | StripPrefixError::NotPrefix) => None, + Err(e @ StripPrefixError::WrongEncoding) => Err(e.into_lua_err())?, + }) }); methods.add_function_mut("into_search", |_, (ud, domain): (AnyUserData, mlua::String)| { - Ok(Self::new(ud.take::()?.inner.into_search(domain.to_str()?))) + let url = ud.take::()?.inner.into_search(domain.to_str()?).into_lua_err()?; + Ok(Self::new(url)) }); methods.add_meta_method(MetaMethod::Eq, |_, me, other: UrlRef| Ok(me.inner == other.inner)); diff --git a/yazi-boot/src/boot.rs b/yazi-boot/src/boot.rs index b08fb59d..2c123084 100644 --- a/yazi-boot/src/boot.rs +++ b/yazi-boot/src/boot.rs @@ -4,13 +4,13 @@ use futures::executor::block_on; use hashbrown::HashSet; use serde::Serialize; use yazi_fs::{CWD, Xdg, path::expand_url}; -use yazi_shared::{path::{PathBufDyn, PathLike}, url::{UrlBuf, UrlCow, UrlLike}}; +use yazi_shared::{strand::StrandBuf, url::{UrlBuf, UrlLike}}; use yazi_vfs::provider; #[derive(Debug, Default, Serialize)] pub struct Boot { pub cwds: Vec, - pub files: Vec, + pub files: Vec, pub local_events: HashSet, pub remote_events: HashSet, @@ -22,25 +22,28 @@ pub struct Boot { } impl Boot { - async fn parse_entries(entries: &[UrlBuf]) -> (Vec, Vec) { + async fn parse_entries(entries: &[UrlBuf]) -> (Vec, Vec) { if entries.is_empty() { - return (vec![CWD.load().as_ref().clone()], vec![PathBufDyn::os_default()]); + return (vec![CWD.load().as_ref().clone()], vec![Default::default()]); } - async fn go(entry: &UrlBuf) -> (UrlBuf, PathBufDyn) { + async fn go(entry: &UrlBuf) -> (UrlBuf, StrandBuf) { let mut entry = expand_url(entry); - if let Ok(u @ UrlCow::Owned { .. }) = provider::absolute(&entry).await { + + if let Ok(u) = provider::absolute(&entry).await + && u.is_owned() + { entry = u.into_owned(); } let Some((parent, child)) = entry.pair() else { - return (entry, PathBufDyn::os_default()); + return (entry, Default::default()); }; if provider::metadata(&entry).await.is_ok_and(|m| m.is_file()) { - (parent.into(), child.owned()) + (parent.into(), child.into()) } else { - (entry, PathBufDyn::os_default()) + (entry, Default::default()) } } diff --git a/yazi-cli/src/package/delete.rs b/yazi-cli/src/package/delete.rs index 02782bc0..4968e143 100644 --- a/yazi-cli/src/package/delete.rs +++ b/yazi-cli/src/package/delete.rs @@ -1,5 +1,5 @@ use anyhow::{Context, Result}; -use yazi_fs::{ok_or_not_found, provider::{DirReader, FileHolder, Provider, local::Local}}; +use yazi_fs::{ok_or_not_found, provider::{Provider, local::Local}}; use yazi_macro::outln; use super::Dependency; @@ -23,9 +23,9 @@ impl Dependency { pub(super) async fn delete_assets(&self) -> Result<()> { let assets = self.target().join("assets"); - match Local.read_dir(&assets).await { + match tokio::fs::read_dir(&assets).await { Ok(mut it) => { - while let Some(entry) = it.next().await? { + while let Some(entry) = it.next_entry().await? { remove_sealed(&entry.path()) .await .with_context(|| format!("failed to remove `{}`", entry.path().display()))?; @@ -35,7 +35,7 @@ impl Dependency { Err(e) => Err(e).context(format!("failed to read `{}`", assets.display()))?, }; - Local.remove_dir_clean(assets).await; + Local::regular(&assets).remove_dir_clean().await; Ok(()) } @@ -49,7 +49,7 @@ impl Dependency { .with_context(|| format!("failed to delete `{}`", path.display()))?; } - if ok_or_not_found(Local.remove_dir(&dir).await).is_ok() { + if ok_or_not_found(Local::regular(&dir).remove_dir().await).is_ok() { outln!("Done!")?; } else { outln!( diff --git a/yazi-cli/src/package/dependency.rs b/yazi-cli/src/package/dependency.rs index aee07cca..eb3eb146 100644 --- a/yazi-cli/src/package/dependency.rs +++ b/yazi-cli/src/package/dependency.rs @@ -3,7 +3,7 @@ use std::{io::BufWriter, path::{Path, PathBuf}, str::FromStr}; use anyhow::{Result, bail}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use twox_hash::XxHash3_128; -use yazi_fs::{Xdg, provider::{DirReader, FileHolder, Provider, local::Local}}; +use yazi_fs::Xdg; use yazi_shared::BytesExt; #[derive(Clone, Default)] @@ -62,11 +62,11 @@ impl Dependency { } pub(super) async fn plugin_files(dir: &Path) -> std::io::Result> { - let mut it = Local.read_dir(dir).await?; + let mut it = tokio::fs::read_dir(dir).await?; let mut files: Vec = ["LICENSE", "README.md", "main.lua"].into_iter().map(Into::into).collect(); - while let Some(entry) = it.next().await? { - if let Ok(name) = entry.name().into_owned().into_string() + while let Some(entry) = it.next_entry().await? { + if let Ok(name) = entry.file_name().into_string() && let Some(stripped) = name.strip_suffix(".lua") && stripped != "main" && stripped.as_bytes().kebab_cased() diff --git a/yazi-cli/src/package/deploy.rs b/yazi-cli/src/package/deploy.rs index 84d758bc..2cc10ac5 100644 --- a/yazi-cli/src/package/deploy.rs +++ b/yazi-cli/src/package/deploy.rs @@ -1,7 +1,7 @@ use std::path::{Path, PathBuf}; use anyhow::{Context, Result}; -use yazi_fs::provider::{DirReader, FileHolder, Provider, local::Local}; +use yazi_fs::provider::{Provider, local::Local}; use yazi_macro::outln; use super::Dependency; @@ -20,7 +20,7 @@ impl Dependency { self.hash_check().await?; } - Local.create_dir_all(&to).await?; + Local::regular(&to).create_dir_all().await?; self.delete_assets().await?; let res1 = Self::deploy_assets(from.join("assets"), to.join("assets")).await; @@ -30,7 +30,7 @@ impl Dependency { self.delete_sources().await?; } - Local.remove_dir_clean(to).await; + Local::regular(&to).remove_dir_clean().await; self.hash = self.hash().await?; res2?; res1?; @@ -40,11 +40,11 @@ impl Dependency { } async fn deploy_assets(from: PathBuf, to: PathBuf) -> Result<()> { - match Local.read_dir(&from).await { + match tokio::fs::read_dir(&from).await { Ok(mut it) => { - Local.create_dir_all(&to).await?; - while let Some(entry) = it.next().await? { - let (src, dist) = (entry.path(), to.join(entry.name())); + Local::regular(&to).create_dir_all().await?; + while let Some(entry) = it.next_entry().await? { + let (src, dist) = (entry.path(), to.join(entry.file_name())); copy_and_seal(&src, &dist).await.with_context(|| { format!("failed to copy `{}` to `{}`", src.display(), dist.display()) })?; diff --git a/yazi-cli/src/package/hash.rs b/yazi-cli/src/package/hash.rs index 88430f01..37fc5350 100644 --- a/yazi-cli/src/package/hash.rs +++ b/yazi-cli/src/package/hash.rs @@ -1,6 +1,6 @@ use anyhow::{Context, Result, bail}; use twox_hash::XxHash3_128; -use yazi_fs::provider::{DirReader, FileHolder, Provider, local::Local}; +use yazi_fs::provider::local::Local; use yazi_macro::ok_or_not_found; use super::Dependency; @@ -15,17 +15,17 @@ impl Dependency { for file in files { h.write(file.as_bytes()); h.write(b"VpvFw9Atb7cWGOdqhZCra634CcJJRlsRl72RbZeV0vpG1\0"); - h.write(&ok_or_not_found!(Local.read(dir.join(file)).await)); + h.write(&ok_or_not_found!(Local::regular(&dir.join(file)).read().await)); } let mut assets = vec![]; - match Local.read_dir(dir.join("assets")).await { + match tokio::fs::read_dir(dir.join("assets")).await { Ok(mut it) => { - while let Some(entry) = it.next().await? { - let Ok(name) = entry.name().into_owned().into_string() else { + while let Some(entry) = it.next_entry().await? { + let Ok(name) = entry.file_name().into_string() else { bail!("asset path is not valid UTF-8: {}", entry.path().display()); }; - assets.push((name, Local.read(entry.path()).await?)); + assets.push((name, Local::regular(&entry.path()).read().await?)); } } Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} diff --git a/yazi-cli/src/package/package.rs b/yazi-cli/src/package/package.rs index 617635da..dc654bc7 100644 --- a/yazi-cli/src/package/package.rs +++ b/yazi-cli/src/package/package.rs @@ -15,7 +15,7 @@ pub(crate) struct Package { impl Package { pub(crate) async fn load() -> Result { - let s = ok_or_not_found!(Local.read_to_string(Self::toml()).await); + let s = ok_or_not_found!(Local::regular(&Self::toml()).read_to_string().await); Ok(toml::from_str(&s)?) } @@ -132,7 +132,7 @@ impl Package { async fn save(&self) -> Result<()> { let s = toml::to_string_pretty(self)?; - Local.write(Self::toml(), s).await.context("Failed to write package.toml") + Local::regular(&Self::toml()).write(s).await.context("Failed to write package.toml") } fn toml() -> PathBuf { Xdg::config_dir().join("package.toml") } diff --git a/yazi-cli/src/shared/shared.rs b/yazi-cli/src/shared/shared.rs index f2077dd9..387b5ccb 100644 --- a/yazi-cli/src/shared/shared.rs +++ b/yazi-cli/src/shared/shared.rs @@ -6,19 +6,19 @@ use yazi_macro::ok_or_not_found; #[inline] pub async fn must_exists(path: impl AsRef) -> bool { - Local.symlink_metadata(path).await.is_ok() + Local::regular(&path).symlink_metadata().await.is_ok() } #[inline] pub async fn maybe_exists(path: impl AsRef) -> bool { - match Local.symlink_metadata(path).await { + match Local::regular(&path).symlink_metadata().await { Ok(_) => true, Err(e) => e.kind() != std::io::ErrorKind::NotFound, } } pub async fn copy_and_seal(from: &Path, to: &Path) -> io::Result<()> { - let b = Local.read(from).await?; + let b = Local::regular(from).read().await?; ok_or_not_found!(remove_sealed(to).await); let mut file = Gate::default().create_new(true).write(true).truncate(true).open(to).await?; @@ -31,7 +31,6 @@ pub async fn copy_and_seal(from: &Path, to: &Path) -> io::Result<()> { Ok(()) } -// TODO: use `yazi_fs` instead of `tokio::fs` pub async fn remove_sealed(p: &Path) -> io::Result<()> { #[cfg(windows)] { @@ -40,5 +39,5 @@ pub async fn remove_sealed(p: &Path) -> io::Result<()> { tokio::fs::set_permissions(p, perm).await?; } - Local.remove_file(p).await + Local::regular(p).remove_file().await } diff --git a/yazi-config/src/mgr/mgr.rs b/yazi-config/src/mgr/mgr.rs index 6be3ebfb..f18066c4 100644 --- a/yazi-config/src/mgr/mgr.rs +++ b/yazi-config/src/mgr/mgr.rs @@ -33,7 +33,7 @@ impl Mgr { } let home = UrlBuf::from(dirs::home_dir().unwrap_or_default()); - let cwd = if let Some(p) = CWD.load().strip_prefix(&home) { + let cwd = if let Ok(p) = CWD.load().try_strip_prefix(&home) { format!("~{}{}", std::path::MAIN_SEPARATOR, p.display()) } else { format!("{}", CWD.load().display()) diff --git a/yazi-config/src/pattern.rs b/yazi-config/src/pattern.rs index ab33b88c..72a52278 100644 --- a/yazi-config/src/pattern.rs +++ b/yazi-config/src/pattern.rs @@ -1,9 +1,9 @@ use std::{fmt::Debug, str::FromStr}; use anyhow::{Result, bail}; -use globset::GlobBuilder; +use globset::{Candidate, GlobBuilder}; use serde::Deserialize; -use yazi_shared::{scheme::SchemeRef, url::AsUrl}; +use yazi_shared::{path::PathLike, scheme::SchemeKind, url::AsUrl}; #[derive(Deserialize)] #[serde(try_from = "String")] @@ -28,28 +28,32 @@ impl Debug for Pattern { } impl Pattern { + // FIXME: simplify conditional compilation pub fn match_url(&self, url: impl AsUrl, is_dir: bool) -> bool { let url = url.as_url(); if is_dir != self.is_dir { return false; - } else if !self.scheme.matches(url.scheme) { + } else if !self.scheme.matches(url.kind()) { return false; } else if self.is_star { return true; } #[cfg(unix)] - let path = &url.loc; + { + self.inner.is_match_candidate(&Candidate::from_bytes(url.loc().encoded_bytes())) + } #[cfg(windows)] - let path = if self.sep_lit { - yazi_fs::path::backslash_to_slash(&url.loc) + if self.sep_lit { + use yazi_shared::strand::AsStrandDyn; + self.inner.is_match_candidate(&Candidate::from_bytes( + url.loc().as_strand_dyn().backslash_to_slash().encoded_bytes(), + )) } else { - std::borrow::Cow::Borrowed(url.loc.as_path()) - }; - - self.inner.is_match(path) + self.inner.is_match_candidate(&Candidate::from_bytes(url.loc().encoded_bytes())) + } } pub fn match_mime(&self, mime: impl AsRef) -> bool { @@ -144,18 +148,19 @@ impl PatternScheme { } #[inline] - fn matches(self, scheme: SchemeRef) -> bool { - use SchemeRef as S; - match (self, scheme) { + fn matches(self, kind: SchemeKind) -> bool { + use SchemeKind as K; + + match (self, kind) { (Self::Any, _) => true, (Self::Local, s) => s.is_local(), (Self::Remote, s) => s.is_remote(), (Self::Virtual, s) => s.is_virtual(), - (Self::Regular, S::Regular) => true, - (Self::Search, S::Search(_)) => true, - (Self::Archive, S::Archive(_)) => true, - (Self::Sftp, S::Sftp(_)) => true, + (Self::Regular, K::Regular) => true, + (Self::Search, K::Search) => true, + (Self::Archive, K::Archive) => true, + (Self::Sftp, K::Sftp) => true, _ => false, } diff --git a/yazi-config/src/preview/preview.rs b/yazi-config/src/preview/preview.rs index 50a02941..85a1e6c4 100644 --- a/yazi-config/src/preview/preview.rs +++ b/yazi-config/src/preview/preview.rs @@ -50,7 +50,7 @@ impl Preview { self.cache_dir = if self.cache_dir.as_os_str().is_empty() { Xdg::cache_dir().to_owned() - } else if let Some(p) = expand_url(self.cache_dir).into_path() { + } else if let Some(p) = expand_url(self.cache_dir).into_local() { p } else { bail!("[preview].cache_dir must be a path within local filesystem."); diff --git a/yazi-config/src/theme/icon.rs b/yazi-config/src/theme/icon.rs index 69892c2d..98d2c01e 100644 --- a/yazi-config/src/theme/icon.rs +++ b/yazi-config/src/theme/icon.rs @@ -5,7 +5,7 @@ use hashbrown::HashMap; use serde::{Deserialize, Deserializer}; use yazi_codegen::DeserializeOver2; use yazi_fs::File; -use yazi_shared::{Condition, url::UrlLike}; +use yazi_shared::{Condition, strand::StrandLike, url::UrlLike}; use crate::{Color, Icon as I, Pattern, Style}; @@ -74,7 +74,7 @@ impl Icon { } fn match_by_name(&self, file: &File) -> Option<&I> { - let name = file.name()?.to_str()?; + let name = file.name()?.to_str().ok()?; if file.is_dir() { self.dirs.get(name).or_else(|| self.dirs.get(&name.to_ascii_lowercase())) } else { @@ -87,7 +87,7 @@ impl Icon { } fn match_by_ext(&self, file: &File) -> Option<&I> { - let ext = file.url.ext()?.to_str()?; + let ext = file.url.ext()?.to_str().ok()?; self.exts.get(ext).or_else(|| self.exts.get(&ext.to_ascii_lowercase())) } } diff --git a/yazi-config/src/theme/theme.rs b/yazi-config/src/theme/theme.rs index b607dbb7..8617662e 100644 --- a/yazi-config/src/theme/theme.rs +++ b/yazi-config/src/theme/theme.rs @@ -229,7 +229,7 @@ impl Theme { self.mgr.syntect_theme = self .flavor .syntect_path(light) - .or_else(|| expand_url(UrlBuf::from(&self.mgr.syntect_theme)).into_path()) + .or_else(|| expand_url(UrlBuf::from(&self.mgr.syntect_theme)).into_local()) .ok_or(anyhow!("[mgr].syntect_theme must be a path within local filesystem"))?; Ok(self) diff --git a/yazi-config/src/vfs/provider.rs b/yazi-config/src/vfs/provider.rs index 841b47a6..5ae75cdc 100644 --- a/yazi-config/src/vfs/provider.rs +++ b/yazi-config/src/vfs/provider.rs @@ -45,7 +45,7 @@ impl ProviderSftp { fn reshape(&mut self) -> io::Result<()> { if !self.key_file.as_os_str().is_empty() { self.key_file = expand_url(&self.key_file) - .into_path() + .into_local() .ok_or_else(|| io::Error::other("key_file must be a path within local filesystem"))?; } @@ -56,7 +56,7 @@ impl ProviderSftp { .unwrap_or_default() } else { expand_url(&self.identity_agent) - .into_path() + .into_local() .ok_or_else(|| io::Error::other("identity_agent must be a path within local filesystem"))? }; diff --git a/yazi-core/src/lib.rs b/yazi-core/src/lib.rs index a9b8257a..4f94bf8b 100644 --- a/yazi-core/src/lib.rs +++ b/yazi-core/src/lib.rs @@ -1,9 +1,4 @@ -#![allow( - clippy::if_same_then_else, - clippy::len_without_is_empty, - clippy::option_map_unit_fn, - clippy::unit_arg -)] +#![allow(clippy::if_same_then_else, clippy::len_without_is_empty, clippy::option_map_unit_fn)] yazi_macro::mod_pub!(cmp confirm help input mgr notify pick spot tab tasks which); diff --git a/yazi-core/src/tab/selected.rs b/yazi-core/src/tab/selected.rs index 87951a92..f5272908 100644 --- a/yazi-core/src/tab/selected.rs +++ b/yazi-core/src/tab/selected.rs @@ -142,23 +142,16 @@ impl Selected { #[cfg(test)] mod tests { - use yazi_shared::{scheme::SchemeCow, url::UrlCow}; + use std::path::Path; use super::*; - fn url(s: &str) -> Url<'_> { - match UrlCow::try_from(s).unwrap() { - UrlCow::Borrowed { loc, scheme: SchemeCow::Borrowed(scheme) } => Url { loc, scheme }, - _ => unreachable!(), - } - } - #[test] fn test_insert_non_conflicting() { let mut s = Selected::default(); - assert!(s.add(url("/a/b"))); - assert!(s.add(url("/c/d"))); + assert!(s.add(Path::new("/a/b"))); + assert!(s.add(Path::new("/c/d"))); assert_eq!(s.inner.len(), 2); } @@ -166,27 +159,27 @@ mod tests { fn test_insert_conflicting_parent() { let mut s = Selected::default(); - assert!(s.add(url("/a"))); - assert!(!s.add(url("/a/b"))); + assert!(s.add(Path::new("/a"))); + assert!(!s.add(Path::new("/a/b"))); } #[test] fn test_insert_conflicting_child() { let mut s = Selected::default(); - assert!(s.add(url("/a/b/c"))); - assert!(!s.add(url("/a/b"))); - assert!(s.add(url("/a/b/d"))); + assert!(s.add(Path::new("/a/b/c"))); + assert!(!s.add(Path::new("/a/b"))); + assert!(s.add(Path::new("/a/b/d"))); } #[test] fn test_remove() { let mut s = Selected::default(); - assert!(s.add(url("/a/b"))); - assert!(!s.remove(url("/a/c"))); - assert!(s.remove(url("/a/b"))); - assert!(!s.remove(url("/a/b"))); + assert!(s.add(Path::new("/a/b"))); + assert!(!s.remove(Path::new("/a/c"))); + assert!(s.remove(Path::new("/a/b"))); + assert!(!s.remove(Path::new("/a/b"))); assert!(s.inner.is_empty()); assert!(s.parents.is_empty()); } @@ -197,7 +190,11 @@ mod tests { assert_eq!( 3, - s.add_same([url("/parent/child1"), url("/parent/child2"), url("/parent/child3")]) + s.add_same([ + Path::new("/parent/child1"), + Path::new("/parent/child2"), + Path::new("/parent/child3") + ]) ); } @@ -205,16 +202,16 @@ mod tests { fn insert_many_with_existing_parent_fails() { let mut s = Selected::default(); - s.add(url("/parent")); - assert_eq!(0, s.add_same([url("/parent/child1"), url("/parent/child2")])); + s.add(Path::new("/parent")); + assert_eq!(0, s.add_same([Path::new("/parent/child1"), Path::new("/parent/child2")])); } #[test] fn insert_many_with_existing_child_fails() { let mut s = Selected::default(); - s.add(url("/parent/child1")); - assert_eq!(2, s.add_same([url("/parent/child1"), url("/parent/child2")])); + s.add(Path::new("/parent/child1")); + assert_eq!(2, s.add_same([Path::new("/parent/child1"), Path::new("/parent/child2")])); } #[test] @@ -228,48 +225,51 @@ mod tests { fn insert_many_with_parent_as_child_of_another_url() { let mut s = Selected::default(); - s.add(url("/parent/child")); - assert_eq!(0, s.add_same([url("/parent/child/child1"), url("/parent/child/child2")])); + s.add(Path::new("/parent/child")); + assert_eq!( + 0, + s.add_same([Path::new("/parent/child/child1"), Path::new("/parent/child/child2")]) + ); } #[test] fn insert_many_with_direct_parent_fails() { let mut s = Selected::default(); - s.add(url("/a")); - assert_eq!(0, s.add_same([url("/a/b")])); + s.add(Path::new("/a")); + assert_eq!(0, s.add_same([Path::new("/a/b")])); } #[test] fn insert_many_with_nested_child_fails() { let mut s = Selected::default(); - s.add(url("/a/b")); - assert_eq!(0, s.add_same([url("/a")])); - assert_eq!(1, s.add_same([url("/b"), url("/a")])); + s.add(Path::new("/a/b")); + assert_eq!(0, s.add_same([Path::new("/a")])); + assert_eq!(1, s.add_same([Path::new("/b"), Path::new("/a")])); } #[test] fn insert_many_sibling_directories_success() { let mut s = Selected::default(); - assert_eq!(2, s.add_same([url("/a/b"), url("/a/c")])); + assert_eq!(2, s.add_same([Path::new("/a/b"), Path::new("/a/c")])); } #[test] fn insert_many_with_grandchild_fails() { let mut s = Selected::default(); - s.add(url("/a/b")); - assert_eq!(0, s.add_same([url("/a/b/c")])); + s.add(Path::new("/a/b")); + assert_eq!(0, s.add_same([Path::new("/a/b/c")])); } #[test] fn test_insert_many_with_remove() { let mut s = Selected::default(); - let child1 = url("/parent/child1"); - let child2 = url("/parent/child2"); - let child3 = url("/parent/child3"); + let child1 = Path::new("/parent/child1"); + let child2 = Path::new("/parent/child2"); + let child3 = Path::new("/parent/child3"); assert_eq!(3, s.add_same([child1, child2, child3])); assert!(s.remove(child1)); diff --git a/yazi-core/src/tasks/file.rs b/yazi-core/src/tasks/file.rs index b4033d36..1935a014 100644 --- a/yazi-core/src/tasks/file.rs +++ b/yazi-core/src/tasks/file.rs @@ -8,9 +8,12 @@ use crate::mgr::Yanked; impl Tasks { pub fn file_cut(&self, src: &Yanked, dest: &UrlBuf, force: bool) { for u in src.iter() { - let to = dest.join(u.name().unwrap()); + let Some(Ok(to)) = u.name().map(|n| dest.try_join(n)) else { + debug!("file_cut: cannot join {u:?} with {dest:?}"); + continue; + }; if force && *u == to { - debug!("file_cut: same file, skipping {:?}", to); + debug!("file_cut: same file, skip {to:?}"); } else { self.scheduler.file_cut(u.0.clone(), to, force); } @@ -19,9 +22,12 @@ impl Tasks { pub fn file_copy(&self, src: &Yanked, dest: &UrlBuf, force: bool, follow: bool) { for u in src.iter() { - let to = dest.join(u.name().unwrap()); + let Some(Ok(to)) = u.name().map(|n| dest.try_join(n)) else { + debug!("file_copy: cannot join {u:?} with {dest:?}"); + continue; + }; if force && *u == to { - debug!("file_copy: same file, skipping {:?}", to); + debug!("file_copy: same file, skip {to:?}"); } else { self.scheduler.file_copy(u.0.clone(), to, force, follow); } @@ -30,9 +36,12 @@ impl Tasks { pub fn file_link(&self, src: &HashSet, dest: &UrlBuf, relative: bool, force: bool) { for u in src { - let to = dest.join(u.name().unwrap()); + let Some(Ok(to)) = u.name().map(|n| dest.try_join(n)) else { + debug!("file_link: cannot join {u:?} with {dest:?}"); + continue; + }; if force && *u == to { - debug!("file_link: same file, skipping {:?}", to); + debug!("file_link: same file, skip {to:?}"); } else { self.scheduler.file_link(u.0.clone(), to, relative, force); } @@ -41,9 +50,12 @@ impl Tasks { pub fn file_hardlink(&self, src: &HashSet, dest: &UrlBuf, force: bool, follow: bool) { for u in src { - let to = dest.join(u.name().unwrap()); + let Some(Ok(to)) = u.name().map(|n| dest.try_join(n)) else { + debug!("file_hardlink: cannot join {u:?} with {dest:?}"); + continue; + }; if force && *u == to { - debug!("file_hardlink: same file, skipping {:?}", to); + debug!("file_hardlink: same file, skip {to:?}"); } else { self.scheduler.file_hardlink(u.0.clone(), to, force, follow); } diff --git a/yazi-dds/src/ember/bulk.rs b/yazi-dds/src/ember/bulk.rs index c4482b55..53343f8e 100644 --- a/yazi-dds/src/ember/bulk.rs +++ b/yazi-dds/src/ember/bulk.rs @@ -1,21 +1,19 @@ -use std::borrow::Cow; - use hashbrown::HashMap; use mlua::{IntoLua, Lua, Value}; use serde::{Deserialize, Serialize}; -use yazi_shared::url::UrlBuf; +use yazi_shared::url::{Url, UrlCow}; use super::Ember; #[derive(Debug, Deserialize, Serialize)] pub struct EmberBulk<'a> { - pub changes: HashMap, Cow<'a, UrlBuf>>, + pub changes: HashMap, UrlCow<'a>>, } impl<'a> EmberBulk<'a> { pub fn borrowed(changes: I) -> Ember<'a> where - I: Iterator, + I: Iterator, Url<'a>)>, { Self { changes: changes.map(|(from, to)| (from.into(), to.into())).collect() }.into() } @@ -24,10 +22,12 @@ impl<'a> EmberBulk<'a> { impl EmberBulk<'static> { pub fn owned<'a, I>(changes: I) -> Ember<'static> where - I: Iterator, + I: Iterator, Url<'a>)>, { - Self { changes: changes.map(|(from, to)| (from.clone().into(), to.clone().into())).collect() } - .into() + Self { + changes: changes.map(|(from, to)| (from.to_owned().into(), to.to_owned().into())).collect(), + } + .into() } } diff --git a/yazi-dds/src/pubsub.rs b/yazi-dds/src/pubsub.rs index 1d3d945e..ab55aac0 100644 --- a/yazi-dds/src/pubsub.rs +++ b/yazi-dds/src/pubsub.rs @@ -4,7 +4,7 @@ use mlua::Function; use parking_lot::RwLock; use yazi_boot::BOOT; use yazi_fs::FolderStage; -use yazi_shared::{Id, RoCell, url::{UrlBuf, UrlBufCov}}; +use yazi_shared::{Id, RoCell, url::{Url, UrlBuf, UrlBufCov}}; use crate::{Client, ID, PEERS, ember::{BodyMoveItem, Ember, EmberBulk, EmberHi}}; @@ -123,7 +123,7 @@ impl Pubsub { pub fn pub_after_bulk<'a, I>(changes: I) -> Result<()> where - I: Iterator + Clone, + I: Iterator, Url<'a>)> + Clone, { if BOOT.local_events.contains("bulk") { EmberBulk::borrowed(changes.clone()).with_receiver(*ID).flush()?; diff --git a/yazi-dds/src/state.rs b/yazi-dds/src/state.rs index 757bc4b4..590181d3 100644 --- a/yazi-dds/src/state.rs +++ b/yazi-dds/src/state.rs @@ -58,7 +58,7 @@ impl State { return Ok(()); } - Local.create_dir_all(&BOOT.state_dir).await?; + Local::regular(&BOOT.state_dir).create_dir_all().await?; let mut buf = BufWriter::new( Gate::default() .write(true) @@ -79,7 +79,7 @@ impl State { } async fn load(&self) -> Result<()> { - let mut file = BufReader::new(Local.open(BOOT.state_dir.join(".dds")).await?); + let mut file = BufReader::new(Local::regular(&BOOT.state_dir.join(".dds")).open().await?); let mut buf = String::new(); let mut inner = HashMap::new(); @@ -103,7 +103,7 @@ impl State { } async fn skip(&self) -> Result { - let cha = Local.symlink_metadata(BOOT.state_dir.join(".dds")).await?; + let cha = Local::regular(&BOOT.state_dir.join(".dds")).symlink_metadata().await?; let modified = cha.mtime_dur()?.as_micros(); Ok(modified >= self.last.load(Ordering::Relaxed) as u128) } diff --git a/yazi-dds/src/stream.rs b/yazi-dds/src/stream.rs index ede51d00..e4b5b6e9 100644 --- a/yazi-dds/src/stream.rs +++ b/yazi-dds/src/stream.rs @@ -40,7 +40,7 @@ impl Stream { let p = Self::socket_file(); - yazi_fs::provider::local::Local.remove_file(&p).await.ok(); + yazi_fs::provider::local::Local::regular(&p).remove_file().await.ok(); tokio::net::UnixListener::bind(p) } diff --git a/yazi-fm/src/app/commands/bootstrap.rs b/yazi-fm/src/app/commands/bootstrap.rs index 574906a5..af83c5b2 100644 --- a/yazi-fm/src/app/commands/bootstrap.rs +++ b/yazi-fm/src/app/commands/bootstrap.rs @@ -3,7 +3,7 @@ use yazi_actor::Ctx; use yazi_boot::BOOT; use yazi_macro::act; use yazi_parser::{VoidOpt, mgr::CdSource}; -use yazi_shared::{data::Data, path::PathBufLike, url::UrlLike}; +use yazi_shared::{data::Data, strand::StrandBufLike, url::UrlLike}; use crate::app::App; @@ -20,8 +20,8 @@ impl App { if file.is_empty() { act!(mgr:cd, cx, (BOOT.cwds[i].clone(), CdSource::Tab))?; - } else { - act!(mgr:reveal, cx, (BOOT.cwds[i].join(file), CdSource::Tab))?; + } else if let Ok(u) = BOOT.cwds[i].try_join(file) { + act!(mgr:reveal, cx, (u, CdSource::Tab))?; } } diff --git a/yazi-fm/src/app/commands/quit.rs b/yazi-fm/src/app/commands/quit.rs index 37efe25d..ecdc1342 100644 --- a/yazi-fm/src/app/commands/quit.rs +++ b/yazi-fm/src/app/commands/quit.rs @@ -26,13 +26,13 @@ impl App { async fn cwd_to_file(&self, no: bool) { if let Some(p) = ARGS.cwd_file.as_ref().filter(|_| !no) { let cwd = self.core.mgr.cwd().os_str(); - Local.write(p, cwd.as_encoded_bytes()).await.ok(); + Local::regular(p).write(cwd.as_encoded_bytes()).await.ok(); } } async fn selected_to_file(&self, selected: Option) { if let (Some(s), Some(p)) = (selected, &ARGS.chooser_file) { - Local.write(p, s.as_encoded_bytes()).await.ok(); + Local::regular(p).write(s.as_encoded_bytes()).await.ok(); } } } diff --git a/yazi-fm/src/cmp/cmp.rs b/yazi-fm/src/cmp/cmp.rs index 3fb0f714..f06fb1c0 100644 --- a/yazi-fm/src/cmp/cmp.rs +++ b/yazi-fm/src/cmp/cmp.rs @@ -4,6 +4,7 @@ use ratatui::{buffer::Buffer, layout::Rect, widgets::{Block, BorderType, List, L use yazi_adapter::Dimension; use yazi_config::{THEME, popup::{Offset, Position}}; use yazi_core::Core; +use yazi_shared::strand::{AsStrand, StrandLike}; pub(crate) struct Cmp<'a> { core: &'a Core, @@ -25,7 +26,7 @@ impl Widget for Cmp<'_> { let icon = if x.is_dir { &THEME.cmp.icon_folder } else { &THEME.cmp.icon_file }; let slash = if x.is_dir { MAIN_SEPARATOR_STR } else { "" }; - let mut item = ListItem::new(format!(" {icon} {}{slash}", x.name.display())); + let mut item = ListItem::new(format!(" {icon} {}{slash}", x.name.as_strand().display())); if i == self.core.cmp.rel_cursor() { item = item.style(THEME.cmp.active); } else { diff --git a/yazi-fm/src/dispatcher.rs b/yazi-fm/src/dispatcher.rs index ebc2bd73..e75abc7d 100644 --- a/yazi-fm/src/dispatcher.rs +++ b/yazi-fm/src/dispatcher.rs @@ -2,6 +2,7 @@ use std::sync::atomic::Ordering; use anyhow::Result; use crossterm::event::KeyEvent; +use tracing::warn; use yazi_config::keymap::Key; use yazi_macro::{act, emit, succ}; use yazi_shared::{data::Data, event::{CmdCow, Event, NEED_RENDER}}; @@ -20,7 +21,7 @@ impl<'a> Dispatcher<'a> { #[inline] pub(super) fn dispatch(&mut self, event: Event) -> Result<()> { // FIXME: handle errors - _ = match event { + let result = match event { Event::Call(cmd) => self.dispatch_call(cmd), Event::Seq(cmds) => self.dispatch_seq(cmds), Event::Render => self.dispatch_render(), @@ -30,6 +31,10 @@ impl<'a> Dispatcher<'a> { Event::Paste(str) => self.dispatch_paste(str), Event::Quit(opt) => act!(quit, self.app, opt), }; + + if let Err(err) = result { + warn!("Event dispatch error: {err:?}"); + } Ok(()) } diff --git a/yazi-fm/src/main.rs b/yazi-fm/src/main.rs index 64ae41e5..bd0032bb 100644 --- a/yazi-fm/src/main.rs +++ b/yazi-fm/src/main.rs @@ -1,4 +1,4 @@ -#![allow(clippy::if_same_then_else, clippy::unit_arg)] +#![allow(clippy::if_same_then_else)] #[cfg(all(not(target_os = "macos"), not(target_os = "windows")))] #[global_allocator] diff --git a/yazi-fs/src/cha/cha.rs b/yazi-fs/src/cha/cha.rs index 469e4f54..f64003e7 100644 --- a/yazi-fs/src/cha/cha.rs +++ b/yazi-fs/src/cha/cha.rs @@ -1,8 +1,8 @@ -use std::{ffi::OsStr, fs::Metadata, ops::Deref, time::{Duration, SystemTime, UNIX_EPOCH}}; +use std::{fs::Metadata, ops::Deref, time::{Duration, SystemTime, UNIX_EPOCH}}; use anyhow::bail; use yazi_macro::{unix_either, win_either}; -use yazi_shared::url::AsUrl; +use yazi_shared::{strand::AsStrand, url::AsUrl}; use super::ChaKind; use crate::cha::{ChaMode, ChaType}; @@ -48,7 +48,10 @@ impl Default for Cha { impl Cha { #[inline] - pub fn new(name: &OsStr, meta: Metadata) -> Self { + pub fn new(name: T, meta: Metadata) -> Self + where + T: AsStrand, + { Self::from_bare(&meta).attach(ChaKind::hidden(name, &meta)) } diff --git a/yazi-fs/src/cha/kind.rs b/yazi-fs/src/cha/kind.rs index d1d9e367..4e2736f9 100644 --- a/yazi-fs/src/cha/kind.rs +++ b/yazi-fs/src/cha/kind.rs @@ -1,6 +1,7 @@ -use std::{ffi::OsStr, fs::Metadata}; +use std::fs::Metadata; use bitflags::bitflags; +use yazi_shared::strand::AsStrand; bitflags! { #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] @@ -14,13 +15,16 @@ bitflags! { impl ChaKind { #[inline] - pub(super) fn hidden(_name: &OsStr, _meta: &Metadata) -> Self { + pub(super) fn hidden(_name: T, _meta: &Metadata) -> Self + where + T: AsStrand, + { let mut me = Self::empty(); #[cfg(unix)] { - use std::os::unix::ffi::OsStrExt; - if _name.as_bytes().starts_with(b".") { + use yazi_shared::strand::StrandLike; + if _name.as_strand().starts_with(".") { me |= Self::HIDDEN; } } diff --git a/yazi-fs/src/cwd.rs b/yazi-fs/src/cwd.rs index d1193ffb..c1156dd0 100644 --- a/yazi-fs/src/cwd.rs +++ b/yazi-fs/src/cwd.rs @@ -28,10 +28,7 @@ impl Default for Cwd { } impl Cwd { - pub fn path(&self) -> PathBuf { - let url = self.0.load(); - url.cache().unwrap_or_else(|| url.loc.to_path()) - } + pub fn path(&self) -> PathBuf { self.0.load().as_url().unified_path().into_owned() } pub fn set(&self, url: &UrlBuf, callback: fn()) -> bool { if !url.is_absolute() { @@ -50,7 +47,7 @@ impl Cwd { use std::{io::ErrorKind::{AlreadyExists, NotADirectory, NotFound}, path::Component as C}; let Some(cache) = url.cache() else { - return url.loc.as_path().into(); + return url.unified_path(); }; if !matches!(std::fs::create_dir_all(&cache), Err(e) if e.kind() == NotADirectory || e.kind() == AlreadyExists) diff --git a/yazi-fs/src/file.rs b/yazi-fs/src/file.rs index 771bd0fc..7e3414c3 100644 --- a/yazi-fs/src/file.rs +++ b/yazi-fs/src/file.rs @@ -1,6 +1,7 @@ -use std::{ffi::OsStr, hash::{Hash, Hasher}, ops::Deref, path::{Path, PathBuf}}; +use std::{hash::{Hash, Hasher}, ops::Deref, path::Path}; -use yazi_shared::{path::PathDyn, url::{UrlBuf, UrlLike}}; +use anyhow::Result; +use yazi_shared::{loc::Loc, path::{AsPathDyn, PathBufDyn, PathDyn}, strand::Strand, url::{Url, UrlBuf, UrlLike}}; use crate::cha::{Cha, ChaType}; @@ -8,7 +9,7 @@ use crate::cha::{Cha, ChaType}; pub struct File { pub url: UrlBuf, pub cha: Cha, - pub link_to: Option, + pub link_to: Option, } impl Deref for File { @@ -26,8 +27,8 @@ impl File { } #[inline] - pub fn chdir(&self, wd: &Path) -> Self { - Self { url: self.url.rebase(wd), cha: self.cha, link_to: self.link_to.clone() } + pub fn chdir(&self, wd: &Path) -> Result { + Ok(Self { url: self.url.rebase(wd)?, cha: self.cha, link_to: self.link_to.clone() }) } } @@ -43,10 +44,27 @@ impl File { pub fn urn(&self) -> PathDyn<'_> { self.url.urn() } #[inline] - pub fn name(&self) -> Option<&OsStr> { self.url.name() } + pub fn name(&self) -> Option> { self.url.name() } #[inline] - pub fn stem(&self) -> Option<&OsStr> { self.url.stem() } + pub fn stem(&self) -> Option> { self.url.stem() } + + pub fn link_to_url(&self) -> Option> { + let to = self.link_to.as_ref()?.as_path_dyn(); + let kind = self.url.kind(); + Some(match &self.url { + UrlBuf::Regular(_) => Url::Regular(Loc::bare(to.as_os().ok()?)), + UrlBuf::Search { domain, .. } => { + Url::Search { loc: Loc::saturated(to.as_os().ok()?, kind), domain } + } + UrlBuf::Archive { domain, .. } => { + Url::Archive { loc: Loc::saturated(to.as_os().ok()?, kind), domain } + } + UrlBuf::Sftp { domain, .. } => { + Url::Sftp { loc: Loc::saturated(to.as_os().ok()?, kind), domain } + } + }) + } } impl Hash for File { diff --git a/yazi-fs/src/files.rs b/yazi-fs/src/files.rs index 557c4891..937e9e9a 100644 --- a/yazi-fs/src/files.rs +++ b/yazi-fs/src/files.rs @@ -1,7 +1,7 @@ use std::{mem, ops::{Deref, DerefMut, Not}}; use hashbrown::{HashMap, HashSet}; -use yazi_shared::{Id, path::{PathBufDyn, PathBufLike, PathDyn, PathLike}}; +use yazi_shared::{Id, path::{PathBufDyn, PathDyn, PathLike}}; use super::{FilesSorter, Filter}; use crate::{FILES_TICKET, File, SortBy}; @@ -121,6 +121,7 @@ impl Files { #[cfg(unix)] pub fn update_deleting(&mut self, urns: HashSet) -> Vec { + use yazi_shared::path::PathBufLike; if urns.is_empty() { return vec![]; } diff --git a/yazi-fs/src/filter.rs b/yazi-fs/src/filter.rs index 8bb1394e..8bed557d 100644 --- a/yazi-fs/src/filter.rs +++ b/yazi-fs/src/filter.rs @@ -1,8 +1,8 @@ -use std::{ffi::OsStr, fmt::Display, ops::Range}; +use std::{fmt::Display, ops::Range}; use anyhow::Result; use regex::bytes::{Regex, RegexBuilder}; -use yazi_shared::{event::Cmd, path::{AsPath, PathLike}}; +use yazi_shared::{event::Cmd, strand::{AsStrand, StrandLike}}; pub struct Filter { raw: String, @@ -26,14 +26,14 @@ impl Filter { #[allow(private_bounds)] pub fn matches(&self, name: T) -> bool where - T: AsPath, + T: AsStrand, { - self.regex.is_match(name.as_path().encoded_bytes()) + self.regex.is_match(name.as_strand().encoded_bytes()) } #[inline] - pub fn highlighted(&self, name: impl AsRef) -> Option>> { - self.regex.find(name.as_ref().as_encoded_bytes()).map(|m| vec![m.range()]) + pub fn highlighted(&self, name: impl AsStrand) -> Option>> { + self.regex.find(name.as_strand().encoded_bytes()).map(|m| vec![m.range()]) } } diff --git a/yazi-fs/src/lib.rs b/yazi-fs/src/lib.rs index bec80dd3..52fd93ce 100644 --- a/yazi-fs/src/lib.rs +++ b/yazi-fs/src/lib.rs @@ -1,4 +1,4 @@ -#![allow(clippy::if_same_then_else, clippy::option_map_unit_fn, clippy::unit_arg)] +#![allow(clippy::if_same_then_else, clippy::option_map_unit_fn)] yazi_macro::mod_pub!(cha error mounts path provider); diff --git a/yazi-fs/src/op.rs b/yazi-fs/src/op.rs index 0e5d82b2..f6d111bd 100644 --- a/yazi-fs/src/op.rs +++ b/yazi-fs/src/op.rs @@ -1,5 +1,6 @@ use std::path::Path; +use anyhow::Result; use hashbrown::{HashMap, HashSet}; use yazi_macro::relay; use yazi_shared::{Id, Ids, path::{PathBufDyn, PathLike}, url::{UrlBuf, UrlLike}}; @@ -98,16 +99,21 @@ impl FilesOp { } } - pub fn chdir(&self, wd: &Path) -> Self { + pub fn chdir(&self, wd: &Path) -> Result { macro_rules! files { - ($files:expr) => {{ $files.iter().map(|file| file.chdir(wd)).collect() }}; + ($files:expr) => {{ $files.iter().map(|file| file.chdir(wd)).collect::>()? }}; } macro_rules! map { - ($map:expr) => {{ $map.iter().map(|(urn, file)| (urn.clone(), file.chdir(wd))).collect() }}; + ($map:expr) => {{ + $map + .iter() + .map(|(urn, file)| file.chdir(wd).map(|file| (urn.clone(), file))) + .collect::>()? + }}; } let w = UrlBuf::from(wd); - match self { + Ok(match self { Self::Full(_, files, cha) => Self::Full(w, files!(files), *cha), Self::Part(_, files, ticket) => Self::Part(w, files!(files), *ticket), Self::Done(_, cha, ticket) => Self::Done(w, *cha, *ticket), @@ -118,16 +124,18 @@ impl FilesOp { Self::Deleting(_, urns) => Self::Deleting(w, urns.clone()), Self::Updating(_, map) => Self::Updating(w, map!(map)), Self::Upserting(_, map) => Self::Upserting(w, map!(map)), - } + }) } pub fn diff_recoverable(&self, contains: impl Fn(&UrlBuf) -> bool) -> (Vec, Vec) { match self { - Self::Deleting(cwd, urns) => (urns.iter().map(|u| cwd.join(u)).collect(), vec![]), + Self::Deleting(cwd, urns) => { + (urns.iter().filter_map(|u| cwd.try_join(u).ok()).collect(), vec![]) + } Self::Updating(cwd, urns) | Self::Upserting(cwd, urns) => urns .iter() .filter(|&(u, f)| u != f.urn()) - .map(|(u, f)| (cwd.join(u), f)) + .filter_map(|(u, f)| cwd.try_join(u).ok().map(|u| (u, f))) .filter(|(u, _)| contains(u)) .map(|(u, f)| (u, f.url_owned())) .unzip(), diff --git a/yazi-fs/src/path/clean.rs b/yazi-fs/src/path/clean.rs index 495a5388..a2ef3f74 100644 --- a/yazi-fs/src/path/clean.rs +++ b/yazi-fs/src/path/clean.rs @@ -1,21 +1,33 @@ -use std::path::{Path, PathBuf}; +use std::path::PathBuf; -use yazi_shared::{loc::LocBuf, url::{UrlBuf, UrlCow}}; +use yazi_shared::{loc::LocBuf, path::{PathDyn, PathLike}, pool::InternStr, url::{AsUrl, Url, UrlBuf, UrlCow, UrlLike}}; pub fn clean_url<'a>(url: impl Into>) -> UrlBuf { let cow: UrlCow = url.into(); let (path, uri, urn) = clean_path_impl( - &cow.loc(), - cow.loc().base().components().count(), - cow.loc().trail().components().count(), + cow.loc(), + cow.base().components().count() - 1, + cow.trail().components().count() - 1, ); - let loc = - LocBuf::::with(path, uri, urn).expect("Failed to create Loc from cleaned path"); - UrlBuf { loc, scheme: cow.into_scheme().into() } + match cow.as_url() { + Url::Regular(_) => UrlBuf::Regular(path.into()), + Url::Search { domain, .. } => UrlBuf::Search { + loc: LocBuf::::with(path, uri, urn).expect("create Loc from cleaned path"), + domain: domain.intern(), + }, + Url::Archive { domain, .. } => UrlBuf::Archive { + loc: LocBuf::::with(path, uri, urn).expect("create Loc from cleaned path"), + domain: domain.intern(), + }, + Url::Sftp { domain, .. } => UrlBuf::Sftp { + loc: LocBuf::::with(path, uri, urn).expect("create Loc from cleaned path"), + domain: domain.intern(), + }, + } } -fn clean_path_impl(path: &Path, base: usize, trail: usize) -> (PathBuf, usize, usize) { +fn clean_path_impl(path: PathDyn, base: usize, trail: usize) -> (PathBuf, usize, usize) { use std::path::Component::*; let mut out = vec![]; diff --git a/yazi-fs/src/path/expand.rs b/yazi-fs/src/path/expand.rs index 8dfd2fdf..5baba83f 100644 --- a/yazi-fs/src/path/expand.rs +++ b/yazi-fs/src/path/expand.rs @@ -1,6 +1,6 @@ -use std::{borrow::Cow, ffi::{OsStr, OsString}, path::{Path, PathBuf}}; +use std::{borrow::Cow, path::PathBuf}; -use yazi_shared::{loc::LocBuf, path::PathLike, url::{AsUrl, Url, UrlBuf, UrlCow}}; +use yazi_shared::{FromWtf8Vec, loc::LocBuf, path::{AsPath, PathBufDyn, PathCow, PathDyn, PathLike}, pool::InternStr, url::{AsUrl, Url, UrlBuf, UrlCow, UrlLike}}; use crate::{CWD, path::clean_url}; @@ -10,33 +10,44 @@ pub fn expand_url<'a>(url: impl Into>) -> UrlBuf { } fn expand_url_impl<'a>(url: Url<'a>) -> UrlCow<'a> { - let (o_base, o_rest, o_urn) = url.loc.triple(); + let (o_base, o_rest, o_urn) = url.triple(); let n_base = expand_variables(o_base); let n_rest = expand_variables(o_rest); let n_urn = expand_variables(o_urn); - let rest_diff = n_rest.components().count() as isize - o_rest.components().count() as isize; - let urn_diff = n_urn.components().count() as isize - o_urn.components().count() as isize; + let rest_diff = + n_rest.as_path().components().count() as isize - o_rest.components().count() as isize; + let urn_diff = + n_urn.as_path().components().count() as isize - o_urn.components().count() as isize; let uri_count = url.uri().components().count() as isize; let urn_count = url.urn().components().count() as isize; + let mut path = PathBufDyn::with_capacity( + url.kind(), + n_base.as_path().len() + n_rest.as_path().len() + n_urn.as_path().len(), + ); + path.try_extend([n_base, n_rest, n_urn]).expect("extend original parts should not fail"); + let loc = LocBuf::::with( - PathBuf::from_iter([n_base, n_rest, n_urn]), + 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 url = UrlBuf { loc, scheme: url.scheme.into() }; - match absolute_url(url.as_url()) { - UrlCow::Borrowed { .. } => url.into(), - c @ UrlCow::Owned { .. } => c.into_owned().into(), - } + 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, .. } => UrlBuf::Sftp { loc, domain: domain.intern() }, + }; + + absolute_url(expanded) } -fn expand_variables(p: &Path) -> Cow<'_, Path> { +fn expand_variables<'a>(p: PathDyn<'a>) -> PathCow<'a> { // ${HOME} or $HOME #[cfg(unix)] let re = regex::bytes::Regex::new(r"\$(?:\{([^}]+)\}|([a-zA-Z\d_]+))").unwrap(); @@ -45,7 +56,7 @@ fn expand_variables(p: &Path) -> Cow<'_, Path> { #[cfg(windows)] let re = regex::bytes::Regex::new(r"%([^%]+)%").unwrap(); - let b = p.as_os_str().as_encoded_bytes(); + let b = p.encoded_bytes(); let b = re.replace_all(b, |caps: ®ex::bytes::Captures| { let name = caps.get(2).or_else(|| caps.get(1)).unwrap(); str::from_utf8(name.as_bytes()) @@ -54,51 +65,58 @@ fn expand_variables(p: &Path) -> Cow<'_, Path> { .map_or_else(|| caps.get(0).unwrap().as_bytes().to_owned(), |s| s.into_encoded_bytes()) }); - unsafe { - match b { - Cow::Borrowed(b) => Path::new(OsStr::from_encoded_bytes_unchecked(b)).into(), - Cow::Owned(b) => PathBuf::from(OsString::from_encoded_bytes_unchecked(b)).into(), + match (b, p) { + (Cow::Borrowed(_), _) => p.into(), + (Cow::Owned(b), PathDyn::Os(_)) => { + PathBufDyn::Os(std::path::PathBuf::from_wtf8_vec(b).expect("valid WTF-8 path")).into() } } } -pub fn absolute_url<'a>(url: Url<'a>) -> UrlCow<'a> { - if url.scheme.is_virtual() { +pub fn absolute_url<'a>(url: impl Into>) -> UrlCow<'a> { absolute_url_impl(url.into()) } + +fn absolute_url_impl<'a>(url: UrlCow<'a>) -> UrlCow<'a> { + if url.kind().is_virtual() { return url.into(); } - let b = url.loc.as_os_str().as_encoded_bytes(); - if cfg!(windows) && b.len() == 2 && b[1] == b':' && b[0].is_ascii_alphabetic() { - let loc = LocBuf::::with( + let path = url.loc().as_os().expect("must be a local path"); + let b = path.as_os_str().as_encoded_bytes(); + + let loc = if cfg!(windows) && b.len() == 2 && b[1] == b':' && b[0].is_ascii_alphabetic() { + LocBuf::::with( format!(r"{}:\", b[0].to_ascii_uppercase() as char).into(), if url.has_base() { 0 } else { 2 }, if url.has_trail() { 0 } else { 2 }, ) - .expect("Failed to create Loc from drive letter"); - UrlBuf { loc, scheme: url.scheme.into() }.into() - } else if let Some(rest) = url.loc.strip_prefix("~/") + .expect("Failed to create Loc from drive letter") + } else if let Ok(rest) = path.strip_prefix("~/") && let Some(home) = dirs::home_dir() && home.is_absolute() { let add = home.components().count() - 1; // Home root ("~") has offset by the absolute root ("/") - let loc = LocBuf::::with( + LocBuf::::with( home.join(rest), url.uri().components().count() + if url.has_base() { 0 } else { add }, url.urn().components().count() + if url.has_trail() { 0 } else { add }, ) - .expect("Failed to create Loc from home directory"); - UrlBuf { loc, scheme: url.scheme.into() }.into() + .expect("Failed to create Loc from home directory") } else if !url.is_absolute() { let cwd = CWD.path(); - let loc = LocBuf::::with( - cwd.join(url.loc), + LocBuf::::with( + cwd.join(path), url.uri().components().count(), url.urn().components().count(), ) - .expect("Failed to create Loc from relative path"); - UrlBuf { loc, scheme: url.scheme.into() }.into() + .expect("Failed to create Loc from relative path") } else { - url.into() + return url; + }; + + match url.as_url() { + Url::Regular(_) => UrlBuf::Regular(loc).into(), + Url::Search { domain, .. } => UrlBuf::Search { loc, domain: domain.intern() }.into(), + Url::Archive { .. } | Url::Sftp { .. } => unreachable!(), } } diff --git a/yazi-fs/src/path/path.rs b/yazi-fs/src/path/path.rs index 1f3803ec..088cf438 100644 --- a/yazi-fs/src/path/path.rs +++ b/yazi-fs/src/path/path.rs @@ -12,28 +12,6 @@ pub fn skip_url(url: &UrlBuf, n: usize) -> Cow<'_, OsStr> { it.os_str() } -#[cfg(windows)] -pub fn backslash_to_slash(p: &std::path::Path) -> Cow<'_, std::path::Path> { - use std::{ffi::OsString, path::PathBuf}; - let bytes = p.as_os_str().as_encoded_bytes(); - - // Fast path to skip if there are no backslashes - let skip_len = bytes.iter().take_while(|&&b| b != b'\\').count(); - if skip_len >= bytes.len() { - return Cow::Borrowed(p); - } - - let (skip, rest) = bytes.split_at(skip_len); - let mut out = Vec::new(); - out.try_reserve_exact(bytes.len()).unwrap_or_else(|_| panic!()); - out.extend(skip); - - for &b in rest { - out.push(if b == b'\\' { b'/' } else { b }); - } - Cow::Owned(PathBuf::from(unsafe { OsString::from_encoded_bytes_unchecked(out) })) -} - #[cfg(test)] mod tests { use yazi_shared::url::{AsUrl, UrlCow}; diff --git a/yazi-fs/src/path/percent.rs b/yazi-fs/src/path/percent.rs index a2c97300..98a5e299 100644 --- a/yazi-fs/src/path/percent.rs +++ b/yazi-fs/src/path/percent.rs @@ -1,7 +1,7 @@ use std::{borrow::Cow, path::{Path, PathBuf}}; use percent_encoding::{AsciiSet, CONTROLS, percent_decode, percent_encode}; -use yazi_shared::loc::Loc; +use yazi_shared::path::PathDyn; const SET: &AsciiSet = &CONTROLS.add(b'"').add(b'*').add(b':').add(b'<').add(b'>').add(b'?').add(b'\\').add(b'|'); @@ -28,8 +28,16 @@ impl PercentEncoding for Path { } } -impl PercentEncoding for Loc<'_> { - fn percent_encode(&self) -> Cow<'_, Path> { self.as_path().percent_encode() } +impl PercentEncoding for PathDyn<'_> { + fn percent_encode(&self) -> Cow<'_, Path> { + match self { + PathDyn::Os(p) => p.percent_encode(), + } + } - fn percent_decode(&self) -> Cow<'_, [u8]> { self.as_path().percent_decode() } + fn percent_decode(&self) -> Cow<'_, [u8]> { + match self { + PathDyn::Os(p) => p.percent_decode(), + } + } } diff --git a/yazi-fs/src/path/relative.rs b/yazi-fs/src/path/relative.rs index d584e908..674ccb69 100644 --- a/yazi-fs/src/path/relative.rs +++ b/yazi-fs/src/path/relative.rs @@ -1,19 +1,17 @@ -use std::{borrow::Cow, path::{Path, PathBuf}}; +use std::path::PathBuf; use anyhow::{Result, bail}; -use yazi_shared::{loc::LocBuf, url::{UrlBuf, UrlCow, UrlLike}}; +use yazi_shared::{path::PathBufDyn, url::{UrlCow, UrlLike}}; -pub fn path_relative_to<'a>( - from: impl AsRef, - to: &'a impl AsRef, -) -> Result> { - Ok(match url_relative_to(from.as_ref().into(), to.as_ref().into())? { - UrlCow::Borrowed { loc, .. } => Cow::Borrowed(loc.as_path()), - UrlCow::Owned { loc, .. } => Cow::Owned(loc.into_path()), - }) +pub fn url_relative_to<'a, 'b, U, V>(from: U, to: V) -> Result> +where + U: Into>, + V: Into>, +{ + url_relative_to_(from.into(), to.into()) } -pub(super) fn url_relative_to<'a>(from: UrlCow<'_>, to: UrlCow<'a>) -> Result> { +fn url_relative_to_<'a>(from: UrlCow<'_>, to: UrlCow<'a>) -> Result> { use yazi_shared::url::Component::*; if from.is_absolute() != to.is_absolute() { @@ -25,7 +23,7 @@ pub(super) fn url_relative_to<'a>(from: UrlCow<'_>, to: UrlCow<'a>) -> Result::zeroed("."), scheme: to.scheme().into() }.into()); + return UrlCow::try_from((to.scheme().zeroed().to_owned(), PathBufDyn::with(to.kind(), ".")?)); } let (mut f_it, mut t_it) = (from.components(), to.components()); @@ -46,6 +44,7 @@ pub(super) fn url_relative_to<'a>(from: UrlCow<'_>, to: UrlCow<'a>) -> Result::zeroed(buf), scheme: to.scheme().into() }.into()) + let buf: PathBuf = dots.chain(rest).collect(); // FIXME: remove PathBuf + let buf = PathBufDyn::from(buf); + UrlCow::try_from((to.scheme().zeroed().to_owned(), buf)) } diff --git a/yazi-fs/src/provider/local/dir_entry.rs b/yazi-fs/src/provider/local/dir_entry.rs index d8985e5b..aa126a85 100644 --- a/yazi-fs/src/provider/local/dir_entry.rs +++ b/yazi-fs/src/provider/local/dir_entry.rs @@ -1,18 +1,47 @@ -use std::{borrow::Cow, ffi::OsStr, io, path::PathBuf}; +use std::{io, sync::Arc}; + +use yazi_shared::{path::PathBufDyn, strand::StrandCow, url::{UrlBuf, UrlLike}}; use crate::{cha::{Cha, ChaType}, provider::FileHolder}; -pub struct DirEntry(pub(super) tokio::fs::DirEntry); +pub enum DirEntry { + Regular(tokio::fs::DirEntry), + Others { entry: tokio::fs::DirEntry, dir: Arc }, +} impl FileHolder for DirEntry { - fn path(&self) -> PathBuf { self.0.path() } - - fn name(&self) -> Cow<'_, OsStr> { self.0.file_name().into() } - - async fn metadata(&self) -> io::Result { - let name = self.name(); // TODO: use `file_name_os_str` when stabilized - Ok(Cha::new(&name, self.0.metadata().await?)) + async fn file_type(&self) -> io::Result { + match self { + Self::Regular(entry) | Self::Others { entry, .. } => entry.file_type().await.map(Into::into), + } } - async fn file_type(&self) -> io::Result { self.0.file_type().await.map(Into::into) } + async fn metadata(&self) -> io::Result { + let meta = match self { + Self::Regular(entry) | Self::Others { entry, .. } => entry.metadata().await?, + }; + + Ok(Cha::new(self.name(), meta)) // TODO: use `file_name_os_str` when stabilized + } + + fn name(&self) -> StrandCow<'_> { + match self { + Self::Regular(entry) | Self::Others { entry, .. } => entry.file_name().into(), + } + } + + fn path(&self) -> PathBufDyn { + match self { + Self::Regular(entry) | Self::Others { entry, .. } => entry.path().into(), + } + } + + fn url(&self) -> UrlBuf { + match self { + Self::Regular(entry) => entry.path().into(), + Self::Others { entry, dir } => { + dir.try_join(entry.file_name()).expect("entry name is a valid component of the local URL") + } + } + } } diff --git a/yazi-fs/src/provider/local/gate.rs b/yazi-fs/src/provider/local/gate.rs index dfa21949..2136b2ec 100644 --- a/yazi-fs/src/provider/local/gate.rs +++ b/yazi-fs/src/provider/local/gate.rs @@ -1,6 +1,6 @@ -use std::{io, path::Path}; +use std::io; -use yazi_shared::scheme::SchemeRef; +use yazi_shared::url::AsUrl; use crate::provider::{Attrs, FileBuilder}; @@ -33,19 +33,16 @@ impl FileBuilder for Gate { self } - async fn new(scheme: SchemeRef<'_>) -> io::Result { - if scheme.is_local() { - Ok(Self::default()) - } else { - Err(io::Error::new(io::ErrorKind::InvalidInput, "Not a local filesystem"))? - } - } - - async fn open

(&self, path: P) -> io::Result + async fn open(&self, url: U) -> io::Result where - P: AsRef, + U: AsUrl, { - self.0.open(path).await + let url = url.as_url(); + if let Some(path) = url.as_local() { + self.0.open(path).await + } else { + Err(io::Error::new(io::ErrorKind::InvalidInput, format!("Not a local URL: {url:?}"))) + } } fn read(&mut self, read: bool) -> &mut Self { diff --git a/yazi-fs/src/provider/local/local.rs b/yazi-fs/src/provider/local/local.rs index 49d297c2..d79e79e6 100644 --- a/yazi-fs/src/provider/local/local.rs +++ b/yazi-fs/src/provider/local/local.rs @@ -1,206 +1,176 @@ -use std::{io, path::{Path, PathBuf}}; +use std::{io, path::{Path, PathBuf}, sync::Arc}; -use yazi_shared::url::{AsUrl, UrlCow}; +use yazi_shared::{path::{AsPathDyn, PathBufDyn}, scheme::SchemeKind, url::{Url, UrlBuf, UrlCow}}; use crate::{cha::Cha, path::absolute_url, provider::{Attrs, Provider}}; #[derive(Clone, Copy)] -pub struct Local; +pub struct Local<'a> { + url: Url<'a>, + path: &'a Path, +} -impl Provider for Local { +impl<'a> Provider for Local<'a> { type File = tokio::fs::File; type Gate = super::Gate; + type Me<'b> = Local<'b>; type ReadDir = super::ReadDir; + type UrlCow = UrlCow<'a>; - async fn absolute<'a, U>(&self, url: &'a U) -> io::Result> - where - U: AsUrl, - { - let url = url.as_url(); - if url.scheme.is_local() { - Ok(absolute_url(url)) - } else { - Err(io::Error::new(io::ErrorKind::InvalidInput, "Not a local URL")) - } + async fn absolute(&self) -> io::Result { Ok(absolute_url(self.url)) } + + #[inline] + async fn canonicalize(&self) -> io::Result { + tokio::fs::canonicalize(self.path).await.map(Into::into) + } + + async fn casefold(&self) -> io::Result { + super::casefold(self.path).await.map(Into::into) } #[inline] - async fn canonicalize

(&self, path: P) -> io::Result + async fn copy

(&self, to: P, attrs: Attrs) -> io::Result where - P: AsRef, + P: AsPathDyn, { - tokio::fs::canonicalize(path).await - } - - async fn casefold

(&self, path: P) -> io::Result - where - P: AsRef, - { - super::casefold(path).await - } - - #[inline] - async fn copy(&self, from: P, to: Q, attrs: Attrs) -> io::Result - where - P: AsRef, - Q: AsRef, - { - let from = from.as_ref().to_owned(); - let to = to.as_ref().to_owned(); + let to = to.as_path_dyn().to_os_owned()?; + let from = self.path.to_owned(); Self::copy_impl(from, to, attrs).await } #[inline] - async fn create_dir

(&self, path: P) -> io::Result<()> + async fn create_dir(&self) -> io::Result<()> { tokio::fs::create_dir(self.path).await } + + #[inline] + async fn create_dir_all(&self) -> io::Result<()> { tokio::fs::create_dir_all(self.path).await } + + #[inline] + async fn hard_link

(&self, to: P) -> io::Result<()> where - P: AsRef, + P: AsPathDyn, { - tokio::fs::create_dir(path).await + let to = to.as_path_dyn().as_os()?; + + tokio::fs::hard_link(self.path, to).await } #[inline] - async fn create_dir_all

(&self, path: P) -> io::Result<()> - where - P: AsRef, - { - tokio::fs::create_dir_all(path).await + async fn metadata(&self) -> io::Result { + Ok(Cha::new(self.path.file_name().unwrap_or_default(), tokio::fs::metadata(self.path).await?)) } #[inline] - async fn gate(&self) -> io::Result { Ok(Self::Gate::default()) } - - #[inline] - async fn hard_link(&self, original: P, link: Q) -> io::Result<()> - where - P: AsRef, - Q: AsRef, - { - tokio::fs::hard_link(original, link).await + async fn new<'b>(url: Url<'b>) -> io::Result> { + match url { + Url::Regular(loc) | Url::Search { loc, .. } => Ok(Self::Me { url, path: loc.as_path() }), + Url::Archive { .. } | Url::Sftp { .. } => { + Err(io::Error::new(io::ErrorKind::InvalidInput, format!("Not a local URL: {url:?}"))) + } + } } #[inline] - async fn metadata

(&self, path: P) -> io::Result - where - P: AsRef, - { - let path = path.as_ref(); - Ok(Cha::new(path.file_name().unwrap_or_default(), tokio::fs::metadata(path).await?)) + async fn read_dir(self) -> io::Result { + Ok(match self.url.kind() { + SchemeKind::Regular => Self::ReadDir::Regular(tokio::fs::read_dir(self.path).await?), + SchemeKind::Search => Self::ReadDir::Others { + reader: tokio::fs::read_dir(self.path).await?, + dir: Arc::new(self.url.to_owned()), + }, + SchemeKind::Archive | SchemeKind::Sftp => Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("Not a local URL: {:?}", self.url), + ))?, + }) } #[inline] - async fn read_dir

(&self, path: P) -> io::Result - where - P: AsRef, - { - tokio::fs::read_dir(path).await.map(super::ReadDir) + async fn read_link(&self) -> io::Result { + Ok(tokio::fs::read_link(self.path).await?.into()) } #[inline] - async fn read_link

(&self, path: P) -> io::Result + async fn remove_dir(&self) -> io::Result<()> { tokio::fs::remove_dir(self.path).await } + + #[inline] + async fn remove_dir_all(&self) -> io::Result<()> { tokio::fs::remove_dir_all(self.path).await } + + #[inline] + async fn remove_file(&self) -> io::Result<()> { tokio::fs::remove_file(self.path).await } + + #[inline] + async fn rename

(&self, to: P) -> io::Result<()> where - P: AsRef, + P: AsPathDyn, { - tokio::fs::read_link(path).await + let to = to.as_path_dyn().as_os()?; + + tokio::fs::rename(self.path, to).await } #[inline] - async fn remove_dir

(&self, path: P) -> io::Result<()> + async fn symlink(&self, original: P, _is_dir: F) -> io::Result<()> where - P: AsRef, - { - tokio::fs::remove_dir(path).await - } - - #[inline] - async fn remove_dir_all

(&self, path: P) -> io::Result<()> - where - P: AsRef, - { - tokio::fs::remove_dir_all(path).await - } - - #[inline] - async fn remove_file

(&self, path: P) -> io::Result<()> - where - P: AsRef, - { - tokio::fs::remove_file(path).await - } - - #[inline] - async fn rename(&self, from: P, to: Q) -> io::Result<()> - where - P: AsRef, - Q: AsRef, - { - tokio::fs::rename(from, to).await - } - - #[inline] - async fn symlink(&self, original: P, link: Q, _is_dir: F) -> io::Result<()> - where - P: AsRef, - Q: AsRef, + P: AsPathDyn, F: AsyncFnOnce() -> io::Result, { #[cfg(unix)] { - tokio::fs::symlink(original, link).await + let original = original.as_path_dyn().as_os()?; + tokio::fs::symlink(original, self.path).await } #[cfg(windows)] if _is_dir().await? { - self.symlink_dir(original, link).await + self.symlink_dir(original).await } else { - self.symlink_file(original, link).await + self.symlink_file(original).await } } #[inline] - async fn symlink_dir(&self, original: P, link: Q) -> io::Result<()> + async fn symlink_dir

(&self, original: P) -> io::Result<()> where - P: AsRef, - Q: AsRef, + P: AsPathDyn, { + let original = original.as_path_dyn().as_os()?; + #[cfg(unix)] { - tokio::fs::symlink(original, link).await + tokio::fs::symlink(original, self.path).await } #[cfg(windows)] { - tokio::fs::symlink_dir(original, link).await + tokio::fs::symlink_dir(original, self.path).await } } #[inline] - async fn symlink_file(&self, original: P, link: Q) -> io::Result<()> + async fn symlink_file

(&self, original: P) -> io::Result<()> where - P: AsRef, - Q: AsRef, + P: AsPathDyn, { + let original = original.as_path_dyn().as_os()?; + #[cfg(unix)] { - tokio::fs::symlink(original, link).await + tokio::fs::symlink(original, self.path).await } #[cfg(windows)] { - tokio::fs::symlink_file(original, link).await + tokio::fs::symlink_file(original, self.path).await } } #[inline] - async fn symlink_metadata

(&self, path: P) -> io::Result - where - P: AsRef, - { - let path = path.as_ref(); - Ok(Cha::new(path.file_name().unwrap_or_default(), tokio::fs::symlink_metadata(path).await?)) + async fn symlink_metadata(&self) -> io::Result { + Ok(Cha::new( + self.path.file_name().unwrap_or_default(), + tokio::fs::symlink_metadata(self.path).await?, + )) } - async fn trash

(&self, path: P) -> io::Result<()> - where - P: AsRef, - { - let path = path.as_ref().to_owned(); + async fn trash(&self) -> io::Result<()> { + let path = self.path.to_owned(); tokio::task::spawn_blocking(move || { #[cfg(target_os = "android")] { @@ -222,16 +192,18 @@ impl Provider for Local { } #[inline] - async fn write(&self, path: P, contents: C) -> io::Result<()> + fn url(&self) -> Url<'_> { self.url } + + #[inline] + async fn write(&self, contents: C) -> io::Result<()> where - P: AsRef, C: AsRef<[u8]>, { - tokio::fs::write(path, contents).await + tokio::fs::write(self.path, contents).await } } -impl Local { +impl<'a> Local<'a> { async fn copy_impl(from: PathBuf, to: PathBuf, attrs: Attrs) -> io::Result { #[cfg(any(target_os = "linux", target_os = "android"))] { @@ -273,18 +245,18 @@ impl Local { } #[inline] - pub async fn read

(&self, path: P) -> io::Result> - where - P: AsRef, - { - tokio::fs::read(path).await + pub async fn read(&self) -> io::Result> { tokio::fs::read(self.path).await } + + #[inline] + pub async fn read_to_string(&self) -> io::Result { + tokio::fs::read_to_string(self.path).await } #[inline] - pub async fn read_to_string

(&self, path: P) -> io::Result + pub fn regular

(path: &'a P) -> Self where - P: AsRef, + P: ?Sized + AsRef, { - tokio::fs::read_to_string(path).await + Self { url: Url::regular(path), path: path.as_ref() } } } diff --git a/yazi-fs/src/provider/local/read_dir.rs b/yazi-fs/src/provider/local/read_dir.rs index 88790809..78ee07c9 100644 --- a/yazi-fs/src/provider/local/read_dir.rs +++ b/yazi-fs/src/provider/local/read_dir.rs @@ -1,13 +1,23 @@ -use std::io; +use std::{io, sync::Arc}; + +use yazi_shared::url::UrlBuf; use crate::provider::DirReader; -pub struct ReadDir(pub(super) tokio::fs::ReadDir); +pub enum ReadDir { + Regular(tokio::fs::ReadDir), + Others { reader: tokio::fs::ReadDir, dir: Arc }, +} impl DirReader for ReadDir { type Entry = super::DirEntry; async fn next(&mut self) -> io::Result> { - self.0.next_entry().await.map(|entry| entry.map(super::DirEntry)) + Ok(match self { + Self::Regular(reader) => reader.next_entry().await?.map(Self::Entry::Regular), + Self::Others { reader, dir } => { + reader.next_entry().await?.map(|entry| Self::Entry::Others { entry, dir: dir.clone() }) + } + }) } } diff --git a/yazi-fs/src/provider/traits.rs b/yazi-fs/src/provider/traits.rs index d5f5be44..52ed3ea6 100644 --- a/yazi-fs/src/provider/traits.rs +++ b/yazi-fs/src/provider/traits.rs @@ -1,75 +1,62 @@ -use std::{borrow::Cow, ffi::OsStr, io, path::{Path, PathBuf}}; +use std::io; use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; use yazi_macro::ok_or_not_found; -use yazi_shared::{scheme::SchemeRef, url::{AsUrl, UrlCow}}; +use yazi_shared::{path::{AsPathDyn, PathBufDyn, PathLike}, strand::StrandCow, url::{AsUrl, Url, UrlBuf}}; use crate::{cha::{Cha, ChaType}, provider::Attrs}; -pub trait Provider { +pub trait Provider: Sized { type File: AsyncRead + AsyncWrite + Unpin; type Gate: FileBuilder; - type ReadDir: DirReader; + type ReadDir: DirReader + 'static; + type UrlCow; + type Me<'a>: Provider; - fn absolute<'a, U>(&self, url: &'a U) -> impl Future>> - where - U: AsUrl; + fn absolute(&self) -> impl Future>; - fn canonicalize

(&self, path: P) -> impl Future> - where - P: AsRef; + fn canonicalize(&self) -> impl Future>; - fn casefold

(&self, path: P) -> impl Future> - where - P: AsRef; + fn casefold(&self) -> impl Future>; - fn copy(&self, from: P, to: Q, attrs: Attrs) -> impl Future> + fn copy

(&self, to: P, attrs: Attrs) -> impl Future> where - P: AsRef, - Q: AsRef; + P: AsPathDyn; - fn create

(&self, path: P) -> impl Future> - where - P: AsRef, - { - async move { self.gate().await?.write(true).create(true).truncate(true).open(path).await } + fn create(&self) -> impl Future> { + async move { self.gate().write(true).create(true).truncate(true).open(self.url()).await } } - fn create_dir

(&self, path: P) -> impl Future> - where - P: AsRef; + fn create_dir(&self) -> impl Future>; - fn create_dir_all

(&self, path: P) -> impl Future> - where - P: AsRef, - { + fn create_dir_all(&self) -> impl Future> { async move { - let mut path = path.as_ref(); - if path == Path::new("") { + let mut url = self.url(); + if url.loc().is_empty() { return Ok(()); } let mut stack = Vec::new(); loop { - match self.create_dir(path).await { + match Self::new(url).await?.create_dir().await { Ok(()) => break, Err(e) if e.kind() == io::ErrorKind::NotFound => { - if let Some(parent) = path.parent() { - stack.push(path); - path = parent; + if let Some(parent) = url.parent() { + stack.push(url); + url = parent; } else { return Err(io::Error::other("failed to create whole tree")); } } - Err(_) if self.metadata(path).await.is_ok_and(|m| m.is_dir()) => break, + Err(_) if Self::new(url).await?.metadata().await.is_ok_and(|m| m.is_dir()) => break, Err(e) => return Err(e), } } - while let Some(p) = stack.pop() { - match self.create_dir(p).await { + while let Some(u) = stack.pop() { + match Self::new(u).await?.create_dir().await { Ok(()) => {} - Err(_) if self.metadata(p).await.is_ok_and(|m| m.is_dir()) => {} + Err(_) if Self::new(u).await?.metadata().await.is_ok_and(|m| m.is_dir()) => {} Err(e) => return Err(e), } } @@ -78,87 +65,74 @@ pub trait Provider { } } - fn gate(&self) -> impl Future>; + fn gate(&self) -> Self::Gate { Self::Gate::default() } - fn hard_link(&self, original: P, link: Q) -> impl Future> + fn hard_link

(&self, to: P) -> impl Future> where - P: AsRef, - Q: AsRef; + P: AsPathDyn; - fn metadata

(&self, path: P) -> impl Future> - where - P: AsRef; + fn metadata(&self) -> impl Future>; - fn open

(&self, path: P) -> impl Future> - where - P: AsRef, - { - async move { self.gate().await?.read(true).open(path).await } + fn new<'a>(url: Url<'a>) -> impl Future>>; + + fn open(&self) -> impl Future> { + async move { self.gate().read(true).open(self.url()).await } } - fn read_dir

(&self, path: P) -> impl Future> - where - P: AsRef; + fn read_dir(self) -> impl Future>; - fn read_link

(&self, path: P) -> impl Future> - where - P: AsRef; + fn read_link(&self) -> impl Future>; - fn remove_dir

(&self, path: P) -> impl Future> - where - P: AsRef; + fn remove_dir(&self) -> impl Future>; - fn remove_dir_all

(&self, path: P) -> impl Future> - where - P: AsRef, - { - async fn remove_dir_all_impl

(me: &P, path: &Path) -> io::Result<()> + fn remove_dir_all(&self) -> impl Future> { + async fn remove_dir_all_impl

(url: Url<'_>) -> io::Result<()> where - P: Provider + ?Sized, + P: Provider, { - let mut it = ok_or_not_found!(me.read_dir(path).await, return Ok(())); + let mut it = ok_or_not_found!(P::new(url).await?.read_dir().await, return Ok(())); while let Some(child) = it.next().await? { let ft = ok_or_not_found!(child.file_type().await, continue); let result = if ft.is_dir() { - Box::pin(remove_dir_all_impl(me, &child.path())).await + Box::pin(remove_dir_all_impl::

(child.url().as_url())).await } else { - me.remove_file(&child.path()).await + P::new(child.url().as_url()).await?.remove_file().await }; () = ok_or_not_found!(result); } - Ok(ok_or_not_found!(me.remove_dir(path).await)) + Ok(ok_or_not_found!(P::new(url).await?.remove_dir().await)) } async move { - let path = path.as_ref(); - let cha = ok_or_not_found!(self.symlink_metadata(path).await, return Ok(())); + let cha = ok_or_not_found!(self.symlink_metadata().await, return Ok(())); if cha.is_link() { - self.remove_file(path).await + self.remove_file().await } else { - remove_dir_all_impl(self, path).await + remove_dir_all_impl::(self.url()).await } } } - fn remove_dir_clean

(&self, root: P) -> impl Future - where - P: AsRef, - { - let root = root.as_ref().to_path_buf(); + fn remove_dir_clean(&self) -> impl Future { + let root = self.url().to_owned(); async move { let mut stack = vec![(root, false)]; while let Some((dir, visited)) = stack.pop() { + let Ok(provider) = Self::new(dir.as_url()).await else { + continue; + }; + if visited { - self.remove_dir(&dir).await.ok(); - } else if let Ok(mut it) = self.read_dir(&dir).await { + provider.remove_dir().await.ok(); + } else if let Ok(mut it) = provider.read_dir().await { stack.push((dir, true)); while let Ok(Some(ent)) = it.next().await { if ent.file_type().await.is_ok_and(|t| t.is_dir()) { - stack.push((ent.path(), false)); + stack.push((ent.url(), false)); } } } @@ -166,56 +140,42 @@ pub trait Provider { } } - fn remove_file

(&self, path: P) -> impl Future> - where - P: AsRef; + fn remove_file(&self) -> impl Future>; - fn rename(&self, from: P, to: Q) -> impl Future> + fn rename

(&self, to: P) -> impl Future> where - P: AsRef, - Q: AsRef; + P: AsPathDyn; - fn symlink( - &self, - original: P, - link: Q, - _is_dir: F, - ) -> impl Future> + fn symlink(&self, original: P, _is_dir: F) -> impl Future> where - P: AsRef, - Q: AsRef, + P: AsPathDyn, F: AsyncFnOnce() -> io::Result; - fn symlink_dir(&self, original: P, link: Q) -> impl Future> + fn symlink_dir

(&self, original: P) -> impl Future> where - P: AsRef, - Q: AsRef, + P: AsPathDyn, { - self.symlink(original, link, async || Ok(true)) + self.symlink(original, async || Ok(true)) } - fn symlink_file(&self, original: P, link: Q) -> impl Future> + fn symlink_file

(&self, original: P) -> impl Future> where - P: AsRef, - Q: AsRef, + P: AsPathDyn, { - self.symlink(original, link, async || Ok(false)) + self.symlink(original, async || Ok(false)) } - fn symlink_metadata

(&self, path: P) -> impl Future> - where - P: AsRef; + fn symlink_metadata(&self) -> impl Future>; - fn trash

(&self, path: P) -> impl Future> - where - P: AsRef; + fn trash(&self) -> impl Future>; - fn write(&self, path: P, contents: C) -> impl Future> + fn url(&self) -> Url<'_>; + + fn write(&self, contents: C) -> impl Future> where - P: AsRef, C: AsRef<[u8]>, { - async move { self.create(path).await?.write_all(contents.as_ref()).await } + async move { self.create().await?.write_all(contents.as_ref()).await } } } @@ -223,24 +183,30 @@ pub trait Provider { pub trait DirReader { type Entry: FileHolder; + #[must_use] fn next(&mut self) -> impl Future>>; } // --- FileHolder pub trait FileHolder { #[must_use] - fn path(&self) -> PathBuf; + fn file_type(&self) -> impl Future>; #[must_use] - fn name(&self) -> Cow<'_, OsStr>; - fn metadata(&self) -> impl Future>; - fn file_type(&self) -> impl Future>; + #[must_use] + fn name(&self) -> StrandCow<'_>; + + #[must_use] + fn path(&self) -> PathBufDyn; + + #[must_use] + fn url(&self) -> UrlBuf; } -// --- FileOpener -pub trait FileBuilder { +// --- FileBuilder +pub trait FileBuilder: Sized + Default { type File: AsyncRead + AsyncWrite + Unpin; fn append(&mut self, append: bool) -> &mut Self; @@ -251,13 +217,9 @@ pub trait FileBuilder { fn create_new(&mut self, create_new: bool) -> &mut Self; - fn new(scheme: SchemeRef) -> impl Future> + fn open(&self, url: U) -> impl Future> where - Self: Sized; - - fn open

(&self, path: P) -> impl Future> - where - P: AsRef; + U: AsUrl; fn read(&mut self, read: bool) -> &mut Self; diff --git a/yazi-fs/src/scheme.rs b/yazi-fs/src/scheme.rs index f6a6d8d1..4eb65431 100644 --- a/yazi-fs/src/scheme.rs +++ b/yazi-fs/src/scheme.rs @@ -11,12 +11,12 @@ pub trait FsScheme { impl FsScheme for SchemeRef<'_> { fn cache(&self) -> Option { match self { - SchemeRef::Regular | SchemeRef::Search(_) => None, - SchemeRef::Archive(name) => { - Some(Xdg::cache_dir().join(format!("archive-{}", yazi_shared::url::Encode::domain(name)))) - } - SchemeRef::Sftp(name) => { - Some(Xdg::cache_dir().join(format!("sftp-{}", yazi_shared::url::Encode::domain(name)))) + Self::Regular { .. } | Self::Search { .. } => None, + Self::Archive { domain, .. } => Some( + Xdg::cache_dir().join(format!("archive-{}", yazi_shared::scheme::Encode::domain(domain))), + ), + Self::Sftp { domain, .. } => { + Some(Xdg::cache_dir().join(format!("sftp-{}", yazi_shared::scheme::Encode::domain(domain)))) } } } diff --git a/yazi-fs/src/sorter.rs b/yazi-fs/src/sorter.rs index 8d85e020..6c747cbd 100644 --- a/yazi-fs/src/sorter.rs +++ b/yazi-fs/src/sorter.rs @@ -1,7 +1,7 @@ use std::cmp::Ordering; use hashbrown::HashMap; -use yazi_shared::{LcgRng, natsort, path::{PathBufDyn, PathLike}, translit::Transliterator, url::UrlLike}; +use yazi_shared::{LcgRng, natsort, path::{PathBufDyn, PathLike}, strand::StrandLike, translit::Transliterator, url::UrlLike}; use crate::{File, SortBy}; @@ -43,8 +43,8 @@ impl FilesSorter { self.cmp(a.url.ext(), b.url.ext(), self.promote(a, b)) } else { self.cmp_insensitive( - a.url.ext().map_or(&[], |s| s.as_encoded_bytes()), - b.url.ext().map_or(&[], |s| s.as_encoded_bytes()), + a.url.ext().map_or(&[], |s| s.encoded_bytes()), + b.url.ext().map_or(&[], |s| s.encoded_bytes()), self.promote(a, b), ) }; diff --git a/yazi-fs/src/url.rs b/yazi-fs/src/url.rs index a7c39489..12171d36 100644 --- a/yazi-fs/src/url.rs +++ b/yazi-fs/src/url.rs @@ -1,6 +1,6 @@ use std::{borrow::Cow, ffi::OsStr, path::{Path, PathBuf}}; -use yazi_shared::{loc::Loc, url::{AsUrl, Url, UrlBuf, UrlCow}}; +use yazi_shared::{path::{PathDyn, PathLike}, url::{AsUrl, Url, UrlBuf, UrlCow}}; use crate::{FsHash128, FsScheme, path::PercentEncoding}; @@ -24,22 +24,22 @@ pub trait FsUrl<'a> { impl<'a> FsUrl<'a> for Url<'a> { fn cache(&self) -> Option { - fn with_loc(loc: Loc, mut path: PathBuf) -> PathBuf { + fn with_loc(loc: PathDyn, mut root: PathBuf) -> PathBuf { let mut it = loc.components(); if it.next() == Some(std::path::Component::RootDir) { - path.push(it.as_path().percent_encode()); + root.push(it.as_path().percent_encode()); } else { - path.push(".%2F"); - path.push(loc.percent_encode()); + root.push(".%2F"); + root.push(loc.percent_encode()); } - path + root } - self.scheme.cache().map(|root| with_loc(self.loc, root)) + self.scheme().cache().map(|root| with_loc(self.loc(), root)) } fn cache_lock(&self) -> Option { - self.scheme.cache().map(|mut root| { + self.scheme().cache().map(|mut root| { root.push("%lock"); root.push(format!("{:x}", self.hash_u128())); root @@ -47,7 +47,12 @@ impl<'a> FsUrl<'a> for Url<'a> { } fn unified_path(self) -> Cow<'a, Path> { - self.cache().map(Cow::Owned).unwrap_or_else(|| Cow::Borrowed(self.loc.as_path())) + match self { + Self::Regular(loc) | Self::Search { loc, .. } => loc.as_path().into(), + Self::Archive { .. } | Self::Sftp { .. } => { + self.cache().expect("non-local URL should have a cache path").into() + } + } } } @@ -57,7 +62,12 @@ impl FsUrl<'_> for UrlBuf { fn cache_lock(&self) -> Option { self.as_url().cache_lock() } fn unified_path(self) -> Cow<'static, Path> { - self.cache().unwrap_or_else(|| self.loc.into_path()).into() + match self { + Self::Regular(loc) | Self::Search { loc, .. } => loc.into_path().into(), + Self::Archive { .. } | Self::Sftp { .. } => { + self.cache().expect("non-local URL should have a cache path").into() + } + } } } @@ -67,10 +77,12 @@ impl<'a> FsUrl<'a> for UrlCow<'a> { fn cache_lock(&self) -> Option { self.as_url().cache_lock() } fn unified_path(self) -> Cow<'a, Path> { - match (self.cache(), self) { - (None, UrlCow::Borrowed { loc, .. }) => loc.as_path().into(), - (None, UrlCow::Owned { loc, .. }) => loc.into_path().into(), - (Some(cache), _) => cache.into(), + match self { + Self::Regular(loc) | Self::Search { loc, .. } => loc.into_path().into(), + Self::RegularRef(loc) | Self::SearchRef { loc, .. } => loc.as_path().into(), + Self::Archive { .. } | Self::ArchiveRef { .. } | Self::Sftp { .. } | Self::SftpRef { .. } => { + self.cache().expect("non-local URL should have a cache path").into() + } } } } diff --git a/yazi-parser/src/cmp/show.rs b/yazi-parser/src/cmp/show.rs index 13508a7d..05478e81 100644 --- a/yazi-parser/src/cmp/show.rs +++ b/yazi-parser/src/cmp/show.rs @@ -1,14 +1,14 @@ -use std::{ffi::OsString, path::{MAIN_SEPARATOR_STR, PathBuf}}; +use std::path::MAIN_SEPARATOR_STR; use anyhow::bail; use mlua::{ExternalError, FromLua, IntoLua, Lua, Value}; -use yazi_shared::{Id, event::CmdCow, url::UrlBuf}; +use yazi_shared::{Id, event::CmdCow, path::PathBufDyn, strand::{StrandBuf, StrandBufLike}, url::UrlBuf}; -#[derive(Debug, Default)] +#[derive(Debug)] pub struct ShowOpt { pub cache: Vec, pub cache_name: UrlBuf, - pub word: PathBuf, + pub word: PathBufDyn, pub ticket: Id, } @@ -35,7 +35,7 @@ impl IntoLua for ShowOpt { // --- Item #[derive(Debug, Clone)] pub struct CmpItem { - pub name: OsString, + pub name: StrandBuf, pub is_dir: bool, } diff --git a/yazi-parser/src/mgr/copy.rs b/yazi-parser/src/mgr/copy.rs index 6468bd17..1496f154 100644 --- a/yazi-parser/src/mgr/copy.rs +++ b/yazi-parser/src/mgr/copy.rs @@ -1,8 +1,8 @@ -use std::{borrow::Cow, ffi::OsStr, path::Path, str::FromStr}; +use std::{borrow::Cow, str::FromStr}; use mlua::{ExternalError, FromLua, IntoLua, Lua, Value}; use serde::Deserialize; -use yazi_shared::{SStr, event::CmdCow}; +use yazi_shared::{SStr, event::CmdCow, strand::{AsStrandDyn, StrandLike}}; #[derive(Debug)] pub struct CopyOpt { @@ -47,14 +47,18 @@ impl FromStr for CopySeparator { } impl CopySeparator { - pub fn transform + ?Sized>(self, p: &T) -> Cow<'_, OsStr> { + pub fn transform(self, s: &T) -> Cow<'_, [u8]> + where + T: ?Sized + AsStrandDyn, + { #[cfg(windows)] if self == Self::Unix { - return match yazi_fs::path::backslash_to_slash(p.as_ref()) { - Cow::Owned(p) => Cow::Owned(p.into_os_string()), - Cow::Borrowed(p) => Cow::Borrowed(p.as_os_str()), + use yazi_shared::strand::{StrandBufLike, StrandCow}; + return match s.as_strand_dyn().backslash_to_slash() { + StrandCow::Borrowed(s) => s.encoded_bytes().into(), + StrandCow::Owned(s) => s.into_encoded_bytes().into(), }; } - Cow::Borrowed(p.as_ref().as_os_str()) + Cow::Borrowed(s.as_strand_dyn().encoded_bytes()) } } diff --git a/yazi-plugin/src/external/fd.rs b/yazi-plugin/src/external/fd.rs index 7cdc38dc..2f9585dc 100644 --- a/yazi-plugin/src/external/fd.rs +++ b/yazi-plugin/src/external/fd.rs @@ -21,7 +21,10 @@ pub fn fd(opt: FdOpt) -> Result> { tokio::spawn(async move { while let Ok(Some(line)) = it.next_line().await { - if let Ok(file) = File::new(opt.cwd.join(line)).await { + let Ok(url) = opt.cwd.try_join(line) else { + continue; + }; + if let Ok(file) = File::new(url).await { tx.send(file).ok(); } } @@ -31,7 +34,7 @@ pub fn fd(opt: FdOpt) -> Result> { } fn spawn(program: &str, opt: &FdOpt) -> std::io::Result { - let Some(path) = opt.cwd.as_path() else { + let Some(path) = opt.cwd.as_local() else { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, "fd can only search local filesystem", diff --git a/yazi-plugin/src/external/highlighter.rs b/yazi-plugin/src/external/highlighter.rs index 2d7fc0e2..b352a055 100644 --- a/yazi-plugin/src/external/highlighter.rs +++ b/yazi-plugin/src/external/highlighter.rs @@ -44,7 +44,7 @@ impl Highlighter { pub fn abort() { INCR.next(); } pub async fn highlight(&self, skip: usize, size: Size) -> Result, PeekError> { - let mut reader = BufReader::new(Local.open(&self.path).await?); + let mut reader = BufReader::new(Local::regular(&self.path).open().await?); let syntax = Self::find_syntax(&self.path, &mut reader).await; let mut plain = syntax.is_err(); diff --git a/yazi-plugin/src/external/rg.rs b/yazi-plugin/src/external/rg.rs index 99553851..4f3c6452 100644 --- a/yazi-plugin/src/external/rg.rs +++ b/yazi-plugin/src/external/rg.rs @@ -14,7 +14,7 @@ pub struct RgOpt { } pub fn rg(opt: RgOpt) -> Result> { - let Some(path) = opt.cwd.as_path() else { + let Some(path) = opt.cwd.as_local() else { bail!("rg can only search local filesystem"); }; @@ -34,7 +34,10 @@ pub fn rg(opt: RgOpt) -> Result> { tokio::spawn(async move { while let Ok(Some(line)) = it.next_line().await { - if let Ok(file) = File::new(opt.cwd.join(line)).await { + let Ok(url) = opt.cwd.try_join(line) else { + continue; + }; + if let Ok(file) = File::new(url).await { tx.send(file).ok(); } } diff --git a/yazi-plugin/src/external/rga.rs b/yazi-plugin/src/external/rga.rs index db9b161a..d038eae8 100644 --- a/yazi-plugin/src/external/rga.rs +++ b/yazi-plugin/src/external/rga.rs @@ -14,7 +14,7 @@ pub struct RgaOpt { } pub fn rga(opt: RgaOpt) -> Result> { - let Some(path) = opt.cwd.as_path() else { + let Some(path) = opt.cwd.as_local() else { bail!("rga can only search local filesystem"); }; @@ -34,7 +34,10 @@ pub fn rga(opt: RgaOpt) -> Result> { tokio::spawn(async move { while let Ok(Some(line)) = it.next_line().await { - if let Ok(file) = File::new(opt.cwd.join(line)).await { + let Ok(url) = opt.cwd.try_join(line) else { + continue; + }; + if let Ok(file) = File::new(url).await { tx.send(file).ok(); } } diff --git a/yazi-plugin/src/fs/fs.rs b/yazi-plugin/src/fs/fs.rs index 76b9f06b..4d0ee1e8 100644 --- a/yazi-plugin/src/fs/fs.rs +++ b/yazi-plugin/src/fs/fs.rs @@ -148,7 +148,7 @@ fn read_dir(lua: &Lua) -> mlua::Result { fn calc_size(lua: &Lua) -> mlua::Result { lua.create_async_function(|lua, url: UrlRef| async move { - let it = if let Some(path) = url.as_path() { + let it = if let Some(path) = url.as_local() { yazi_fs::provider::local::SizeCalculator::new(path).await.map(SizeCalculator::Local) } else { yazi_vfs::provider::SizeCalculator::new(&*url).await.map(SizeCalculator::Remote) diff --git a/yazi-plugin/src/lib.rs b/yazi-plugin/src/lib.rs index da074117..9b767077 100644 --- a/yazi-plugin/src/lib.rs +++ b/yazi-plugin/src/lib.rs @@ -1,4 +1,4 @@ -#![allow(clippy::if_same_then_else, clippy::unit_arg)] +#![allow(clippy::if_same_then_else)] yazi_macro::mod_pub!(bindings elements external fs isolate loader process pubsub runtime theme utils); diff --git a/yazi-plugin/src/loader/loader.rs b/yazi-plugin/src/loader/loader.rs index af53db93..d5fff9bd 100644 --- a/yazi-plugin/src/loader/loader.rs +++ b/yazi-plugin/src/loader/loader.rs @@ -65,8 +65,11 @@ impl Loader { } let p = BOOT.plugin_dir.join(format!("{plugin}.yazi/{entry}.lua")); - let chunk = - Local.read(&p).await.with_context(|| format!("Failed to load plugin from {p:?}"))?.into(); + let chunk = Local::regular(&p) + .read() + .await + .with_context(|| format!("Failed to load plugin from {p:?}"))? + .into(); let result = Self::compatible_or_error(id, &chunk); let inspect = f(&chunk); diff --git a/yazi-plugin/src/utils/image.rs b/yazi-plugin/src/utils/image.rs index 6cef3ab6..4f88b014 100644 --- a/yazi-plugin/src/utils/image.rs +++ b/yazi-plugin/src/utils/image.rs @@ -30,7 +30,7 @@ impl Utils { pub(super) fn image_precache(lua: &Lua) -> mlua::Result { lua.create_async_function(|lua, (src, dist): (UrlRef, UrlRef)| async move { - let Some(dist) = dist.as_path() else { + let Some(dist) = dist.as_local() else { return (Value::Nil, Error::custom("Destination must be a local path")) .into_lua_multi(&lua); }; diff --git a/yazi-plugin/src/utils/text.rs b/yazi-plugin/src/utils/text.rs index 0fffee25..296d0d1f 100644 --- a/yazi-plugin/src/utils/text.rs +++ b/yazi-plugin/src/utils/text.rs @@ -91,7 +91,7 @@ impl Utils { CLIPBOARD.set(text).await; Ok(None) } else { - Some(lua.create_string(CLIPBOARD.get().await.as_encoded_bytes())).transpose() + Some(lua.create_string(CLIPBOARD.get().await)).transpose() } }) } diff --git a/yazi-scheduler/src/file/file.rs b/yazi-scheduler/src/file/file.rs index 14bdca90..d1a17619 100644 --- a/yazi-scheduler/src/file/file.rs +++ b/yazi-scheduler/src/file/file.rs @@ -1,12 +1,12 @@ -use std::{borrow::Cow, collections::VecDeque, hash::{BuildHasher, Hash, Hasher}}; +use std::{collections::VecDeque, hash::{BuildHasher, Hash, Hasher}}; use anyhow::{Context, Result, anyhow}; use tokio::{io::{self, ErrorKind::{AlreadyExists, NotFound}}, sync::mpsc}; use tracing::warn; use yazi_config::YAZI; -use yazi_fs::{Cwd, FsHash128, FsUrl, cha::Cha, ok_or_not_found, path::{path_relative_to, skip_url}, provider::{Attrs, DirReader, FileHolder, Provider, local::Local}}; +use yazi_fs::{Cwd, FsHash128, FsUrl, cha::Cha, ok_or_not_found, path::{skip_url, url_relative_to}, provider::{Attrs, DirReader, FileHolder, Provider, local::Local}}; use yazi_macro::ok_or_not_found; -use yazi_shared::{scheme::SchemeLike, timestamp_us, url::{AsUrl, UrlBuf, UrlCow, UrlLike}}; +use yazi_shared::{path::PathCow, timestamp_us, url::{AsUrl, UrlBuf, UrlCow, UrlLike}}; use yazi_vfs::{VfsCha, copy_with_progress, maybe_exists, provider::{self, DirEntry}, unique_name}; use super::{FileInDelete, FileInHardlink, FileInLink, FileInPaste, FileInTrash}; @@ -64,7 +64,7 @@ impl File { let mut dirs = VecDeque::from([task.from.clone()]); while let Some(src) = dirs.pop_front() { - let dest = root.join(skip_url(&src, skip)); + let dest = continue_unless_ok!(root.try_join(skip_url(&src, skip))); continue_unless_ok!(match provider::create_dir(&dest).await { Err(e) if e.kind() != AlreadyExists => Err(e), _ => Ok(()), @@ -80,7 +80,7 @@ impl File { continue; } - let to = dest.join(from.name().unwrap()); + let to = continue_unless_ok!(dest.try_join(from.name().unwrap())); if cha.is_orphan() || (cha.is_link() && !task.follow) { self.ops.out(task.id, FileOutPaste::New(0)); self.queue(task.spawn(from, to, cha).into_link(), NORMAL); @@ -132,20 +132,21 @@ impl File { } pub(crate) async fn link_do(&self, task: FileInLink) -> Result<(), FileOutLink> { - let src: Cow<_> = if task.resolve { + let src: PathCow = if task.resolve { ok_or_not_found!( provider::read_link(&task.from).await, return Ok(self.ops.out(task.id, FileOutLink::Succ)) ) .into() - } else if task.from.scheme.covariant(&task.to.scheme) { - task.from.loc.as_path().into() + } else if task.from.scheme().covariant(task.to.scheme()) { + task.from.loc().into() } else { Err(anyhow!("Source and target must be on the same filesystem: {task:?}"))? }; + let src = UrlCow::try_from((task.from.scheme(), src))?; let src = if task.relative { - path_relative_to(provider::canonicalize(task.to.parent().unwrap()).await?.loc, &src)? + url_relative_to(provider::canonicalize(task.to.parent().unwrap()).await?, &src)? } else { src }; @@ -196,7 +197,7 @@ impl File { let mut dirs = VecDeque::from([task.from.clone()]); while let Some(src) = dirs.pop_front() { - let dest = root.join(skip_url(&src, skip)); + let dest = continue_unless_ok!(root.try_join(skip_url(&src, skip))); continue_unless_ok!(match provider::create_dir(&dest).await { Err(e) if e.kind() != AlreadyExists => Err(e), _ => Ok(()), @@ -212,7 +213,7 @@ impl File { continue; } - let to = dest.join(from.name().unwrap()); + let to = continue_unless_ok!(dest.try_join(from.name().unwrap())); self.ops.out(task.id, FileOutHardlink::New); self.queue(task.spawn(from, to, cha), NORMAL); } @@ -353,12 +354,12 @@ impl File { while let Some(res) = it.recv().await { match res { Ok(0) => { - Local.remove_dir_all(&cache).await.ok(); + Local::regular(&cache).remove_dir_all().await.ok(); provider::rename(cache_tmp, cache).await.context("Cannot persist downloaded file")?; let lock = task.url.cache_lock().context("Cannot determine cache lock")?; let hash = format!("{:x}", cha.hash_u128()); - Local.write(lock, hash).await.context("Cannot lock cache")?; + Local::regular(&lock).write(hash).await.context("Cannot lock cache")?; break; } @@ -448,7 +449,7 @@ impl File { pub(crate) async fn upload_do(&self, task: FileInUploadDo) -> Result<(), FileOutUploadDo> { let lock = task.url.cache_lock().context("Cannot determine cache lock")?; - let hash = Local.read_to_string(&lock).await.context("Cannot read cache lock")?; + let hash = Local::regular(&lock).read_to_string().await.context("Cannot read cache lock")?; let hash = u128::from_str_radix(&hash, 16).context("Cannot parse hash from lock")?; if hash != task.cha.hash_u128() { Err(anyhow!("Remote file has changed since last download"))?; @@ -474,7 +475,7 @@ impl File { let cha = Self::cha(&task.url, true, None).await.context("Cannot stat uploaded file")?; let hash = format!("{:x}", cha.hash_u128()); - Local.write(lock, hash).await.context("Cannot lock cache")?; + Local::regular(&lock).write(hash).await.context("Cannot lock cache")?; break; } @@ -510,7 +511,7 @@ impl File { url.hash(&mut h); timestamp_us().hash(&mut h); - unique_name(parent.join(format!(".{:x}.%tmp", h.finish())), async { false }).await + unique_name(parent.try_join(format!(".{:x}.%tmp", h.finish()))?, async { false }).await } } diff --git a/yazi-scheduler/src/lib.rs b/yazi-scheduler/src/lib.rs index 57cb1330..6a288622 100644 --- a/yazi-scheduler/src/lib.rs +++ b/yazi-scheduler/src/lib.rs @@ -1,4 +1,4 @@ -#![allow(clippy::option_map_unit_fn, clippy::unit_arg)] +#![allow(clippy::option_map_unit_fn)] mod macros; diff --git a/yazi-scheduler/src/scheduler.rs b/yazi-scheduler/src/scheduler.rs index 0459b95b..2c72a50b 100644 --- a/yazi-scheduler/src/scheduler.rs +++ b/yazi-scheduler/src/scheduler.rs @@ -8,7 +8,7 @@ use yazi_config::{YAZI, plugin::{Fetcher, Preloader}}; use yazi_dds::Pump; use yazi_parser::{app::PluginOpt, tasks::ProcessOpenOpt}; use yazi_proxy::TasksProxy; -use yazi_shared::{Id, Throttle, scheme::SchemeLike, url::{UrlBuf, UrlLike}}; +use yazi_shared::{Id, Throttle, url::{UrlBuf, UrlLike}}; use yazi_vfs::{must_be_dir, provider, unique_name}; use super::{Ongoing, TaskOp}; @@ -79,7 +79,13 @@ impl Scheduler { let mut ongoing = self.ongoing.lock(); let id = ongoing.add::(format!("Cut {} to {}", from.display(), to.display())); - if to.starts_with(&from) && !to.covariant(&from) { + let Ok(prefixed) = to.try_starts_with(&from) else { + return self + .ops + .out(id, FileOutPaste::Fail("Path being cut has a different encoding".to_owned())); + }; + + if prefixed && !to.covariant(&from) { return self.ops.out(id, FileOutPaste::Fail("Cannot cut directory into itself".to_owned())); } @@ -112,7 +118,13 @@ impl Scheduler { to.display() )); - if to.starts_with(&from) && !to.covariant(&from) { + let Ok(prefixed) = to.try_starts_with(&from) else { + return self + .ops + .out(id, FileOutPaste::Fail("Path being copied has a different encoding".to_owned())); + }; + + if prefixed && !to.covariant(&from) { return self.ops.out(id, FileOutPaste::Fail("Cannot copy directory into itself".to_owned())); } @@ -148,7 +160,14 @@ impl Scheduler { to.display() )); - if to.starts_with(&from) && !to.covariant(&from) { + let Ok(prefixed) = to.try_starts_with(&from) else { + return self.ops.out( + id, + FileOutHardlink::Fail("Path being hardlinked has a different encoding".to_owned()), + ); + }; + + if prefixed && !to.covariant(&from) { return self .ops .out(id, FileOutHardlink::Fail("Cannot hardlink directory into itself".to_owned())); @@ -215,7 +234,7 @@ impl Scheduler { ongoing.hooks.add_sync(id, move |canceled| _ = tx.send(canceled)); } - if !url.scheme.is_remote() { + if !url.kind().is_remote() { return self.ops.out(id, FileOutDownload::Fail("Cannot download non-remote file".to_owned())); }; @@ -229,7 +248,7 @@ impl Scheduler { let mut ongoing = self.ongoing.lock(); let id = ongoing.add::(format!("Upload {}", url.display())); - if !url.scheme.is_remote() { + if !url.kind().is_remote() { return self.ops.out(id, FileOutUpload::Fail("Cannot upload non-remote file".to_owned())); }; diff --git a/yazi-shared/Cargo.toml b/yazi-shared/Cargo.toml index 9c14365e..c19fe6bf 100644 --- a/yazi-shared/Cargo.toml +++ b/yazi-shared/Cargo.toml @@ -23,6 +23,7 @@ ordered-float = { workspace = true } parking_lot = { workspace = true } percent-encoding = { workspace = true } serde = { workspace = true } +thiserror = { workspace = true } tokio = { workspace = true } [target."cfg(unix)".dependencies] diff --git a/yazi-shared/src/bytes.rs b/yazi-shared/src/bytes.rs index 659c1969..0492b947 100644 --- a/yazi-shared/src/bytes.rs +++ b/yazi-shared/src/bytes.rs @@ -1,16 +1,61 @@ +use std::fmt::Display; + +use crate::BytePredictor; + pub trait BytesExt { + fn display(&self) -> impl Display; + fn kebab_cased(&self) -> bool; - fn split_by_seq(&self, sep: &[u8]) -> Option<(&[u8], &[u8])>; + + fn rsplit_pred_once(&self, pred: P) -> Option<(&[u8], &[u8])>; + + fn rsplit_seq_once(&self, sep: &[u8]) -> Option<(&[u8], &[u8])>; + + fn split_seq_once(&self, sep: &[u8]) -> Option<(&[u8], &[u8])>; } impl BytesExt for [u8] { + fn display(&self) -> impl Display { + struct D<'a>(&'a [u8]); + + impl Display for D<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for chunk in self.0.utf8_chunks() { + chunk.valid().fmt(f)?; + if !chunk.invalid().is_empty() { + char::REPLACEMENT_CHARACTER.fmt(f)?; + } + } + Ok(()) + } + } + + D(self) + } + fn kebab_cased(&self) -> bool { self.iter().all(|&b| matches!(b, b'0'..=b'9' | b'a'..=b'z' | b'-')) } - fn split_by_seq(&self, sep: &[u8]) -> Option<(&[u8], &[u8])> { + fn rsplit_pred_once(&self, pred: P) -> Option<(&[u8], &[u8])> { + for (i, &byte) in self.iter().enumerate().rev() { + if pred.predicate(byte) { + let (a, b) = self.split_at(i); + return Some((a, &b[1..])); + } + } + None + } + + fn split_seq_once(&self, sep: &[u8]) -> Option<(&[u8], &[u8])> { let idx = memchr::memmem::find(self, sep)?; - let (left, right) = self.split_at(idx); - Some((left, &right[sep.len()..])) + let (a, b) = self.split_at(idx); + Some((a, &b[sep.len()..])) + } + + fn rsplit_seq_once(&self, sep: &[u8]) -> Option<(&[u8], &[u8])> { + let idx = memchr::memmem::rfind(self, sep)?; + let (a, b) = self.split_at(idx); + Some((a, &b[sep.len()..])) } } diff --git a/yazi-shared/src/chars.rs b/yazi-shared/src/chars.rs index f6e7a943..590067b6 100644 --- a/yazi-shared/src/chars.rs +++ b/yazi-shared/src/chars.rs @@ -1,5 +1,5 @@ use core::str; -use std::{borrow::Cow, ffi::OsStr}; +use std::borrow::Cow; #[derive(Clone, Copy, PartialEq, Eq)] pub enum CharKind { @@ -117,23 +117,3 @@ pub fn replace_to_printable(s: &[String], tab_size: u8) -> String { } unsafe { String::from_utf8_unchecked(buf) } } - -pub fn osstr_contains(s: impl AsRef, needle: impl AsRef) -> bool { - memchr::memmem::find(s.as_ref().as_encoded_bytes(), needle.as_ref().as_encoded_bytes()).is_some() -} - -pub fn osstr_starts_with( - s: impl AsRef, - prefix: impl AsRef, - insensitive: bool, -) -> bool { - let (s, prefix) = (s.as_ref().as_encoded_bytes(), prefix.as_ref().as_encoded_bytes()); - if s.len() < prefix.len() { - return false; - } - if insensitive { - s[..prefix.len()].eq_ignore_ascii_case(prefix) - } else { - s[..prefix.len()] == *prefix - } -} diff --git a/yazi-shared/src/lib.rs b/yazi-shared/src/lib.rs index 08f23f36..3b1af6dc 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 translit url); +yazi_macro::mod_pub!(data errors event loc path pool scheme shell strand translit url); -yazi_macro::mod_flat!(alias bytes chars condition debounce either env id layer natsort os osstr rand ro_cell source string sync_cell terminal tests throttle time utf8); +yazi_macro::mod_flat!(alias bytes chars condition debounce either env id layer natsort os osstr predictor rand ro_cell source string sync_cell terminal tests throttle time utf8 wtf8); pub fn init() { pool::init(); diff --git a/yazi-shared/src/loc/buf.rs b/yazi-shared/src/loc/buf.rs index 5e669c6c..24f5f325 100644 --- a/yazi-shared/src/loc/buf.rs +++ b/yazi-shared/src/loc/buf.rs @@ -2,7 +2,7 @@ use std::{cmp, ffi::OsStr, fmt::{self, Debug, Formatter}, hash::{Hash, Hasher}, use anyhow::Result; -use crate::{loc::Loc, path::{AsInnerView, AsPathDyn, AsPathView, PathBufLike, PathDyn, PathLike}}; +use crate::{loc::Loc, path::{AsPathDyn, AsPathView, PathBufLike, PathBufUnsafeExt, PathDyn, PathLike, PathUnsafeExt, SetNameError}, scheme::SchemeKind, strand::AsStrandView}; #[derive(Clone, Default, Eq)] pub struct LocBuf { @@ -65,7 +65,8 @@ where // --- Hash impl

Hash for LocBuf

where - P: PathBufLike, + P: PathBufLike + PathBufUnsafeExt, + for<'a> P::Borrowed<'a>: PathUnsafeExt<'a>, for<'a> &'a P: AsPathView<'a, P::Borrowed<'a>>, { fn hash(&self, state: &mut H) { self.as_loc().hash(state) } @@ -73,7 +74,8 @@ where impl

Debug for LocBuf

where - P: PathBufLike + Debug, + P: PathBufLike + PathBufUnsafeExt + Debug, + for<'a> P::Borrowed<'a>: PathUnsafeExt<'a>, for<'a> &'a P: AsPathView<'a, P::Borrowed<'a>>, { fn fmt(&self, f: &mut Formatter) -> fmt::Result { @@ -87,7 +89,8 @@ where impl

From

for LocBuf

where - P: PathBufLike, + P: PathBufLike + PathBufUnsafeExt, + for<'a> P::Borrowed<'a>: PathUnsafeExt<'a>, for<'a> &'a P: AsPathView<'a, P::Borrowed<'a>>, { fn from(path: P) -> Self { @@ -106,12 +109,13 @@ impl> From<&T> for LocBuf { impl

LocBuf

where - P: PathBufLike, + P: PathBufLike + PathBufUnsafeExt, + for<'a> P::Borrowed<'a>: PathUnsafeExt<'a>, for<'a> &'a P: AsPathView<'a, P::Borrowed<'a>>, { - pub fn new<'a, T>(path: P, base: T, trail: T) -> Self + pub fn new<'a, S>(path: P, base: S, trail: S) -> Self where - T: for<'b> AsPathView<'a, as PathLike<'b>>::View<'a>>, + S: for<'b> AsStrandView<'a, as PathLike<'b>>::Strand<'a>>, { let loc = Self::from(path); let Loc { inner, uri, urn, _phantom } = Loc::new(&loc.inner, base, trail); @@ -142,9 +146,9 @@ where Self { inner: loc.inner, uri, urn } } - pub fn floated<'a, T>(path: P, base: T) -> Self + pub fn floated<'a, S>(path: P, base: S) -> Self where - T: for<'b> AsPathView<'a, as PathLike<'b>>::View<'a>>, + S: for<'b> AsStrandView<'a, as PathLike<'b>>::Strand<'a>>, { let loc = Self::from(path); let Loc { inner, uri, urn, _phantom } = Loc::floated(&loc.inner, base); @@ -153,6 +157,14 @@ where Self { inner: loc.inner, uri, urn } } + pub fn saturated(path: P, kind: SchemeKind) -> Self { + let loc = Self::from(path); + let Loc { inner, uri, urn, _phantom } = Loc::saturated(&loc.inner, kind); + + debug_assert!(inner.encoded_bytes() == loc.inner.encoded_bytes()); + Self { inner: loc.inner, uri, urn } + } + #[inline] pub fn as_loc<'a>(&'a self) -> Loc<'a, P::Borrowed<'a>> { Loc { @@ -174,16 +186,16 @@ where #[inline] pub fn into_path(self) -> P { self.inner } - pub fn set_name(&mut self, name: T) + pub fn try_set_name<'a, T>(&mut self, name: T) -> Result<(), SetNameError> where - T: for<'a> AsInnerView<'a, P::InnerRef<'a>>, + T: AsStrandView<'a, P::Strand<'a>>, { let old = self.inner.len(); - self.mutate(|path| path.set_file_name(name)); + self.mutate(|path| path.try_set_name(name))?; let new = self.len(); if new == old { - return; + return Ok(()); } if self.uri != 0 { @@ -200,31 +212,34 @@ where self.urn -= old - new; } } + Ok(()) } #[inline] - pub fn rebase<'a, 'b>(&'a self, base: P::Borrowed<'b>) -> Self + pub fn rebase<'a, 'b>(&'a self, base: P::Borrowed<'b>) -> Result where 'a: 'b, for<'c> as PathLike<'c>>::Owned: Into, { - let mut loc: Self = base.join(self.uri()).into(); + let mut loc: Self = base.try_join(self.uri())?.into(); (loc.uri, loc.urn) = (self.uri, self.urn); - loc + Ok(loc) } #[inline] - fn mutate(&mut self, f: F) { + fn mutate T>(&mut self, f: F) -> T { let mut inner = self.inner.take(); - f(&mut inner); + let result = f(&mut inner); self.inner = Self::from(inner).inner; + result } } // FIXME: macro impl

LocBuf

where - P: PathBufLike, + P: PathBufLike + PathBufUnsafeExt, + for<'a> P::Borrowed<'a>: PathUnsafeExt<'a>, for<'a> &'a P: AsPathView<'a, P::Borrowed<'a>>, { #[inline] @@ -246,13 +261,17 @@ where pub fn has_trail(&self) -> bool { self.as_loc().has_trail() } #[inline] - pub fn name(&self) -> Option< as PathLike<'_>>::Inner> { self.as_loc().name() } + pub fn name(&self) -> Option< as PathLike<'_>>::Strand<'_>> { + self.as_loc().name() + } #[inline] - pub fn stem(&self) -> Option< as PathLike<'_>>::Inner> { self.as_loc().stem() } + pub fn stem(&self) -> Option< as PathLike<'_>>::Strand<'_>> { + self.as_loc().stem() + } #[inline] - pub fn ext(&self) -> Option< as PathLike<'_>>::Inner> { self.as_loc().ext() } + pub fn ext(&self) -> Option< as PathLike<'_>>::Strand<'_>> { self.as_loc().ext() } } impl LocBuf { @@ -360,7 +379,7 @@ mod tests { for (input, name, expected) in cases { let mut a: UrlBuf = input.parse()?; let b: UrlBuf = expected.parse()?; - a.set_name(name); + a.try_set_name(name).unwrap(); assert_eq!( (a.name(), format!("{a:?}").replace(r"\", "/")), (b.name(), expected.replace(r"\", "/")) diff --git a/yazi-shared/src/loc/loc.rs b/yazi-shared/src/loc/loc.rs index 9d3697a7..a24d5801 100644 --- a/yazi-shared/src/loc/loc.rs +++ b/yazi-shared/src/loc/loc.rs @@ -2,7 +2,7 @@ use std::{hash::{Hash, Hasher}, marker::PhantomData, ops::Deref, path::Path}; use anyhow::{Result, bail}; -use crate::{loc::LocBuf, path::{AsPathView, PathBufLike, PathInner, PathLike}}; +use crate::{loc::LocBuf, path::{AsPathDyn, AsPathView, PathBufLike, PathDyn, PathLike, PathUnsafeExt}, scheme::SchemeKind, strand::{AsStrandView, StrandLike}}; #[derive(Clone, Copy, Debug)] pub struct Loc<'p, P = &'p Path> { @@ -28,6 +28,13 @@ where fn deref(&self) -> &Self::Target { &self.inner } } +impl<'p, P> AsPathDyn for Loc<'p, P> +where + P: PathLike<'p> + AsPathDyn, +{ + fn as_path_dyn(&self) -> PathDyn<'_> { self.inner.as_path_dyn() } +} + // FIXME: remove impl AsRef for Loc<'_, &std::path::Path> { fn as_ref(&self) -> &std::path::Path { self.inner } @@ -63,16 +70,16 @@ impl<'p, P> Eq for Loc<'p, P> where P: PathLike<'p> + Eq {} impl<'p, P> Loc<'p, P> where - P: PathLike<'p>, + P: PathLike<'p> + PathUnsafeExt<'p>, { - pub fn new<'a, T, U>(path: T, base: U, trail: U) -> Self + pub fn new<'a, T, S>(path: T, base: S, trail: S) -> Self where T: AsPathView<'p, P>, - U: AsPathView<'a, P::View<'a>>, + S: AsStrandView<'a, P::Strand<'a>>, { let mut loc = Self::bare(path); - loc.uri = loc.inner.strip_prefix(base).expect("Loc must start with the given base").len(); - loc.urn = loc.inner.strip_prefix(trail).expect("Loc must start with the given trail").len(); + loc.uri = loc.inner.try_strip_prefix(base).expect("Loc must start with the given base").len(); + loc.urn = loc.inner.try_strip_prefix(trail).expect("Loc must start with the given trail").len(); loc } @@ -98,10 +105,10 @@ where bail!("URI exceeds the entire URL"); } if i == urn { - loc.urn = loc.strip_prefix(it.clone()).unwrap().len(); + loc.urn = loc.try_strip_prefix(it.clone())?.len(); } if i == uri { - loc.uri = loc.strip_prefix(it).unwrap().len(); + loc.uri = loc.try_strip_prefix(it)?.len(); break; } } @@ -113,7 +120,7 @@ where T: AsPathView<'p, P>, { let path = path.as_path_view(); - let Some(name) = path.file_name() else { + let Some(name) = path.name() else { let uri = path.len(); return Self { inner: path, uri, urn: 0, _phantom: PhantomData }; }; @@ -140,22 +147,37 @@ where loc } - pub fn floated<'a, T, U>(path: T, base: U) -> Self + pub fn floated<'a, T, S>(path: T, base: S) -> Self where T: AsPathView<'p, P>, - U: AsPathView<'a, P::View<'a>>, + S: AsStrandView<'a, P::Strand<'a>>, { let mut loc = Self::bare(path); - loc.uri = loc.inner.strip_prefix(base).expect("Loc must start with the given base").len(); + loc.uri = loc.inner.try_strip_prefix(base).expect("Loc must start with the given base").len(); loc } + pub fn saturated<'a, T>(path: T, kind: SchemeKind) -> Self + where + T: AsPathView<'p, P>, + { + match kind { + SchemeKind::Regular => Self::bare(path), + SchemeKind::Search => Self::zeroed(path), + SchemeKind::Archive => Self::zeroed(path), + SchemeKind::Sftp => Self::bare(path), + } + } + #[inline] pub fn as_loc(self) -> Self { self } #[inline] pub fn as_path(self) -> P { self.inner } + #[inline] + pub fn is_empty(self) -> bool { self.inner.is_empty() } + #[inline] pub fn uri(self) -> P { unsafe { @@ -190,18 +212,6 @@ where #[inline] pub fn has_trail(self) -> bool { self.inner.len() != self.urn } - #[inline] - pub fn name(self) -> Option { self.inner.file_name() } - - #[inline] - pub fn stem(self) -> Option { self.inner.file_stem() } - - #[inline] - pub fn ext(self) -> Option { self.inner.extension() } - - #[inline] - pub fn parent(self) -> Option

{ self.inner.parent() } - #[inline] pub fn triple(self) -> (P, P, P) { let len = self.inner.len(); @@ -218,12 +228,4 @@ where ) } } - - #[inline] - pub fn strip_prefix<'a, T>(self, base: T) -> Option

- where - T: AsPathView<'a, P::View<'a>>, - { - self.inner.strip_prefix(base) - } } diff --git a/yazi-shared/src/osstr.rs b/yazi-shared/src/osstr.rs index ef3dbb8b..7fc560af 100644 --- a/yazi-shared/src/osstr.rs +++ b/yazi-shared/src/osstr.rs @@ -58,38 +58,3 @@ where result } } - -// --- OsStrSplit -pub trait OsStrSplit { - fn rsplit_once(&self, predicate: P) -> Option<(&Self, &Self)>; -} - -impl OsStrSplit for OsStr { - fn rsplit_once(&self, pat: P) -> Option<(&Self, &Self)> { - let bytes = self.as_encoded_bytes(); - for (i, &byte) in bytes.iter().enumerate().rev() { - if !pat.predicate(byte) { - continue; - } - - let (a, b) = bytes.split_at(i); - // SAFETY: These substrings were separated by a UTF-8 string. - return Some(unsafe { - (Self::from_encoded_bytes_unchecked(a), Self::from_encoded_bytes_unchecked(&b[1..])) - }); - } - None - } -} - -pub trait Pattern { - fn predicate(&self, byte: u8) -> bool; -} - -impl Pattern for char { - fn predicate(&self, byte: u8) -> bool { *self == byte as Self } -} - -impl Pattern for &[char] { - fn predicate(&self, byte: u8) -> bool { self.contains(&(byte as char)) } -} diff --git a/yazi-shared/src/path/buf.rs b/yazi-shared/src/path/buf.rs index 7e3ff236..97a135e5 100644 --- a/yazi-shared/src/path/buf.rs +++ b/yazi-shared/src/path/buf.rs @@ -1,54 +1,61 @@ use std::{fmt::Debug, hash::Hash}; -use crate::path::{AsInnerView, AsPathView, PathInner, PathLike}; +use crate::{path::{AsPath, AsPathView, PathBufDyn, PathLike, SetNameError, StartsWithError}, strand::{AsStrandView, StrandLike}}; pub trait PathBufLike where - Self: 'static, + Self: 'static + AsPath, { - type Inner: for<'a> AsInnerView<'a, Self::InnerRef<'a>>; - type InnerRef<'a>: PathInner<'a>; + type Strand<'a>: StrandLike<'a>; type Borrowed<'a>: PathLike<'a> + AsPathView<'a, Self::Borrowed<'a>> + Debug + Hash; fn borrow(&self) -> Self::Borrowed<'_>; - fn encoded_bytes(&self) -> &[u8]; + fn encoded_bytes(&self) -> &[u8] { self.borrow().encoded_bytes() } - unsafe fn from_encoded_bytes(bytes: Vec) -> Self; + fn into_dyn(self) -> PathBufDyn; fn into_encoded_bytes(self) -> Vec; - fn is_empty(&self) -> bool { self.encoded_bytes().is_empty() } + fn is_empty(&self) -> bool { self.borrow().is_empty() } - fn len(&self) -> usize { self.encoded_bytes().len() } + fn len(&self) -> usize { self.borrow().len() } - fn set_file_name(&mut self, name: T) + fn to_str(&self) -> Result<&str, std::str::Utf8Error> { self.borrow().to_str() } + + fn try_set_name<'a, T>(&mut self, name: T) -> Result<(), SetNameError> where - T: for<'a> AsInnerView<'a, Self::InnerRef<'a>>; + T: AsStrandView<'a, Self::Strand<'a>>; + + fn try_starts_with<'a, T>(&self, base: T) -> Result + where + T: AsStrandView<'a, Self::Strand<'a>>; fn take(&mut self) -> Self; } impl PathBufLike for std::path::PathBuf { type Borrowed<'a> = &'a std::path::Path; - type Inner = std::ffi::OsString; - type InnerRef<'a> = &'a std::ffi::OsStr; + type Strand<'a> = &'a std::ffi::OsStr; fn borrow(&self) -> Self::Borrowed<'_> { self.as_path() } - fn encoded_bytes(&self) -> &[u8] { self.as_os_str().as_encoded_bytes() } - - unsafe fn from_encoded_bytes(bytes: Vec) -> Self { - Self::from(unsafe { Self::Inner::from_encoded_bytes_unchecked(bytes) }) - } + fn into_dyn(self) -> PathBufDyn { PathBufDyn::Os(self) } fn into_encoded_bytes(self) -> Vec { self.into_os_string().into_encoded_bytes() } - fn set_file_name(&mut self, name: T) + fn try_set_name<'a, T>(&mut self, name: T) -> Result<(), SetNameError> where - T: for<'a> AsInnerView<'a, Self::InnerRef<'a>>, + T: AsStrandView<'a, Self::Strand<'a>>, { - self.set_file_name(name.as_inner_view()); + Ok(self.set_file_name(name.as_strand_view())) + } + + fn try_starts_with<'a, T>(&self, base: T) -> Result + where + T: AsStrandView<'a, Self::Strand<'a>>, + { + Ok(self.starts_with(base.as_strand_view())) } fn take(&mut self) -> Self { std::mem::take(self) } diff --git a/yazi-shared/src/path/conversion.rs b/yazi-shared/src/path/conversion.rs new file mode 100644 index 00000000..8eee3e53 --- /dev/null +++ b/yazi-shared/src/path/conversion.rs @@ -0,0 +1,105 @@ +use std::{borrow::Cow, ffi::OsStr}; + +use super::{PathBufDyn, PathDyn}; +use crate::path::{PathBufLike, PathCow, PathLike}; + +// --- AsPath +pub trait AsPath { + fn as_path(&self) -> impl PathLike<'_>; +} + +impl AsPath for OsStr { + fn as_path(&self) -> impl PathLike<'_> { std::path::Path::new(self) } +} + +impl AsPath for &OsStr { + fn as_path(&self) -> impl PathLike<'_> { std::path::Path::new(self) } +} + +impl AsPath for std::path::Path { + fn as_path(&self) -> impl PathLike<'_> { self } +} + +impl AsPath for std::path::PathBuf { + fn as_path(&self) -> impl PathLike<'_> { self.as_path() } +} + +impl AsPath for PathDyn<'_> { + fn as_path(&self) -> impl PathLike<'_> { *self } +} + +impl AsPath for PathBufDyn { + fn as_path(&self) -> impl PathLike<'_> { self.borrow() } +} + +impl AsPath for &PathBufDyn { + fn as_path(&self) -> impl PathLike<'_> { self.borrow() } +} + +impl AsPath for PathCow<'_> { + fn as_path(&self) -> impl PathLike<'_> { + match self { + PathCow::Borrowed(p) => *p, + PathCow::Owned(p) => p.into(), + } + } +} + +// --- AsPathDyn +pub trait AsPathDyn { + fn as_path_dyn(&self) -> PathDyn<'_>; +} + +impl AsPathDyn for OsStr { + fn as_path_dyn(&self) -> PathDyn<'_> { std::path::Path::new(self).into() } +} + +impl AsPathDyn for &OsStr { + fn as_path_dyn(&self) -> PathDyn<'_> { std::path::Path::new(self).into() } +} + +impl AsPathDyn for Cow<'_, OsStr> { + fn as_path_dyn(&self) -> PathDyn<'_> { std::path::Path::new(self).into() } +} + +impl AsPathDyn for std::path::Path { + fn as_path_dyn(&self) -> PathDyn<'_> { self.into() } +} + +impl AsPathDyn for std::path::PathBuf { + fn as_path_dyn(&self) -> PathDyn<'_> { self.as_path().into() } +} + +impl AsPathDyn for &std::path::PathBuf { + fn as_path_dyn(&self) -> PathDyn<'_> { self.as_path().into() } +} + +impl AsPathDyn for PathDyn<'_> { + fn as_path_dyn(&self) -> PathDyn<'_> { *self } +} + +impl AsPathDyn for PathBufDyn { + fn as_path_dyn(&self) -> PathDyn<'_> { self.borrow() } +} + +impl AsPathDyn for &PathBufDyn { + fn as_path_dyn(&self) -> PathDyn<'_> { self.borrow() } +} + +impl AsPathDyn for PathCow<'_> { + fn as_path_dyn(&self) -> PathDyn<'_> { + match self { + Self::Borrowed(p) => p.as_path_dyn(), + Self::Owned(p) => p.as_path_dyn(), + } + } +} + +// --- AsPathRef +pub trait AsPathRef<'a> { + fn as_path_ref(self) -> PathDyn<'a>; +} + +impl<'a> AsPathRef<'a> for PathDyn<'a> { + fn as_path_ref(self) -> PathDyn<'a> { self } +} diff --git a/yazi-shared/src/path/cow.rs b/yazi-shared/src/path/cow.rs new file mode 100644 index 00000000..238a11b5 --- /dev/null +++ b/yazi-shared/src/path/cow.rs @@ -0,0 +1,56 @@ +use std::borrow::Cow; + +use anyhow::Result; + +use crate::{IntoOsStr, path::{AsPathDyn, PathBufDyn, PathBufLike, PathDyn, PathLike}}; + +// --- PathCow +pub enum PathCow<'a> { + Borrowed(PathDyn<'a>), + Owned(PathBufDyn), +} + +impl<'a> From> for PathCow<'a> { + fn from(value: PathDyn<'a>) -> Self { Self::Borrowed(value) } +} + +impl From for PathCow<'_> { + fn from(value: PathBufDyn) -> Self { Self::Owned(value) } +} + +impl From> for PathBufDyn { + fn from(value: PathCow<'_>) -> Self { value.into_owned() } +} + +impl PartialEq<&str> for PathCow<'_> { + fn eq(&self, other: &&str) -> bool { + match self { + Self::Borrowed(s) => s.as_path_dyn() == *other, + Self::Owned(s) => s.as_path_dyn() == *other, + } + } +} + +impl<'a> PathCow<'a> { + pub fn from_os_bytes(bytes: impl Into>) -> Result { + Ok(match bytes.into().into_os_str()? { + Cow::Borrowed(s) => PathDyn::os(s).into(), + Cow::Owned(s) => PathBufDyn::os(s).into(), + }) + } + + pub fn into_owned(self) -> PathBufDyn { + match self { + Self::Borrowed(s) => s.to_buf_dyn(), + Self::Owned(s) => s, + } + } + + // FIXME: remove, instead implement PathLike for PathCow + pub fn encoded_bytes(&self) -> &[u8] { + match self { + Self::Borrowed(s) => s.encoded_bytes(), + Self::Owned(s) => s.encoded_bytes(), + } + } +} diff --git a/yazi-shared/src/path/dyn.rs b/yazi-shared/src/path/dyn.rs index 10f32014..777f2ef7 100644 --- a/yazi-shared/src/path/dyn.rs +++ b/yazi-shared/src/path/dyn.rs @@ -1,8 +1,11 @@ +use std::ffi::OsStr; + +use anyhow::Result; use hashbrown::Equivalent; use serde::Serialize; -use super::{AsInnerView, AsPathView}; -use crate::path::{PathBufLike, PathLike}; +use super::{AsPathView, RsplitOnceError, StartsWithError}; +use crate::{FromWtf8, Utf8BytePredictor, path::{AsPathDyn, EndsWithError, JoinError, PathBufDynError, PathBufLike, PathBufUnsafeExt, PathDynError, PathKind, PathLike, SetNameError, StripPrefixError}, scheme::SchemeKind, strand::{AsStrandDyn, AsStrandView, Strand, StrandError}}; #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub enum PathDyn<'p> { @@ -18,20 +21,46 @@ impl<'a> AsPathView<'a, PathDyn<'a>> for std::path::Components<'a> { } impl<'a> From<&'a std::path::Path> for PathDyn<'a> { - fn from(value: &'a std::path::Path) -> Self { PathDyn::Os(value) } + fn from(value: &'a std::path::Path) -> Self { Self::Os(value) } } impl<'a> From<&'a PathBufDyn> for PathDyn<'a> { fn from(value: &'a PathBufDyn) -> Self { value.borrow() } } +impl PartialEq for PathDyn<'_> { + fn eq(&self, other: &PathBufDyn) -> bool { *self == other.borrow() } +} + +impl PartialEq> for &std::path::Path { + fn eq(&self, other: &PathDyn<'_>) -> bool { matches!(*other, PathDyn::Os(p) if p == *self) } +} + +impl PartialEq<&std::path::Path> for PathDyn<'_> { + fn eq(&self, other: &&std::path::Path) -> bool { matches!(*self, PathDyn::Os(p) if p == *other) } +} + +impl PartialEq<&str> for PathDyn<'_> { + fn eq(&self, other: &&str) -> bool { + match self { + PathDyn::Os(p) => p == other, + } + } +} + +impl Equivalent for PathDyn<'_> { + fn equivalent(&self, key: &PathBufDyn) -> bool { *self == key.borrow() } +} + impl<'p> PathLike<'p> for PathDyn<'p> { type Components<'a> = std::path::Components<'a>; type Display<'a> = std::path::Display<'a>; - type Inner = &'p [u8]; type Owned = PathBufDyn; + type Strand<'a> = Strand<'a>; type View<'a> = PathDyn<'a>; + fn as_dyn(self) -> PathDyn<'p> { self } + fn components(self) -> Self::Components<'p> { match self { Self::Os(p) => p.components(), @@ -53,38 +82,36 @@ impl<'p> PathLike<'p> for PathDyn<'p> { } } - fn extension(self) -> Option { + fn ext(self) -> Option> { Some(match self { - Self::Os(p) => p.extension()?.as_encoded_bytes(), + Self::Os(p) => p.extension()?.into(), }) } - fn file_name(self) -> Option { - Some(match self { - Self::Os(p) => p.file_name()?.as_encoded_bytes(), - }) - } - - fn file_stem(self) -> Option { - Some(match self { - Self::Os(p) => p.file_stem()?.as_encoded_bytes(), - }) - } - - // FIXME: remove - unsafe fn from_encoded_bytes(bytes: &'p [u8]) -> Self { - Self::Os(std::path::Path::new(unsafe { std::ffi::OsStr::from_encoded_bytes_unchecked(bytes) })) - } - - fn join<'a, T>(self, base: T) -> Self::Owned - where - T: AsPathView<'a, Self::View<'a>>, - { - match (self, base.as_path_view()) { - (Self::Os(p), PathDyn::Os(q)) => Self::Owned::Os(p.join(q)), + fn has_root(self) -> bool { + match self { + Self::Os(p) => p.has_root(), } } + fn is_absolute(self) -> bool { + match self { + Self::Os(p) => p.is_absolute(), + } + } + + fn kind(self) -> PathKind { + match self { + Self::Os(_) => PathKind::Os, + } + } + + fn name(self) -> Option> { + Some(match self { + Self::Os(p) => p.file_name()?.into(), + }) + } + fn owned(self) -> Self::Owned { match self { Self::Os(p) => Self::Owned::Os(p.to_path_buf()), @@ -97,26 +124,129 @@ impl<'p> PathLike<'p> for PathDyn<'p> { }) } - fn strip_prefix<'a, T>(self, base: T) -> Option + fn stem(self) -> Option> { + Some(match self { + Self::Os(p) => p.file_stem()?.into(), + }) + } + + fn try_ends_with<'a, T>(self, child: T) -> Result where - T: AsPathView<'a, Self::View<'a>>, + T: AsStrandView<'a, Self::Strand<'a>>, { - Some(match (self, base.as_path_view()) { - (Self::Os(p), PathDyn::Os(q)) => Self::Os(p.strip_prefix(q).ok()?), + Ok(match (self, child.as_strand_view()) { + (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)?) + } + }) + } + + fn try_join<'a, T>(self, path: T) -> Result + where + T: AsStrandView<'a, Self::Strand<'a>>, + { + Ok(match (self, path.as_strand_view()) { + (Self::Os(p), Strand::Os(q)) => Self::Owned::Os(p.join(q)), + (Self::Os(p), Strand::Utf8(q)) => Self::Owned::Os(p.join(q)), + (Self::Os(p), Strand::Bytes(b)) => { + Self::Owned::Os(p.join(OsStr::from_wtf8(b).map_err(|_| JoinError::FromWtf8)?)) + } + }) + } + + fn rsplit_pred<'a, T>(self, pred: T) -> Option<(Self, Self)> + where + T: Utf8BytePredictor, + { + match self { + PathDyn::Os(p) => p.rsplit_pred(pred).map(|(l, r)| (Self::Os(l), Self::Os(r))), + } + } + + fn try_rsplit_seq<'a, T>(self, pat: T) -> Result<(Self, Self), RsplitOnceError> + where + T: AsStrandView<'a, Self::Strand<'a>>, + { + let pat = pat.as_strand_view(); + match self { + PathDyn::Os(p) => { + let (l, r) = p.try_rsplit_seq(pat.as_os().map_err(|_| RsplitOnceError::AsOs)?)?; + Ok((Self::Os(l), Self::Os(r))) + } + } + } + + fn try_starts_with<'a, T>(self, base: T) -> Result + where + T: AsStrandView<'a, Self::Strand<'a>>, + { + Ok(match (self, base.as_strand_view()) { + (Self::Os(p), Strand::Os(q)) => p.starts_with(q), + (Self::Os(p), Strand::Utf8(q)) => p.starts_with(q), + (Self::Os(p), Strand::Bytes(b)) => { + p.starts_with(OsStr::from_wtf8(b).map_err(|_| StartsWithError)?) + } + }) + } + + fn try_strip_prefix<'a, T>(self, base: T) -> Result + where + T: AsStrandView<'a, Self::Strand<'a>>, + { + Ok(match (self, base.as_strand_view()) { + (Self::Os(p), Strand::Os(q)) => Self::Os(p.strip_prefix(q)?), + (Self::Os(p), Strand::Utf8(q)) => Self::Os(p.strip_prefix(q)?), + (Self::Os(p), Strand::Bytes(b)) => { + Self::Os(p.strip_prefix(OsStr::from_wtf8(b).map_err(|_| StripPrefixError::WrongEncoding)?)?) + } }) } } -impl PartialEq for PathDyn<'_> { - fn eq(&self, other: &PathBufDyn) -> bool { *self == other.borrow() } -} +impl<'a> PathDyn<'a> { + #[inline] + pub fn os(path: &'a T) -> Self + where + T: ?Sized + AsRef, + { + Self::Os(path.as_ref()) + } -impl PartialEq> for &std::path::Path { - fn eq(&self, other: &PathDyn<'_>) -> bool { matches!(*other, PathDyn::Os(p) if p == *self) } -} + #[inline] + pub fn as_os(self) -> Result<&'a std::path::Path, PathDynError> { + match self { + Self::Os(p) => Ok(p), + } + } -impl Equivalent for PathDyn<'_> { - fn equivalent(&self, key: &PathBufDyn) -> bool { *self == key.borrow() } + #[inline] + pub fn to_os_owned(self) -> Result { + match self { + Self::Os(p) => Ok(p.to_owned()), + } + } + + pub fn with(kind: SchemeKind, strand: &'a T) -> Result + where + T: ?Sized + AsStrandDyn, + { + use SchemeKind as K; + + let s = strand.as_strand_dyn(); + Ok(match kind { + K::Regular | K::Search | K::Archive => Self::Os(std::path::Path::new(s.as_os()?)), + K::Sftp => Self::Os(std::path::Path::new(s.as_os()?)), // FIXME + }) + } + + #[inline] + pub unsafe fn from_encoded_bytes(kind: impl Into, bytes: &'a [u8]) -> Self { + match kind.into() { + PathKind::Os => Self::Os(unsafe { OsStr::from_encoded_bytes_unchecked(bytes) }.as_ref()), + } + } } // --- PathBufDyn @@ -126,67 +256,20 @@ pub enum PathBufDyn { Os(std::path::PathBuf), } -impl PathBufDyn { - pub const fn os_default() -> Self { Self::Os(std::path::PathBuf::new()) } -} - -impl PathBufLike for PathBufDyn { - type Borrowed<'a> = PathDyn<'a>; - type Inner = Vec; - type InnerRef<'a> = &'a [u8]; - - fn borrow(&self) -> Self::Borrowed<'_> { - match self { - Self::Os(p) => Self::Borrowed::Os(p.as_path()), - } - } - - fn encoded_bytes(&self) -> &[u8] { - match self { - Self::Os(p) => p.as_os_str().as_encoded_bytes(), - } - } - - // FIXME: remove - unsafe fn from_encoded_bytes(bytes: Vec) -> Self { - Self::Os(std::path::PathBuf::from(unsafe { - std::ffi::OsString::from_encoded_bytes_unchecked(bytes) - })) - } - - fn into_encoded_bytes(self) -> Vec { - match self { - Self::Os(p) => p.into_os_string().into_encoded_bytes(), - } - } - - fn set_file_name(&mut self, name: T) - where - T: for<'a> AsInnerView<'a, Self::InnerRef<'a>>, - { - // TODO: introduce a new `PathInnerDyn` - todo!() - } - - fn take(&mut self) -> Self { - match self { - Self::Os(p) => Self::Os(std::mem::take(p)), - } - } -} - -impl From> for PathBufDyn { - fn from(value: PathDyn<'_>) -> Self { - match value { - PathDyn::Os(p) => Self::Os(p.to_path_buf()), - } - } +impl From for PathBufDyn { + fn from(value: std::path::PathBuf) -> Self { Self::Os(value) } } impl From<&PathBufDyn> for PathBufDyn { fn from(value: &PathBufDyn) -> Self { value.clone() } } +impl TryFrom for std::path::PathBuf { + type Error = PathBufDynError; + + fn try_from(value: PathBufDyn) -> Result { value.into_os() } +} + impl PartialEq> for PathBufDyn { fn eq(&self, other: &PathDyn<'_>) -> bool { self.borrow() == *other } } @@ -198,3 +281,131 @@ impl PartialEq> for &PathBufDyn { impl Equivalent> for PathBufDyn { fn equivalent(&self, key: &PathDyn<'_>) -> bool { self.borrow() == *key } } + +impl PathBufLike for PathBufDyn { + type Borrowed<'a> = PathDyn<'a>; + type Strand<'a> = Strand<'a>; + + fn borrow(&self) -> Self::Borrowed<'_> { + match self { + Self::Os(p) => Self::Borrowed::Os(p.as_path()), + } + } + + fn encoded_bytes(&self) -> &[u8] { + match self { + Self::Os(p) => p.as_os_str().as_encoded_bytes(), + } + } + + fn into_dyn(self) -> PathBufDyn { self } + + fn into_encoded_bytes(self) -> Vec { + match self { + Self::Os(p) => p.into_os_string().into_encoded_bytes(), + } + } + + fn try_set_name<'a, T>(&mut self, name: T) -> Result<(), SetNameError> + where + T: AsStrandView<'a, Self::Strand<'a>>, + { + Ok(match (self, name.as_strand_view()) { + (Self::Os(p), Strand::Os(q)) => p.set_file_name(q), + (Self::Os(p), Strand::Utf8(q)) => p.set_file_name(q), + (Self::Os(p), Strand::Bytes(b)) => { + p.set_file_name(OsStr::from_wtf8(b).map_err(|_| SetNameError::FromWtf8)?) + } + }) + } + + fn try_starts_with<'a, T>(&self, base: T) -> Result + where + T: AsStrandView<'a, Self::Strand<'a>>, + { + Ok(match (self, base.as_strand_view()) { + (Self::Os(p), Strand::Os(q)) => p.starts_with(q), + (Self::Os(p), Strand::Utf8(q)) => p.starts_with(q), + (Self::Os(p), Strand::Bytes(b)) => { + p.starts_with(OsStr::from_wtf8(b).map_err(|_| StartsWithError)?) + } + }) + } + + fn take(&mut self) -> Self { + match self { + Self::Os(p) => Self::Os(std::mem::take(p)), + } + } +} + +impl PathBufDyn { + #[inline] + pub fn os(path: impl Into) -> Self { Self::Os(path.into()) } + + #[inline] + pub fn into_os(self) -> Result { + Ok(match self { + PathBufDyn::Os(p) => p, + }) + } + + #[inline] + pub fn new(kind: SchemeKind) -> Self { + use SchemeKind as K; + + match kind { + K::Regular | K::Search | K::Archive => Self::Os(std::path::PathBuf::new()), + K::Sftp => Self::Os(std::path::PathBuf::new()), // FIXME + } + } + + pub fn with(kind: SchemeKind, strand: T) -> Result + where + T: AsStrandDyn, + { + use SchemeKind as K; + + let s = strand.as_strand_dyn(); + Ok(match kind { + K::Regular | K::Search | K::Archive => Self::Os(std::path::PathBuf::from(s.as_os()?)), + K::Sftp => Self::Os(std::path::PathBuf::from(s.as_os()?)), // FIXME + }) + } + + pub fn with_capacity(kind: SchemeKind, capacity: usize) -> Self { + use SchemeKind as K; + match kind { + K::Regular | K::Search | K::Archive => Self::Os(std::path::PathBuf::with_capacity(capacity)), + K::Sftp => Self::Os(std::path::PathBuf::with_capacity(capacity)), // FIXME + } + } + + pub fn try_push(&mut self, path: T) -> Result<(), StrandError> + where + T: AsPathDyn, + { + let path = path.as_path_dyn(); + Ok(match self { + PathBufDyn::Os(p) => p.push(path.as_os()?), + }) + } + + pub fn try_extend(&mut self, paths: T) -> Result<(), StrandError> + where + T: IntoIterator, + T::Item: AsPathDyn, + { + for p in paths { + self.try_push(p)?; + } + Ok(()) + } + + #[inline] + pub unsafe fn from_encoded_bytes(kind: impl Into, bytes: Vec) -> Self { + match kind.into() { + PathKind::Os => Self::Os(unsafe { std::path::PathBuf::from_encoded_bytes(bytes) }), + } + } +} diff --git a/yazi-shared/src/path/error.rs b/yazi-shared/src/path/error.rs new file mode 100644 index 00000000..30f6dc14 --- /dev/null +++ b/yazi-shared/src/path/error.rs @@ -0,0 +1,82 @@ +use thiserror::Error; + +use crate::strand::StrandError; + +// --- EndsWithError +#[derive(Debug, Error)] +#[error("calling ends_with on paths with different encodings")] +pub struct EndsWithError; + +// --- JoinError +#[derive(Debug, Error)] +#[error("calling join on paths with different encodings")] +pub enum JoinError { + FromWtf8, + FromPathBufDyn(#[from] PathBufDynError), +} + +impl From for std::io::Error { + fn from(err: JoinError) -> Self { std::io::Error::other(err) } +} + +// --- PathDynError +#[derive(Debug, Error)] +pub enum PathDynError { + #[error("conversion to OsStr failed")] + AsOs, +} + +impl From for std::io::Error { + fn from(err: PathDynError) -> Self { std::io::Error::other(err) } +} + +// --- PathBufDynError +#[derive(Debug, Error)] +pub enum PathBufDynError { + #[error("conversion to OsString failed")] + IntoOs, +} + +// --- SetNameError +#[derive(Debug, Error)] +#[error("calling set_name on paths with different encodings")] +pub enum SetNameError { + FromWtf8, + FromStrandDyn(#[from] StrandError), +} + +impl From for std::io::Error { + fn from(err: SetNameError) -> Self { std::io::Error::other(err) } +} + +// --- RsplitOnce +#[derive(Error, Debug)] +#[error("calling rsplit_once on paths with different encodings")] +pub enum RsplitOnceError { + #[error("conversion to OsStr failed")] + AsOs, + #[error("conversion to UTF-8 str failed")] + AsUtf8, + #[error("the pattern was not found")] + NotFound, +} + +// --- StartsWithError +#[derive(Error, Debug)] +#[error("calling starts_with on paths with different encodings")] +pub struct StartsWithError; + +// --- StripPrefixError +#[derive(Debug, Error)] +pub enum StripPrefixError { + #[error("calling strip_prefix on URLs with different schemes")] + Exotic, + #[error("the base is not a prefix of the path")] + NotPrefix, + #[error("calling strip_prefix on paths with different encodings")] + WrongEncoding, +} + +impl From for StripPrefixError { + fn from(_: std::path::StripPrefixError) -> Self { Self::NotPrefix } +} diff --git a/yazi-shared/src/path/inner.rs b/yazi-shared/src/path/inner.rs deleted file mode 100644 index 4237078a..00000000 --- a/yazi-shared/src/path/inner.rs +++ /dev/null @@ -1,15 +0,0 @@ -pub trait PathInner<'a>: Copy { - fn encoded_bytes(self) -> &'a [u8]; - - fn is_empty(self) -> bool { self.encoded_bytes().is_empty() } - - fn len(self) -> usize { self.encoded_bytes().len() } -} - -impl<'a> PathInner<'a> for &'a std::ffi::OsStr { - fn encoded_bytes(self) -> &'a [u8] { self.as_encoded_bytes() } -} - -impl<'a> PathInner<'a> for &'a [u8] { - fn encoded_bytes(self) -> &'a [u8] { self } -} diff --git a/yazi-shared/src/path/kind.rs b/yazi-shared/src/path/kind.rs new file mode 100644 index 00000000..4c9e1920 --- /dev/null +++ b/yazi-shared/src/path/kind.rs @@ -0,0 +1,16 @@ +use crate::scheme::SchemeKind; + +pub enum PathKind { + Os, +} + +impl From for PathKind { + fn from(value: SchemeKind) -> Self { + match value { + SchemeKind::Regular => Self::Os, + SchemeKind::Search => Self::Os, + SchemeKind::Archive => Self::Os, + SchemeKind::Sftp => Self::Os, // FIXME + } + } +} diff --git a/yazi-shared/src/path/mod.rs b/yazi-shared/src/path/mod.rs index 99d255a7..30fde723 100644 --- a/yazi-shared/src/path/mod.rs +++ b/yazi-shared/src/path/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(buf component components inner path r#dyn traits); +yazi_macro::mod_flat!(buf component components conversion cow error kind path r#dyn r#unsafe view); diff --git a/yazi-shared/src/path/path.rs b/yazi-shared/src/path/path.rs index 51e83577..43e02dea 100644 --- a/yazi-shared/src/path/path.rs +++ b/yazi-shared/src/path/path.rs @@ -1,15 +1,24 @@ -use crate::path::{AsPathView, PathBufLike, PathInner}; +use std::{borrow::Cow, ffi::OsStr}; + +use anyhow::Result; + +use crate::{BytesExt, Utf8BytePredictor, path::{AsPathView, EndsWithError, JoinError, PathBufDyn, PathBufLike, PathDyn, PathKind, RsplitOnceError, StartsWithError, StripPrefixError}, strand::{AsStrandView, StrandLike}}; pub trait PathLike<'p> where - Self: Copy + AsPathView<'p, Self::View<'p>>, + Self: Copy + AsPathView<'p, Self::View<'p>> + AsStrandView<'p, Self::Strand<'p>>, { - type Inner: PathInner<'p>; + type Strand<'a>: StrandLike<'a>; type Owned: PathBufLike + Into; type View<'a>; - type Components<'a>: AsPathView<'a, Self::View<'a>> + Clone + DoubleEndedIterator; + type Components<'a>: Clone + + DoubleEndedIterator + + AsPathView<'a, Self::View<'a>> + + AsStrandView<'a, Self::Strand<'a>>; type Display<'a>: std::fmt::Display; + fn as_dyn(self) -> PathDyn<'p>; + fn components(self) -> Self::Components<'p>; fn default() -> Self; @@ -18,43 +27,71 @@ where fn encoded_bytes(self) -> &'p [u8]; - fn extension(self) -> Option; + fn ext(self) -> Option>; - fn file_name(self) -> Option; + fn has_root(self) -> bool; - fn file_stem(self) -> Option; - - unsafe fn from_encoded_bytes(bytes: &'p [u8]) -> Self; + fn is_absolute(self) -> bool; fn is_empty(self) -> bool { self.encoded_bytes().is_empty() } #[cfg(unix)] fn is_hidden(self) -> bool { - self.file_name().is_some_and(|n| n.encoded_bytes().first() == Some(&b'.')) + self.name().is_some_and(|n| n.encoded_bytes().first() == Some(&b'.')) } - fn join<'a, T>(self, base: T) -> Self::Owned - where - T: AsPathView<'a, Self::View<'a>>; + fn kind(self) -> PathKind; fn len(self) -> usize { self.encoded_bytes().len() } + fn name(self) -> Option>; + fn owned(self) -> Self::Owned; fn parent(self) -> Option; - fn strip_prefix<'a, T>(self, base: T) -> Option + fn rsplit_pred(self, pred: T) -> Option<(Self, Self)> where - T: AsPathView<'a, Self::View<'a>>; + T: Utf8BytePredictor; + + fn stem(self) -> Option>; + + fn to_str(self) -> Result<&'p str, std::str::Utf8Error> { str::from_utf8(self.encoded_bytes()) } + + fn to_string_lossy(self) -> Cow<'p, str> { String::from_utf8_lossy(self.encoded_bytes()) } + + fn to_buf_dyn(self) -> PathBufDyn { self.as_dyn().owned() } + + fn try_ends_with<'a, T>(self, child: T) -> Result + where + T: AsStrandView<'a, Self::Strand<'a>>; + + fn try_join<'a, T>(self, path: T) -> Result + where + T: AsStrandView<'a, Self::Strand<'a>>; + + fn try_rsplit_seq<'a, T>(self, pat: T) -> Result<(Self, Self), RsplitOnceError> + where + T: AsStrandView<'a, Self::Strand<'a>>; + + fn try_starts_with<'a, T>(self, base: T) -> Result + where + T: AsStrandView<'a, Self::Strand<'a>>; + + fn try_strip_prefix<'a, T>(self, base: T) -> Result + where + T: AsStrandView<'a, Self::Strand<'a>>; } impl<'p> PathLike<'p> for &'p std::path::Path { type Components<'a> = std::path::Components<'a>; type Display<'a> = std::path::Display<'a>; - type Inner = &'p std::ffi::OsStr; type Owned = std::path::PathBuf; + type Strand<'a> = &'a std::ffi::OsStr; type View<'a> = &'a std::path::Path; + fn as_dyn(self) -> PathDyn<'p> { PathDyn::Os(self) } + fn components(self) -> Self::Components<'p> { self.components() } fn default() -> Self { std::path::Path::new("") } @@ -63,31 +100,85 @@ impl<'p> PathLike<'p> for &'p std::path::Path { fn encoded_bytes(self) -> &'p [u8] { self.as_os_str().as_encoded_bytes() } - fn extension(self) -> Option { self.extension() } + fn ext(self) -> Option> { self.extension() } - fn file_name(self) -> Option { self.file_name() } + fn has_root(self) -> bool { self.has_root() } - fn file_stem(self) -> Option { self.file_stem() } + fn is_absolute(self) -> bool { self.is_absolute() } - unsafe fn from_encoded_bytes(bytes: &'p [u8]) -> Self { - std::path::Path::new(unsafe { std::ffi::OsStr::from_encoded_bytes_unchecked(bytes) }) - } + fn kind(self) -> PathKind { PathKind::Os } - fn join<'a, T>(self, base: T) -> Self::Owned - where - T: AsPathView<'a, Self::View<'a>>, - { - self.join(base.as_path_view()) - } + fn name(self) -> Option> { self.file_name() } fn owned(self) -> Self::Owned { self.to_path_buf() } fn parent(self) -> Option { self.parent() } - fn strip_prefix<'a, T>(self, base: T) -> Option + fn stem(self) -> Option> { self.file_stem() } + + fn try_ends_with<'a, T>(self, child: T) -> Result where - T: AsPathView<'a, Self::View<'a>>, + T: AsStrandView<'a, Self::Strand<'a>>, { - self.strip_prefix(base.as_path_view()).ok() + Ok(self.ends_with(child.as_strand_view())) + } + + fn try_join<'a, T>(self, path: T) -> Result + where + T: AsStrandView<'a, Self::Strand<'a>>, + { + Ok(self.join(path.as_strand_view())) + } + + fn rsplit_pred<'a, T>(self, pred: T) -> Option<(Self, Self)> + where + T: Utf8BytePredictor, + { + let b = self.encoded_bytes(); + let (left, right) = b.rsplit_pred_once(pred)?; + + Some(unsafe { + ( + OsStr::from_encoded_bytes_unchecked(left).as_ref(), + OsStr::from_encoded_bytes_unchecked(right).as_ref(), + ) + }) + } + + fn try_rsplit_seq<'a, T>(self, pat: T) -> Result<(Self, Self), RsplitOnceError> + where + T: AsStrandView<'a, Self::Strand<'a>>, + { + let b = self.encoded_bytes(); + let p = pat.as_strand_view().encoded_bytes(); + + let (left, right) = b.rsplit_seq_once(p).ok_or(RsplitOnceError::NotFound)?; + Ok(unsafe { + ( + OsStr::from_encoded_bytes_unchecked(left).as_ref(), + OsStr::from_encoded_bytes_unchecked(right).as_ref(), + ) + }) + } + + fn try_starts_with<'a, T>(self, base: T) -> Result + where + T: AsStrandView<'a, Self::Strand<'a>>, + { + Ok(self.starts_with(base.as_strand_view())) + } + + fn try_strip_prefix<'a, T>(self, base: T) -> Result + where + T: AsStrandView<'a, Self::Strand<'a>>, + { + Ok(self.strip_prefix(base.as_strand_view())?) } } + +impl<'a, P> From

for PathBufDyn +where + P: PathLike<'a>, +{ + fn from(value: P) -> Self { value.to_buf_dyn() } +} diff --git a/yazi-shared/src/path/traits.rs b/yazi-shared/src/path/traits.rs deleted file mode 100644 index f24f1c4d..00000000 --- a/yazi-shared/src/path/traits.rs +++ /dev/null @@ -1,154 +0,0 @@ -use std::{borrow::Cow, ffi::{OsStr, OsString}}; - -use super::{PathBufDyn, PathDyn}; -use crate::path::{PathBufLike, PathLike}; - -pub trait AsPath { - fn as_path(&self) -> impl PathLike<'_>; -} - -impl AsPath for OsStr { - fn as_path(&self) -> impl PathLike<'_> { std::path::Path::new(self) } -} - -impl AsPath for &OsStr { - fn as_path(&self) -> impl PathLike<'_> { std::path::Path::new(self) } -} - -impl AsPath for std::path::Path { - fn as_path(&self) -> impl PathLike<'_> { self } -} - -impl AsPath for std::path::PathBuf { - fn as_path(&self) -> impl PathLike<'_> { self.as_path() } -} - -impl AsPath for PathDyn<'_> { - fn as_path(&self) -> impl PathLike<'_> { *self } -} - -impl AsPath for PathBufDyn { - fn as_path(&self) -> impl PathLike<'_> { self.borrow() } -} - -impl AsPath for &PathBufDyn { - fn as_path(&self) -> impl PathLike<'_> { self.borrow() } -} - -// --- AsPathDyn -pub trait AsPathDyn { - fn as_path_dyn(&self) -> PathDyn<'_>; -} - -impl AsPathDyn for &str { - fn as_path_dyn(&self) -> PathDyn<'_> { std::path::Path::new(self).into() } -} - -impl AsPathDyn for String { - fn as_path_dyn(&self) -> PathDyn<'_> { std::path::Path::new(self).into() } -} - -impl AsPathDyn for &String { - fn as_path_dyn(&self) -> PathDyn<'_> { std::path::Path::new(self).into() } -} - -impl AsPathDyn for OsStr { - fn as_path_dyn(&self) -> PathDyn<'_> { std::path::Path::new(self).into() } -} - -impl AsPathDyn for &OsStr { - fn as_path_dyn(&self) -> PathDyn<'_> { std::path::Path::new(self).into() } -} - -impl AsPathDyn for Cow<'_, OsStr> { - fn as_path_dyn(&self) -> PathDyn<'_> { std::path::Path::new(self).into() } -} - -impl AsPathDyn for std::path::PathBuf { - fn as_path_dyn(&self) -> PathDyn<'_> { self.as_path().into() } -} - -impl AsPathDyn for &std::path::PathBuf { - fn as_path_dyn(&self) -> PathDyn<'_> { self.as_path().into() } -} - -impl AsPathDyn for PathDyn<'_> { - fn as_path_dyn(&self) -> PathDyn<'_> { *self } -} - -impl AsPathDyn for PathBufDyn { - fn as_path_dyn(&self) -> PathDyn<'_> { self.borrow() } -} - -impl AsPathDyn for &PathBufDyn { - fn as_path_dyn(&self) -> PathDyn<'_> { self.borrow() } -} - -// --- AsPathView -pub trait AsPathView<'a, T> { - fn as_path_view(self) -> T; -} - -impl<'a> AsPathView<'a, &'a std::path::Path> for &'a str { - fn as_path_view(self) -> &'a std::path::Path { std::path::Path::new(self) } -} - -impl<'a> AsPathView<'a, &'a std::path::Path> for &'a OsStr { - fn as_path_view(self) -> &'a std::path::Path { std::path::Path::new(self) } -} - -impl<'a> AsPathView<'a, &'a std::path::Path> for &'a std::path::Path { - fn as_path_view(self) -> &'a std::path::Path { self } -} - -impl<'a> AsPathView<'a, &'a std::path::Path> for &'a std::path::PathBuf { - fn as_path_view(self) -> &'a std::path::Path { self } -} - -impl<'a> AsPathView<'a, &'a std::path::Path> for std::path::Components<'a> { - fn as_path_view(self) -> &'a std::path::Path { self.as_path() } -} - -impl<'a> AsPathView<'a, PathDyn<'a>> for &'a PathBufDyn { - fn as_path_view(self) -> PathDyn<'a> { - match self { - PathBufDyn::Os(p) => PathDyn::Os(p.as_path()), - } - } -} - -impl<'a> AsPathView<'a, &'a std::path::Path> for &'a Cow<'_, OsStr> { - fn as_path_view(self) -> &'a std::path::Path { std::path::Path::new(self) } -} - -impl<'a> AsPathView<'a, &'a std::path::Path> for crate::loc::Loc<'a, &'a std::path::Path> { - fn as_path_view(self) -> &'a std::path::Path { *self } -} - -// --- AsInnerView -pub trait AsInnerView<'a, T> { - fn as_inner_view(&'a self) -> T; -} - -impl<'a> AsInnerView<'a, &'a OsStr> for OsStr { - fn as_inner_view(&'a self) -> &'a OsStr { self } -} - -impl<'a> AsInnerView<'a, &'a OsStr> for OsString { - fn as_inner_view(&'a self) -> &'a OsStr { self } -} - -impl<'a> AsInnerView<'a, &'a [u8]> for [u8] { - fn as_inner_view(&'a self) -> &'a [u8] { self } -} - -impl<'a> AsInnerView<'a, &'a [u8]> for Vec { - fn as_inner_view(&'a self) -> &'a [u8] { self } -} - -impl<'a, T, U> AsInnerView<'a, U> for &T -where - T: ?Sized + AsInnerView<'a, U>, -{ - fn as_inner_view(&'a self) -> U { (*self).as_inner_view() } -} diff --git a/yazi-shared/src/path/unsafe.rs b/yazi-shared/src/path/unsafe.rs new file mode 100644 index 00000000..4bfcb847 --- /dev/null +++ b/yazi-shared/src/path/unsafe.rs @@ -0,0 +1,23 @@ +use std::ffi::OsString; + +// --- PathUnsafeExt +pub trait PathUnsafeExt<'p> { + unsafe fn from_encoded_bytes(bytes: &'p [u8]) -> Self; +} + +impl<'p> PathUnsafeExt<'p> for &'p std::path::Path { + unsafe fn from_encoded_bytes(bytes: &'p [u8]) -> Self { + std::path::Path::new(unsafe { std::ffi::OsStr::from_encoded_bytes_unchecked(bytes) }) + } +} + +// --- PathBufUnsafeExt +pub trait PathBufUnsafeExt { + unsafe fn from_encoded_bytes(bytes: Vec) -> Self; +} + +impl PathBufUnsafeExt for std::path::PathBuf { + unsafe fn from_encoded_bytes(bytes: Vec) -> Self { + Self::from(unsafe { OsString::from_encoded_bytes_unchecked(bytes) }) + } +} diff --git a/yazi-shared/src/path/view.rs b/yazi-shared/src/path/view.rs new file mode 100644 index 00000000..a23c8422 --- /dev/null +++ b/yazi-shared/src/path/view.rs @@ -0,0 +1,39 @@ +use std::{borrow::Cow, ffi::OsStr}; + +use crate::path::{AsPathDyn, PathDyn}; + +// --- AsPathView +pub trait AsPathView<'a, T> { + fn as_path_view(self) -> T; +} + +impl<'a> AsPathView<'a, &'a std::path::Path> for &'a OsStr { + fn as_path_view(self) -> &'a std::path::Path { std::path::Path::new(self) } +} + +impl<'a> AsPathView<'a, &'a std::path::Path> for &'a std::path::Path { + fn as_path_view(self) -> &'a std::path::Path { self } +} + +impl<'a> AsPathView<'a, &'a std::path::Path> for &'a std::path::PathBuf { + fn as_path_view(self) -> &'a std::path::Path { self } +} + +impl<'a> AsPathView<'a, &'a std::path::Path> for std::path::Components<'a> { + fn as_path_view(self) -> &'a std::path::Path { self.as_path() } +} + +impl<'a, T> AsPathView<'a, PathDyn<'a>> for &'a T +where + T: AsPathDyn, +{ + fn as_path_view(self) -> PathDyn<'a> { self.as_path_dyn() } +} + +impl<'a> AsPathView<'a, &'a std::path::Path> for &'a Cow<'_, OsStr> { + fn as_path_view(self) -> &'a std::path::Path { std::path::Path::new(self) } +} + +impl<'a> AsPathView<'a, &'a std::path::Path> for crate::loc::Loc<'a, &'a std::path::Path> { + fn as_path_view(self) -> &'a std::path::Path { *self } +} diff --git a/yazi-shared/src/pool/cow.rs b/yazi-shared/src/pool/cow.rs new file mode 100644 index 00000000..5f91283f --- /dev/null +++ b/yazi-shared/src/pool/cow.rs @@ -0,0 +1,96 @@ +use std::ops::Deref; + +use crate::pool::{Pool, Symbol}; + +pub enum SymbolCow<'a, T: ?Sized> { + Borrowed(&'a T), + Owned(Symbol), +} + +impl Clone for SymbolCow<'_, T> { + fn clone(&self) -> Self { + match self { + Self::Borrowed(t) => Self::Borrowed(t), + Self::Owned(t) => Self::Owned(t.clone()), + } + } +} + +impl AsRef<[u8]> for SymbolCow<'_, [u8]> { + fn as_ref(&self) -> &[u8] { + match self { + Self::Borrowed(b) => b, + Self::Owned(b) => b.as_ref(), + } + } +} + +impl AsRef for SymbolCow<'_, str> { + fn as_ref(&self) -> &str { + match self { + Self::Borrowed(s) => s, + Self::Owned(s) => s.as_ref(), + } + } +} + +// --- Deref +impl Deref for SymbolCow<'_, [u8]> { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { self.as_ref() } +} + +impl Deref for SymbolCow<'_, str> { + type Target = str; + + fn deref(&self) -> &Self::Target { self.as_ref() } +} + +// --- From +impl<'a, T: ?Sized> From<&'a T> for SymbolCow<'a, T> { + fn from(value: &'a T) -> Self { Self::Borrowed(value) } +} + +impl From> for SymbolCow<'_, T> { + fn from(value: Symbol) -> Self { Self::Owned(value) } +} + +impl From> for Symbol<[u8]> { + fn from(value: SymbolCow<'_, [u8]>) -> Self { value.into_owned() } +} + +impl From> for Symbol { + fn from(value: SymbolCow<'_, str>) -> Self { value.into_owned() } +} + +// --- Debug +impl std::fmt::Debug for SymbolCow<'_, [u8]> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "SymbolCow<[u8]>({:?})", self.as_ref()) + } +} + +impl std::fmt::Debug for SymbolCow<'_, str> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "SymbolCow({:?})", self.as_ref()) + } +} + +impl SymbolCow<'_, [u8]> { + pub fn into_owned(self) -> Symbol<[u8]> { + match self { + Self::Borrowed(t) => Pool::<[u8]>::intern(t), + Self::Owned(t) => t, + } + } +} + +impl SymbolCow<'_, str> { + pub fn into_owned(self) -> Symbol { + match self { + Self::Borrowed(t) => Pool::::intern(t), + Self::Owned(t) => t, + } + } +} diff --git a/yazi-shared/src/pool/mod.rs b/yazi-shared/src/pool/mod.rs index dd9924e8..cf34b1dc 100644 --- a/yazi-shared/src/pool/mod.rs +++ b/yazi-shared/src/pool/mod.rs @@ -1,4 +1,4 @@ -yazi_macro::mod_flat!(pool ptr symbol traits); +yazi_macro::mod_flat!(cow pool ptr symbol traits); static SYMBOLS: crate::RoCell< parking_lot::Mutex>, diff --git a/yazi-shared/src/predictor.rs b/yazi-shared/src/predictor.rs new file mode 100644 index 00000000..4603e2e9 --- /dev/null +++ b/yazi-shared/src/predictor.rs @@ -0,0 +1,29 @@ +// --- BytePredictor +pub trait BytePredictor { + fn predicate(&self, byte: u8) -> bool; +} + +// --- Utf8BytePredictor +pub trait Utf8BytePredictor { + fn predicate(&self, byte: u8) -> bool; +} + +// --- AnyAsciiChar +pub struct AnyAsciiChar<'a>(&'a [u8]); + +impl<'a> AnyAsciiChar<'a> { + pub fn new(chars: &'a [u8]) -> Option { + if chars.iter().all(|&b| b <= 0x7f) { Some(Self(chars)) } else { None } + } +} + +impl Utf8BytePredictor for AnyAsciiChar<'_> { + fn predicate(&self, byte: u8) -> bool { self.0.contains(&byte) } +} + +impl BytePredictor for T +where + T: Utf8BytePredictor, +{ + fn predicate(&self, byte: u8) -> bool { self.predicate(byte) } +} diff --git a/yazi-shared/src/scheme/cow.rs b/yazi-shared/src/scheme/cow.rs index 7f35a927..3e16e4cb 100644 --- a/yazi-shared/src/scheme/cow.rs +++ b/yazi-shared/src/scheme/cow.rs @@ -1,9 +1,9 @@ -use std::{borrow::Cow, ops::Not, path::Path}; +use std::{borrow::Cow, ops::Not}; -use anyhow::{Result, bail, ensure}; +use anyhow::{Result, ensure}; use percent_encoding::percent_decode; -use crate::{BytesExt, pool::InternStr, scheme::{AsScheme, Scheme, SchemeRef}}; +use crate::{path::{AsPath, PathCow, PathLike}, pool::{InternStr, SymbolCow}, scheme::{AsScheme, Scheme, SchemeKind, SchemeRef}, url::Url}; #[derive(Clone, Debug)] pub enum SchemeCow<'a> { @@ -11,10 +11,6 @@ pub enum SchemeCow<'a> { Owned(Scheme), } -impl Default for SchemeCow<'_> { - fn default() -> Self { Self::Borrowed(SchemeRef::Regular) } -} - impl<'a> From> for SchemeCow<'a> { fn from(value: SchemeRef<'a>) -> Self { Self::Borrowed(value) } } @@ -31,71 +27,71 @@ impl From for SchemeCow<'_> { } impl From> for Scheme { - fn from(value: SchemeCow<'_>) -> Self { - match value { - SchemeCow::Borrowed(s) => s.into(), - SchemeCow::Owned(s) => s, - } - } + fn from(value: SchemeCow<'_>) -> Self { value.into_owned() } } impl<'a> SchemeCow<'a> { - pub fn search(domain: impl Into>) -> Self { + pub fn regular(uri: usize, urn: usize) -> Self { SchemeRef::Regular { uri, urn }.into() } + + pub fn search(domain: T, uri: usize, urn: usize) -> Self + where + T: Into>, + { match domain.into() { - Cow::Borrowed(s) => SchemeRef::Search(s).into(), - Cow::Owned(s) => Scheme::Search(s.intern()).into(), + Cow::Borrowed(domain) => SchemeRef::Search { domain, uri, urn }.into(), + Cow::Owned(domain) => Scheme::Search { domain: domain.intern(), uri, urn }.into(), } } - pub fn archive(domain: impl Into>) -> Self { + pub fn archive(domain: T, uri: usize, urn: usize) -> Self + where + T: Into>, + { match domain.into() { - Cow::Borrowed(s) => SchemeRef::Archive(s).into(), - Cow::Owned(s) => Scheme::Archive(s.intern()).into(), + Cow::Borrowed(domain) => SchemeRef::Archive { domain, uri, urn }.into(), + Cow::Owned(domain) => Scheme::Archive { domain: domain.intern(), uri, urn }.into(), } } - pub fn sftp(domain: impl Into>) -> Self { + pub fn sftp(domain: T, uri: usize, urn: usize) -> Self + where + T: Into>, + { match domain.into() { - Cow::Borrowed(s) => SchemeRef::Sftp(s).into(), - Cow::Owned(s) => Scheme::Sftp(s.intern()).into(), + Cow::Borrowed(domain) => SchemeRef::Sftp { domain, uri, urn }.into(), + Cow::Owned(domain) => Scheme::Sftp { domain: domain.intern(), uri, urn }.into(), } } - pub(crate) fn parse( - bytes: &'a [u8], - skip: &mut usize, - ) -> Result<(Self, bool, Option, Option)> { - let Some((mut protocol, rest)) = bytes.split_by_seq(b"://") else { - return Ok((Self::default(), false, None, None)); + pub fn parse(bytes: &'a [u8]) -> Result<(Self, PathCow<'a>)> { + let Some((kind, tilde)) = SchemeKind::parse(bytes)? else { + let path = Self::decode_path(SchemeKind::Regular, false, bytes)?; + let (uri, urn) = Self::normalize_ports(SchemeKind::Regular, None, None, &path)?; + return Ok((Self::regular(uri, urn), path)); }; - // Advance to the beginning of the path - *skip += 3 + protocol.len(); - - // Tilded schemes - let tilde = protocol.ends_with(b"~"); - if tilde { - protocol = &protocol[..protocol.len() - 1]; - } - - let (scheme, uri, urn) = match protocol { - b"regular" => (Self::default(), None, None), - b"search" => { - let (domain, uri, urn) = Self::decode_param(rest, skip)?; - (Self::search(domain), uri, urn) - } - b"archive" => { - let (domain, uri, urn) = Self::decode_param(rest, skip)?; - (Self::archive(domain), uri, urn) - } - b"sftp" => { - let (domain, uri, urn) = Self::decode_param(rest, skip)?; - (Self::sftp(domain), uri, urn) - } - _ => bail!("Could not parse protocol from URL: {}", String::from_utf8_lossy(bytes)), + // Decode domain and ports + let mut skip = kind.offset(tilde); + let (domain, uri, urn) = match kind { + SchemeKind::Regular => ("".into(), None, None), + SchemeKind::Search => Self::decode_param(&bytes[skip..], &mut skip)?, + SchemeKind::Archive => Self::decode_param(&bytes[skip..], &mut skip)?, + SchemeKind::Sftp => Self::decode_param(&bytes[skip..], &mut skip)?, }; - Ok((scheme, tilde, uri, urn)) + // Decode path + let path = Self::decode_path(kind, tilde, &bytes[skip..])?; + + // Build scheme + let (uri, urn) = Self::normalize_ports(kind, uri, urn, &path)?; + let scheme = match kind { + SchemeKind::Regular => Self::regular(uri, urn), + SchemeKind::Search => Self::search(domain, uri, urn), + SchemeKind::Archive => Self::archive(domain, uri, urn), + SchemeKind::Sftp => Self::sftp(domain, uri, urn), + }; + + Ok((scheme, path)) } fn decode_param( @@ -132,35 +128,68 @@ impl<'a> SchemeCow<'a> { Ok((b, a)) } - pub(crate) fn normalize_ports( - &self, + fn decode_path(kind: SchemeKind, tilde: bool, bytes: &'a [u8]) -> Result> { + let bytes: Cow<_> = if tilde { percent_decode(bytes).into() } else { bytes.into() }; + Ok(match kind { + SchemeKind::Regular => PathCow::from_os_bytes(bytes)?, + SchemeKind::Search => PathCow::from_os_bytes(bytes)?, + SchemeKind::Archive => PathCow::from_os_bytes(bytes)?, + SchemeKind::Sftp => PathCow::from_os_bytes(bytes)?, + }) + } + + fn normalize_ports( + kind: SchemeKind, uri: Option, urn: Option, - path: &Path, - ) -> Result> { - Ok(match self.as_scheme() { - SchemeRef::Regular => { + path: &PathCow, + ) -> Result<(usize, usize)> { + let path = path.as_path(); + Ok(match kind { + SchemeKind::Regular => { ensure!(uri.is_none() && urn.is_none(), "Regular scheme cannot have ports"); - None + (path.is_empty().not() as usize, path.name().is_some() as usize) } - SchemeRef::Search(_) => { + SchemeKind::Search => { let (uri, urn) = (uri.unwrap_or(0), urn.unwrap_or(0)); ensure!(uri == urn, "Search scheme requires URI and URN to be equal"); - Some((uri, urn)) + (uri, urn) } - SchemeRef::Archive(_) => Some((uri.unwrap_or(0), urn.unwrap_or(0))), - SchemeRef::Sftp(_) => { - let uri = uri.unwrap_or(path.as_os_str().is_empty().not() as usize); - let urn = urn.unwrap_or(path.file_name().is_some() as usize); - Some((uri, urn)) + SchemeKind::Archive => (uri.unwrap_or(0), urn.unwrap_or(0)), + SchemeKind::Sftp => { + let uri = uri.unwrap_or(path.is_empty().not() as usize); + let urn = urn.unwrap_or(path.name().is_some() as usize); + (uri, urn) } }) } + + pub fn retrieve_ports(url: Url) -> (usize, usize) { + match url { + Url::Regular(loc) => (loc.is_empty().not() 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()), + } + } } -impl SchemeCow<'_> { +impl<'a> SchemeCow<'a> { #[inline] - pub fn into_owned(self) -> Scheme { self.into() } + pub fn into_owned(self) -> Scheme { + match self { + Self::Borrowed(s) => s.to_owned(), + Self::Owned(s) => s, + } + } + + #[inline] + pub fn into_domain(self) -> Option> { + Some(match self { + SchemeCow::Borrowed(s) => s.domain()?.into(), + SchemeCow::Owned(s) => s.into_domain()?.into(), + }) + } } #[cfg(test)] diff --git a/yazi-shared/src/scheme/encode.rs b/yazi-shared/src/scheme/encode.rs new file mode 100644 index 00000000..7b97244c --- /dev/null +++ b/yazi-shared/src/scheme/encode.rs @@ -0,0 +1,63 @@ +use std::{fmt::{self, Display}, ops::Not}; + +use percent_encoding::{AsciiSet, CONTROLS, PercentEncode, percent_encode}; + +use crate::{path::PathLike, scheme::SchemeKind, url::Url}; + +#[derive(Clone, Copy)] +pub struct Encode<'a>(pub Url<'a>); + +impl<'a> From> for Encode<'a> { + fn from(value: crate::url::Encode<'a>) -> Self { Self(value.0) } +} + +impl<'a> Encode<'a> { + #[inline] + pub fn domain<'s>(s: &'s str) -> PercentEncode<'s> { + const SET: &AsciiSet = &CONTROLS.add(b'/').add(b':'); + percent_encode(s.as_bytes(), SET) + } + + pub(crate) fn ports(self) -> impl Display { + struct D<'a>(Encode<'a>); + + impl Display for D<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + macro_rules! w { + ($default_uri:expr, $default_urn:expr) => {{ + let (uri, urn) = self.0.0.scheme().ports(); + match (uri != $default_uri, urn != $default_urn) { + (true, true) => write!(f, ":{uri}:{urn}"), + (true, false) => write!(f, ":{uri}"), + (false, true) => write!(f, "::{urn}"), + (false, false) => Ok(()), + } + }}; + } + + match self.0.0.kind() { + SchemeKind::Regular => Ok(()), + SchemeKind::Search | SchemeKind::Archive => w!(0, 0), + SchemeKind::Sftp => { + w!(self.0.0.loc().is_empty().not() as usize, self.0.0.loc().name().is_some() as usize) + } + } + } + } + + D(self) + } +} + +impl Display for Encode<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.0 { + Url::Regular(_) => write!(f, "regular://"), + Url::Search { domain, .. } => write!(f, "search://{}{}/", Self::domain(domain), self.ports()), + Url::Archive { domain, .. } => { + write!(f, "archive://{}{}/", Self::domain(domain), self.ports()) + } + Url::Sftp { domain, .. } => write!(f, "sftp://{}{}/", Self::domain(domain), self.ports()), + } + } +} diff --git a/yazi-shared/src/scheme/kind.rs b/yazi-shared/src/scheme/kind.rs new file mode 100644 index 00000000..88bccc30 --- /dev/null +++ b/yazi-shared/src/scheme/kind.rs @@ -0,0 +1,92 @@ +use anyhow::{Result, bail}; + +use crate::{BytesExt, scheme::{AsScheme, SchemeRef}}; + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum SchemeKind { + Regular, + Search, + Archive, + Sftp, +} + +impl From for SchemeKind +where + T: AsScheme, +{ + fn from(value: T) -> Self { + match value.as_scheme() { + SchemeRef::Regular { .. } => Self::Regular, + SchemeRef::Search { .. } => Self::Search, + SchemeRef::Archive { .. } => Self::Archive, + SchemeRef::Sftp { .. } => Self::Sftp, + } + } +} + +impl TryFrom<&[u8]> for SchemeKind { + type Error = anyhow::Error; + + fn try_from(value: &[u8]) -> Result { + match value { + b"regular" => Ok(Self::Regular), + b"search" => Ok(Self::Search), + b"archive" => Ok(Self::Archive), + b"sftp" => Ok(Self::Sftp), + _ => bail!("invalid scheme kind: {}", String::from_utf8_lossy(value)), + } + } +} + +impl SchemeKind { + #[inline] + pub const fn as_str(self) -> &'static str { + match self { + Self::Regular => "regular", + Self::Search => "search", + Self::Archive => "archive", + Self::Sftp => "sftp", + } + } + + #[inline] + pub fn is_local(self) -> bool { + match self { + Self::Regular | Self::Search => true, + Self::Archive | Self::Sftp => false, + } + } + + #[inline] + pub fn is_remote(self) -> bool { + match self { + Self::Regular | Self::Search | Self::Archive => false, + Self::Sftp => true, + } + } + + #[inline] + pub fn is_virtual(self) -> bool { + match self { + Self::Regular | Self::Search => false, + Self::Archive | Self::Sftp => true, + } + } + + #[inline] + pub(super) const fn offset(self, tilde: bool) -> usize { + 3 + self.as_str().len() + tilde as usize + } + + pub fn parse(bytes: &[u8]) -> Result> { + let Some((kind, _)) = bytes.split_seq_once(b"://") else { + return Ok(None); + }; + + Ok(Some(if let Some(stripped) = kind.strip_suffix(b"~") { + (Self::try_from(stripped)?, true) + } else { + (Self::try_from(kind)?, false) + })) + } +} diff --git a/yazi-shared/src/scheme/mod.rs b/yazi-shared/src/scheme/mod.rs index 60bc0744..f65b6204 100644 --- a/yazi-shared/src/scheme/mod.rs +++ b/yazi-shared/src/scheme/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(cow r#ref scheme traits); +yazi_macro::mod_flat!(cow encode kind r#ref scheme traits); diff --git a/yazi-shared/src/scheme/ref.rs b/yazi-shared/src/scheme/ref.rs index 06cf2961..1468305e 100644 --- a/yazi-shared/src/scheme/ref.rs +++ b/yazi-shared/src/scheme/ref.rs @@ -1,44 +1,84 @@ -use crate::{pool::InternStr, scheme::{AsScheme, Scheme}}; +use std::{hash::Hash, ops::Deref}; -#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] +use crate::{pool::InternStr, scheme::{AsScheme, Scheme, SchemeKind}}; + +#[derive(Clone, Copy, Debug)] pub enum SchemeRef<'a> { - #[default] - Regular, - - Search(&'a str), - - Archive(&'a str), - - Sftp(&'a str), + Regular { uri: usize, urn: usize }, + Search { domain: &'a str, uri: usize, urn: usize }, + Archive { domain: &'a str, uri: usize, urn: usize }, + Sftp { domain: &'a str, uri: usize, urn: usize }, } -impl From> for Scheme { - fn from(value: SchemeRef) -> Self { - match value { - SchemeRef::Regular => Self::Regular, - SchemeRef::Search(d) => Self::Search(d.intern()), - SchemeRef::Archive(d) => Self::Archive(d.intern()), - SchemeRef::Sftp(d) => Self::Sftp(d.intern()), +impl Deref for SchemeRef<'_> { + type Target = SchemeKind; + + #[inline] + fn deref(&self) -> &Self::Target { + match self { + Self::Regular { .. } => &SchemeKind::Regular, + Self::Search { .. } => &SchemeKind::Search, + Self::Archive { .. } => &SchemeKind::Archive, + Self::Sftp { .. } => &SchemeKind::Sftp, } } } +impl Hash for SchemeRef<'_> { + fn hash(&self, state: &mut H) { + self.kind().hash(state); + self.domain().hash(state); + } +} + +impl PartialEq> for SchemeRef<'_> { + fn eq(&self, other: &SchemeRef) -> bool { + self.kind() == other.kind() && self.domain() == other.domain() + } +} + +impl From> for Scheme { + fn from(value: SchemeRef) -> Self { value.to_owned() } +} + impl<'a> SchemeRef<'a> { #[inline] - pub const fn kind(self) -> &'static str { + pub const fn kind(self) -> SchemeKind { match self { - Self::Regular => "regular", - Self::Search(_) => "search", - Self::Archive(_) => "archive", - Self::Sftp(_) => "sftp", + Self::Regular { .. } => SchemeKind::Regular, + Self::Search { .. } => SchemeKind::Search, + Self::Archive { .. } => SchemeKind::Archive, + Self::Sftp { .. } => SchemeKind::Sftp, } } #[inline] pub const fn domain(self) -> Option<&'a str> { match self { - Self::Regular => None, - Self::Search(s) | Self::Archive(s) | Self::Sftp(s) => Some(s), + Self::Regular { .. } => None, + Self::Search { domain, .. } | Self::Archive { domain, .. } | Self::Sftp { domain, .. } => { + Some(domain) + } + } + } + + #[inline] + pub const fn ports(self) -> (usize, usize) { + match self { + Self::Regular { uri, urn } => (uri, urn), + Self::Search { uri, urn, .. } => (uri, urn), + Self::Archive { uri, urn, .. } => (uri, urn), + Self::Sftp { uri, urn, .. } => (uri, urn), + } + } + + #[inline] + pub const fn zeroed(self) -> Self { + match self { + Self::Regular { .. } => Self::Regular { uri: 0, urn: 0 }, + Self::Search { domain, .. } => Self::Search { domain, uri: 0, urn: 0 }, + Self::Archive { domain, .. } => Self::Archive { domain, uri: 0, urn: 0 }, + Self::Sftp { domain, .. } => Self::Sftp { domain, uri: 0, urn: 0 }, } } @@ -48,27 +88,12 @@ impl<'a> SchemeRef<'a> { if self.is_virtual() || other.is_virtual() { self == other } else { true } } - #[inline] - pub fn is_local(self) -> bool { + pub fn to_owned(self) -> Scheme { match self { - Self::Regular | Self::Search(_) => true, - Self::Archive(_) | Self::Sftp(_) => false, - } - } - - #[inline] - pub fn is_remote(self) -> bool { - match self { - Self::Regular | Self::Search(_) | Self::Archive(_) => false, - Self::Sftp(_) => true, - } - } - - #[inline] - pub fn is_virtual(self) -> bool { - match self { - Self::Regular | Self::Search(_) => false, - Self::Archive(_) | Self::Sftp(_) => true, + Self::Regular { uri, urn } => Scheme::Regular { uri, urn }, + Self::Search { domain, uri, urn } => Scheme::Search { domain: domain.intern(), uri, urn }, + Self::Archive { domain, uri, urn } => Scheme::Archive { domain: domain.intern(), uri, urn }, + Self::Sftp { domain, uri, urn } => Scheme::Sftp { domain: domain.intern(), uri, urn }, } } } diff --git a/yazi-shared/src/scheme/scheme.rs b/yazi-shared/src/scheme/scheme.rs index a7d54fe6..886dbca0 100644 --- a/yazi-shared/src/scheme/scheme.rs +++ b/yazi-shared/src/scheme/scheme.rs @@ -2,16 +2,12 @@ use std::hash::{Hash, Hasher}; use crate::{pool::Symbol, scheme::{AsScheme, SchemeRef}}; -#[derive(Clone, Debug, Default, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub enum Scheme { - #[default] - Regular, - - Search(Symbol), - - Archive(Symbol), - - Sftp(Symbol), + Regular { uri: usize, urn: usize }, + Search { domain: Symbol, uri: usize, urn: usize }, + Archive { domain: Symbol, uri: usize, urn: usize }, + Sftp { domain: Symbol, uri: usize, urn: usize }, } impl Hash for Scheme { @@ -21,3 +17,15 @@ impl Hash for Scheme { impl PartialEq> for Scheme { fn eq(&self, other: &SchemeRef<'_>) -> bool { self.as_scheme() == *other } } + +impl Scheme { + #[inline] + pub fn into_domain(self) -> Option> { + match self { + Self::Regular { .. } => None, + Self::Search { domain, .. } | Self::Archive { domain, .. } | Self::Sftp { domain, .. } => { + Some(domain) + } + } + } +} diff --git a/yazi-shared/src/scheme/traits.rs b/yazi-shared/src/scheme/traits.rs index e4a2b477..5467fbf0 100644 --- a/yazi-shared/src/scheme/traits.rs +++ b/yazi-shared/src/scheme/traits.rs @@ -1,4 +1,4 @@ -use crate::scheme::{Scheme, SchemeCow, SchemeRef}; +use crate::scheme::{Scheme, SchemeCow, SchemeKind, SchemeRef}; pub trait AsScheme { fn as_scheme(&self) -> SchemeRef<'_>; @@ -12,11 +12,11 @@ impl AsScheme for SchemeRef<'_> { impl AsScheme for Scheme { #[inline] fn as_scheme(&self) -> SchemeRef<'_> { - match self { - Scheme::Regular => SchemeRef::Regular, - Scheme::Search(d) => SchemeRef::Search(d), - Scheme::Archive(d) => SchemeRef::Archive(d), - Scheme::Sftp(d) => SchemeRef::Sftp(d), + match *self { + Scheme::Regular { uri, urn } => SchemeRef::Regular { uri, urn }, + Scheme::Search { ref domain, uri, urn } => SchemeRef::Search { domain, uri, urn }, + Scheme::Archive { ref domain, uri, urn } => SchemeRef::Archive { domain, uri, urn }, + Scheme::Sftp { ref domain, uri, urn } => SchemeRef::Sftp { domain, uri, urn }, } } } @@ -46,7 +46,7 @@ pub trait SchemeLike where Self: AsScheme + Sized, { - fn kind(&self) -> &'static str { self.as_scheme().kind() } + fn kind(&self) -> SchemeKind { *self.as_scheme() } fn domain(&self) -> Option<&str> { self.as_scheme().domain() } diff --git a/yazi-shared/src/strand/conversion.rs b/yazi-shared/src/strand/conversion.rs new file mode 100644 index 00000000..ad2ce1aa --- /dev/null +++ b/yazi-shared/src/strand/conversion.rs @@ -0,0 +1,159 @@ +use std::{borrow::Cow, ffi::{OsStr, OsString}}; + +use crate::{path::{PathBufDyn, PathDyn}, strand::{Strand, StrandBuf, StrandBufLike, StrandCow, StrandLike}}; + +// --- AsStrand +pub trait AsStrand { + fn as_strand(&self) -> impl StrandLike<'_>; +} + +impl AsStrand for [u8] { + fn as_strand(&self) -> impl StrandLike<'_> { self } +} + +impl AsStrand for &[u8] { + fn as_strand(&self) -> impl StrandLike<'_> { *self } +} + +impl AsStrand for Vec { + fn as_strand(&self) -> impl StrandLike<'_> { self.as_slice() } +} + +impl AsStrand for str { + fn as_strand(&self) -> impl StrandLike<'_> { self } +} + +impl AsStrand for &str { + fn as_strand(&self) -> impl StrandLike<'_> { *self } +} + +impl AsStrand for String { + fn as_strand(&self) -> impl StrandLike<'_> { self.as_str() } +} + +impl AsStrand for OsStr { + fn as_strand(&self) -> impl StrandLike<'_> { self } +} + +impl AsStrand for &OsStr { + fn as_strand(&self) -> impl StrandLike<'_> { *self } +} + +impl AsStrand for OsString { + fn as_strand(&self) -> impl StrandLike<'_> { self.as_os_str() } +} + +impl AsStrand for Cow<'_, OsStr> { + fn as_strand(&self) -> impl StrandLike<'_> { AsRef::::as_ref(self) } +} + +impl AsStrand for PathDyn<'_> { + fn as_strand(&self) -> impl StrandLike<'_> { + match self { + Self::Os(p) => p.as_os_str(), + } + } +} + +impl AsStrand for &PathBufDyn { + fn as_strand(&self) -> impl StrandLike<'_> { + match self { + PathBufDyn::Os(p) => p.as_os_str(), + } + } +} + +impl AsStrand for Strand<'_> { + fn as_strand(&self) -> impl StrandLike<'_> { *self } +} + +impl AsStrand for StrandBuf { + fn as_strand(&self) -> impl StrandLike<'_> { self.borrow() } +} + +impl AsStrand for StrandCow<'_> { + fn as_strand(&self) -> impl StrandLike<'_> { + match self { + StrandCow::Borrowed(s) => *s, + StrandCow::Owned(s) => s.into(), + } + } +} + +impl AsStrand for &StrandCow<'_> { + fn as_strand(&self) -> impl StrandLike<'_> { (**self).as_strand() } +} + +// --- AsStrandDyn +pub trait AsStrandDyn { + fn as_strand_dyn(&self) -> Strand<'_>; +} + +impl AsStrandDyn for OsStr { + fn as_strand_dyn(&self) -> Strand<'_> { Strand::Os(self) } +} + +impl AsStrandDyn for &OsStr { + fn as_strand_dyn(&self) -> Strand<'_> { Strand::Os(self) } +} + +impl AsStrandDyn for OsString { + fn as_strand_dyn(&self) -> Strand<'_> { Strand::Os(self) } +} + +impl AsStrandDyn for &std::path::PathBuf { + fn as_strand_dyn(&self) -> Strand<'_> { Strand::Os(self.as_os_str()) } +} + +impl AsStrandDyn for &str { + fn as_strand_dyn(&self) -> Strand<'_> { Strand::Utf8(self) } +} + +impl AsStrandDyn for String { + fn as_strand_dyn(&self) -> Strand<'_> { Strand::Utf8(self) } +} + +impl AsStrandDyn for &String { + fn as_strand_dyn(&self) -> Strand<'_> { Strand::Utf8(self) } +} + +impl AsStrandDyn for Cow<'_, OsStr> { + fn as_strand_dyn(&self) -> Strand<'_> { Strand::Os(self) } +} + +impl AsStrandDyn for PathDyn<'_> { + fn as_strand_dyn(&self) -> Strand<'_> { + match self { + Self::Os(p) => Strand::Os(p.as_os_str()), + } + } +} + +impl AsStrandDyn for &PathBufDyn { + fn as_strand_dyn(&self) -> Strand<'_> { + match self { + PathBufDyn::Os(p) => Strand::Os(p.as_os_str()), + } + } +} + +impl AsStrandDyn for Strand<'_> { + fn as_strand_dyn(&self) -> Strand<'_> { *self } +} + +impl AsStrandDyn for StrandBuf { + fn as_strand_dyn(&self) -> Strand<'_> { self.borrow() } +} + +impl AsStrandDyn for &StrandBuf { + fn as_strand_dyn(&self) -> Strand<'_> { (*self).borrow() } +} + +impl AsStrandDyn for StrandCow<'_> { + fn as_strand_dyn(&self) -> Strand<'_> { + match self { + Self::Borrowed(s) => s.as_strand_dyn(), + Self::Owned(s) => s.as_strand_dyn(), + } + } +} diff --git a/yazi-shared/src/strand/cow.rs b/yazi-shared/src/strand/cow.rs new file mode 100644 index 00000000..7f7fb567 --- /dev/null +++ b/yazi-shared/src/strand/cow.rs @@ -0,0 +1,75 @@ +use std::{borrow::Cow, ffi::{OsStr, OsString}}; + +use anyhow::Result; + +use crate::{IntoOsStr, scheme::SchemeKind, strand::{Strand, StrandBuf, StrandBufLike, StrandLike}}; + +// --- StrandCow +pub enum StrandCow<'a> { + Borrowed(Strand<'a>), + Owned(StrandBuf), +} + +impl From for StrandCow<'_> { + fn from(value: OsString) -> Self { Self::Owned(StrandBuf::Os(value)) } +} + +impl<'a> From> for StrandCow<'a> { + fn from(value: Cow<'a, OsStr>) -> Self { + match value { + Cow::Borrowed(s) => Self::Borrowed(Strand::Os(s)), + Cow::Owned(s) => Self::Owned(StrandBuf::Os(s)), + } + } +} + +impl<'a> From> for StrandCow<'a> { + fn from(value: Strand<'a>) -> Self { Self::Borrowed(value) } +} + +impl From for StrandCow<'_> { + fn from(value: StrandBuf) -> Self { Self::Owned(value) } +} + +impl PartialEq> for StrandCow<'_> { + fn eq(&self, other: &Strand) -> bool { + match self { + Self::Borrowed(s) => s == other, + Self::Owned(s) => s == other, + } + } +} + +impl<'a> StrandCow<'a> { + pub fn with(kind: SchemeKind, bytes: T) -> Result + where + T: Into>, + { + match kind { + SchemeKind::Regular | SchemeKind::Search | SchemeKind::Archive => Self::from_os_bytes(bytes), + SchemeKind::Sftp => Self::from_os_bytes(bytes), // FIXME + } + } + + pub fn from_os_bytes(bytes: impl Into>) -> Result { + Ok(match bytes.into().into_os_str()? { + Cow::Borrowed(s) => Strand::Os(s).into(), + Cow::Owned(s) => StrandBuf::Os(s).into(), + }) + } + + pub fn into_owned(self) -> StrandBuf { + match self { + Self::Borrowed(s) => s.to_owned(), + Self::Owned(s) => s, + } + } + + // FIXME: remove, instead implement StrandLike for StrandCow + pub fn encoded_bytes(&self) -> &[u8] { + match self { + Self::Borrowed(s) => s.encoded_bytes(), + Self::Owned(s) => s.encoded_bytes(), + } + } +} diff --git a/yazi-shared/src/strand/dyn.rs b/yazi-shared/src/strand/dyn.rs new file mode 100644 index 00000000..db36d266 --- /dev/null +++ b/yazi-shared/src/strand/dyn.rs @@ -0,0 +1,177 @@ +use std::ffi::{OsStr, OsString}; + +use serde::Serialize; + +use crate::{FromWtf8, path::PathDyn, scheme::SchemeKind, strand::{AsStrandDyn, StrandBufLike, StrandError, StrandKind, StrandLike}}; + +// --- Strand +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub enum Strand<'p> { + Os(&'p OsStr), + Utf8(&'p str), + Bytes(&'p [u8]), +} + +impl Default for Strand<'_> { + fn default() -> Self { Self::Utf8("") } +} + +impl<'a> From<&'a OsStr> for Strand<'a> { + fn from(value: &'a OsStr) -> Self { Self::Os(value) } +} + +impl<'a> From<&'a str> for Strand<'a> { + fn from(value: &'a str) -> Self { Self::Utf8(value) } +} + +impl<'a> From<&'a StrandBuf> for Strand<'a> { + fn from(value: &'a StrandBuf) -> Self { + match value { + StrandBuf::Os(s) => Self::Os(s), + StrandBuf::Utf8(s) => Self::Utf8(s), + StrandBuf::Bytes(s) => Self::Bytes(s), + } + } +} + +impl PartialEq<&str> for Strand<'_> { + fn eq(&self, other: &&str) -> bool { + match self { + Self::Os(s) => s == other, + Self::Utf8(s) => s == other, + Self::Bytes(b) => *b == other.as_bytes(), + } + } +} + +impl<'a> Strand<'a> { + pub fn to_owned(self) -> StrandBuf { + match self { + Self::Os(s) => StrandBuf::Os(s.to_owned()), + Self::Utf8(s) => StrandBuf::Utf8(s.to_owned()), + Self::Bytes(b) => StrandBuf::Bytes(b.to_owned()), + } + } +} + +impl<'a> Strand<'a> { + #[inline] + pub fn as_os(self) -> Result<&'a OsStr, StrandError> { + match self { + Self::Os(s) => Ok(s), + Self::Utf8(s) => Ok(OsStr::new(s)), + Self::Bytes(b) => OsStr::from_wtf8(b).map_err(|_| StrandError::AsOs), + } + } + + #[inline] + pub fn as_utf8(self) -> Result<&'a str, StrandError> { + match self { + Self::Os(s) => s.to_str().ok_or(StrandError::AsUtf8), + Self::Utf8(s) => Ok(s), + Self::Bytes(b) => str::from_utf8(b).map_err(|_| StrandError::AsUtf8), + } + } + + #[cfg(windows)] + pub fn backslash_to_slash(self) -> super::StrandCow<'a> { + let bytes = self.encoded_bytes(); + + // Fast path to skip if there are no backslashes + let skip_len = bytes.iter().take_while(|&&b| b != b'\\').count(); + if skip_len >= bytes.len() { + return self.into(); + } + + let (skip, rest) = bytes.split_at(skip_len); + let mut out = Vec::new(); + out.try_reserve_exact(bytes.len()).unwrap_or_else(|_| panic!()); + out.extend(skip); + + for &b in rest { + out.push(if b == b'\\' { b'/' } else { b }); + } + unsafe { StrandBuf::from_encoded_bytes(self.kind(), out) }.into() + } + + #[inline] + pub unsafe fn from_encoded_bytes(kind: impl Into, bytes: &'a [u8]) -> Self { + match kind.into() { + StrandKind::Os => Self::Os(unsafe { OsStr::from_encoded_bytes_unchecked(bytes) }), + StrandKind::Utf8 => Self::Utf8(unsafe { str::from_utf8_unchecked(bytes) }), + StrandKind::Bytes => Self::Bytes(bytes), + } + } +} + +// --- StrandBuf +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] +#[serde(untagged)] +pub enum StrandBuf { + Os(OsString), + Utf8(String), + Bytes(Vec), +} + +impl Default for StrandBuf { + fn default() -> Self { Self::Utf8(String::new()) } +} + +impl From for StrandBuf { + fn from(value: OsString) -> Self { Self::Os(value) } +} + +impl From for StrandBuf { + fn from(value: String) -> Self { Self::Utf8(value) } +} + +impl From> for StrandBuf { + fn from(value: PathDyn) -> Self { + match value { + PathDyn::Os(p) => Self::Os(p.as_os_str().to_owned()), + } + } +} + +impl PartialEq> for StrandBuf { + fn eq(&self, other: &Strand<'_>) -> bool { self.borrow() == *other } +} + +impl StrandBuf { + pub fn clear(&mut self) { + match self { + Self::Os(buf) => buf.clear(), + Self::Utf8(buf) => buf.clear(), + Self::Bytes(buf) => buf.clear(), + } + } + + pub fn try_push(&mut self, s: T) -> Result<(), StrandError> + where + T: AsStrandDyn, + { + let s = s.as_strand_dyn(); + Ok(match self { + Self::Os(buf) => buf.push(s.as_os()?), + Self::Utf8(buf) => buf.push_str(s.as_utf8()?), + Self::Bytes(buf) => buf.extend(s.encoded_bytes()), + }) + } + + pub fn with_capacity(kind: SchemeKind, capacity: usize) -> Self { + use SchemeKind as K; + match kind { + K::Regular | K::Search | K::Archive => Self::Os(OsString::with_capacity(capacity)), + K::Sftp => Self::Os(OsString::with_capacity(capacity)), // FIXME + } + } + + #[inline] + pub unsafe fn from_encoded_bytes(kind: impl Into, bytes: Vec) -> Self { + match kind.into() { + StrandKind::Os => Self::Os(unsafe { OsString::from_encoded_bytes_unchecked(bytes) }), + StrandKind::Utf8 => Self::Utf8(unsafe { String::from_utf8_unchecked(bytes) }), + StrandKind::Bytes => Self::Bytes(bytes), + } + } +} diff --git a/yazi-shared/src/strand/error.rs b/yazi-shared/src/strand/error.rs new file mode 100644 index 00000000..d110a229 --- /dev/null +++ b/yazi-shared/src/strand/error.rs @@ -0,0 +1,24 @@ +use thiserror::Error; + +use crate::path::PathDynError; + +// --- StrandDynError +#[derive(Debug, Error)] +pub enum StrandError { + #[error("conversion to OsStr failed")] + AsOs, + #[error("conversion to UTF-8 str failed")] + AsUtf8, +} + +impl From for StrandError { + fn from(err: PathDynError) -> Self { + match err { + PathDynError::AsOs => Self::AsOs, + } + } +} + +impl From for std::io::Error { + fn from(err: StrandError) -> Self { std::io::Error::other(err) } +} diff --git a/yazi-shared/src/strand/kind.rs b/yazi-shared/src/strand/kind.rs new file mode 100644 index 00000000..77c050aa --- /dev/null +++ b/yazi-shared/src/strand/kind.rs @@ -0,0 +1,5 @@ +pub enum StrandKind { + Os, + Utf8, + Bytes, +} diff --git a/yazi-shared/src/strand/mod.rs b/yazi-shared/src/strand/mod.rs new file mode 100644 index 00000000..25180bc2 --- /dev/null +++ b/yazi-shared/src/strand/mod.rs @@ -0,0 +1 @@ +yazi_macro::mod_flat!(conversion cow error kind r#dyn strand view); diff --git a/yazi-shared/src/strand/strand.rs b/yazi-shared/src/strand/strand.rs new file mode 100644 index 00000000..a3a1dc3e --- /dev/null +++ b/yazi-shared/src/strand/strand.rs @@ -0,0 +1,168 @@ +use std::{borrow::Cow, ffi::{OsStr, OsString}, fmt::Display}; + +use crate::{BytesExt, strand::{AsStrand, Strand, StrandBuf, StrandKind}}; + +// --- StrandLike +pub trait StrandLike<'a>: Copy { + type Owned: StrandBufLike; + + fn contains(self, x: impl AsStrand) -> bool { + memchr::memmem::find(self.encoded_bytes(), x.as_strand().encoded_bytes()).is_some() + } + + fn display(self) -> impl Display; + + fn encoded_bytes(self) -> &'a [u8]; + + fn is_empty(self) -> bool { self.encoded_bytes().is_empty() } + + fn kind(self) -> StrandKind; + + fn len(self) -> usize { self.encoded_bytes().len() } + + fn to_str(self) -> Result<&'a str, std::str::Utf8Error> { str::from_utf8(self.encoded_bytes()) } + + fn to_string_lossy(self) -> Cow<'a, str> { String::from_utf8_lossy(self.encoded_bytes()) } + + fn eq_ignore_ascii_case(self, other: impl AsStrand) -> bool { + self.encoded_bytes().eq_ignore_ascii_case(other.as_strand().encoded_bytes()) + } + + fn starts_with(self, needle: impl AsStrand) -> bool { + self.encoded_bytes().starts_with(needle.as_strand().encoded_bytes()) + } +} + +impl<'a> StrandLike<'a> for &'a [u8] { + type Owned = Vec; + + fn display(self) -> impl Display { BytesExt::display(self) } + + fn encoded_bytes(self) -> &'a [u8] { self } + + fn kind(self) -> StrandKind { StrandKind::Bytes } +} + +impl<'a> StrandLike<'a> for &'a str { + type Owned = String; + + fn display(self) -> impl Display { self } + + fn encoded_bytes(self) -> &'a [u8] { self.as_bytes() } + + fn kind(self) -> StrandKind { StrandKind::Utf8 } +} + +impl<'a> StrandLike<'a> for &'a OsStr { + type Owned = OsString; + + fn display(self) -> impl Display { self.display() } + + fn encoded_bytes(self) -> &'a [u8] { self.as_encoded_bytes() } + + fn kind(self) -> StrandKind { StrandKind::Os } +} + +impl<'a> StrandLike<'a> for Strand<'a> { + type Owned = StrandBuf; + + fn display(self) -> impl Display { + struct D<'a>(Strand<'a>); + + impl<'a> Display for D<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.0 { + Strand::Os(s) => s.display().fmt(f), + Strand::Utf8(s) => s.fmt(f), + Strand::Bytes(b) => BytesExt::display(b).fmt(f), + } + } + } + + D(self) + } + + fn encoded_bytes(self) -> &'a [u8] { + match self { + Self::Os(s) => s.as_encoded_bytes(), + Self::Utf8(s) => s.as_bytes(), + Self::Bytes(b) => b, + } + } + + fn kind(self) -> StrandKind { + match self { + Self::Os(_) => StrandKind::Os, + Self::Utf8(_) => StrandKind::Utf8, + Self::Bytes(_) => StrandKind::Bytes, + } + } +} + +// --- StrandBufLike +pub trait StrandBufLike +where + Self: 'static + AsStrand, +{ + type Borrowed<'a>: StrandLike<'a>; + + fn borrow(&self) -> Self::Borrowed<'_>; + + fn encoded_bytes(&self) -> &[u8] { self.borrow().encoded_bytes() } + + fn into_encoded_bytes(self) -> Vec; + + fn is_empty(&self) -> bool { self.borrow().is_empty() } + + fn kind(&self) -> StrandKind { self.borrow().kind() } + + fn len(&self) -> usize { self.borrow().len() } + + fn to_str(&self) -> Result<&str, std::str::Utf8Error> { self.borrow().to_str() } + + fn to_string_lossy(&self) -> Cow<'_, str> { self.borrow().to_string_lossy() } +} + +impl StrandBufLike for Vec { + type Borrowed<'a> = &'a [u8]; + + fn borrow(&self) -> Self::Borrowed<'_> { self.as_slice() } + + fn into_encoded_bytes(self) -> Vec { self } +} + +impl StrandBufLike for String { + type Borrowed<'a> = &'a str; + + fn borrow(&self) -> Self::Borrowed<'_> { self.as_str() } + + fn into_encoded_bytes(self) -> Vec { self.into_bytes() } +} + +impl StrandBufLike for OsString { + type Borrowed<'a> = &'a OsStr; + + fn borrow(&self) -> Self::Borrowed<'_> { self.as_os_str() } + + fn into_encoded_bytes(self) -> Vec { self.into_encoded_bytes() } +} + +impl StrandBufLike for StrandBuf { + type Borrowed<'a> = Strand<'a>; + + fn borrow(&self) -> Self::Borrowed<'_> { + match self { + Self::Os(s) => Strand::Os(s.as_os_str()), + Self::Utf8(s) => Strand::Utf8(s.as_str()), + Self::Bytes(b) => Strand::Bytes(b), + } + } + + fn into_encoded_bytes(self) -> Vec { + match self { + Self::Os(s) => s.into_encoded_bytes(), + Self::Utf8(s) => s.into_bytes(), + Self::Bytes(b) => b, + } + } +} diff --git a/yazi-shared/src/strand/view.rs b/yazi-shared/src/strand/view.rs new file mode 100644 index 00000000..d29d687a --- /dev/null +++ b/yazi-shared/src/strand/view.rs @@ -0,0 +1,51 @@ +use std::ffi::OsStr; + +use crate::{path::PathDyn, strand::{AsStrandDyn, Strand}}; + +// --- AsStrandView +pub trait AsStrandView<'a, T> { + fn as_strand_view(self) -> T; +} + +impl<'a> AsStrandView<'a, &'a OsStr> for &'a str { + fn as_strand_view(self) -> &'a OsStr { OsStr::new(self) } +} + +impl<'a> AsStrandView<'a, &'a OsStr> for &'a OsStr { + fn as_strand_view(self) -> &'a OsStr { self } +} + +impl<'a> AsStrandView<'a, &'a OsStr> for &'a std::path::Path { + fn as_strand_view(self) -> &'a OsStr { self.as_os_str() } +} + +impl<'a> AsStrandView<'a, &'a OsStr> for std::path::Components<'a> { + fn as_strand_view(self) -> &'a OsStr { self.as_path().as_os_str() } +} + +impl<'a> AsStrandView<'a, Strand<'a>> for &'a str { + fn as_strand_view(self) -> Strand<'a> { Strand::Utf8(self) } +} + +impl<'a> AsStrandView<'a, Strand<'a>> for &'a std::path::Path { + fn as_strand_view(self) -> Strand<'a> { Strand::Os(self.as_os_str()) } +} + +impl<'a> AsStrandView<'a, Strand<'a>> for std::path::Components<'a> { + fn as_strand_view(self) -> Strand<'a> { Strand::Os(self.as_path().as_os_str()) } +} + +impl<'a> AsStrandView<'a, Strand<'a>> for PathDyn<'a> { + fn as_strand_view(self) -> Strand<'a> { + match self { + Self::Os(p) => Strand::Os(p.as_os_str()), + } + } +} + +impl<'a, T> AsStrandView<'a, Strand<'a>> for &'a T +where + T: AsStrandDyn, +{ + fn as_strand_view(self) -> Strand<'a> { self.as_strand_dyn() } +} diff --git a/yazi-shared/src/url/buf.rs b/yazi-shared/src/url/buf.rs index 1120a3d4..8f346907 100644 --- a/yazi-shared/src/url/buf.rs +++ b/yazi-shared/src/url/buf.rs @@ -1,14 +1,21 @@ -use std::{borrow::Cow, ffi::OsStr, fmt::{Debug, Formatter}, path::{Path, PathBuf}, str::FromStr}; +use std::{borrow::Cow, fmt::{Debug, Formatter}, path::{Path, PathBuf}, str::FromStr}; use anyhow::Result; use serde::{Deserialize, Serialize}; -use crate::{loc::{Loc, LocBuf}, path::PathDyn, pool::Pool, scheme::{Scheme, SchemeLike}, url::{AsUrl, Encode, EncodeTilded, Url, UrlCow}}; +use crate::{loc::LocBuf, path::{PathBufDyn, PathBufDynError, PathDyn, PathDynError, PathLike, SetNameError}, pool::{InternStr, Pool, Symbol}, scheme::SchemeKind, strand::AsStrandDyn, url::{AsUrl, Url, UrlCow, UrlLike}}; -#[derive(Clone, Default, Eq, Hash, PartialEq)] -pub struct UrlBuf { - pub loc: LocBuf, - pub scheme: Scheme, +#[derive(Clone, Eq, Hash, PartialEq)] +pub enum UrlBuf { + Regular(LocBuf), + Search { loc: LocBuf, domain: Symbol }, + Archive { loc: LocBuf, domain: Symbol }, + Sftp { loc: LocBuf, domain: Symbol }, +} + +// FIXME: remove +impl Default for UrlBuf { + fn default() -> Self { Self::Regular(Default::default()) } } impl From<&UrlBuf> for UrlBuf { @@ -16,15 +23,22 @@ impl From<&UrlBuf> for UrlBuf { } impl From> for UrlBuf { - fn from(url: Url<'_>) -> Self { Self { loc: url.loc.into(), scheme: url.scheme.into() } } + fn from(url: Url<'_>) -> Self { + match url { + Url::Regular(loc) => Self::Regular(loc.into()), + Url::Search { loc, domain } => Self::Search { loc: loc.into(), domain: domain.intern() }, + Url::Archive { loc, domain } => Self::Archive { loc: loc.into(), domain: domain.intern() }, + Url::Sftp { loc, domain } => Self::Sftp { loc: loc.into(), domain: domain.intern() }, + } + } } impl From<&Url<'_>> for UrlBuf { - fn from(url: &Url<'_>) -> Self { Self { loc: url.loc.into(), scheme: url.scheme.into() } } + fn from(url: &Url<'_>) -> Self { (*url).into() } } impl From for UrlBuf { - fn from(loc: LocBuf) -> Self { Self { loc, scheme: Scheme::Regular } } + fn from(loc: LocBuf) -> Self { Self::Regular(loc) } } impl From for UrlBuf { @@ -89,72 +103,95 @@ impl PartialEq> for &UrlBuf { impl UrlBuf { #[inline] pub fn new() -> &'static Self { - static U: UrlBuf = UrlBuf { loc: LocBuf::empty(), scheme: Scheme::Regular }; + static U: UrlBuf = UrlBuf::Regular(LocBuf::empty()); &U } #[inline] - pub fn into_path(self) -> Option { - Some(self.loc.into_path()).filter(|_| self.scheme.is_local()) + pub fn into_loc(self) -> PathBufDyn { + match self { + Self::Regular(loc) => loc.into_path().into(), + Self::Search { loc, .. } => loc.into_path().into(), + Self::Archive { loc, .. } => loc.into_path().into(), + Self::Sftp { loc, .. } => loc.into_path().into(), + } } #[inline] - pub fn set_name(&mut self, name: impl AsRef) { self.loc.set_name(name.as_ref()); } + pub fn into_local(self) -> Option { + if self.kind().is_local() { self.into_loc().into_os().ok() } else { None } + } - #[inline] - pub fn rebase(&self, base: &Path) -> Self { - Self { loc: self.loc.rebase(base), scheme: self.scheme.clone() } + pub fn try_set_name(&mut self, name: impl AsStrandDyn) -> Result<(), SetNameError> { + let name = name.as_strand_dyn(); + Ok(match self { + Self::Regular(loc) => loc.try_set_name(name.as_os()?)?, + Self::Search { loc, .. } => loc.try_set_name(name.as_os()?)?, + Self::Archive { loc, .. } => loc.try_set_name(name.as_os()?)?, + Self::Sftp { loc, .. } => loc.try_set_name(name.as_os()?)?, + }) + } + + pub fn rebase(&self, base: &Path) -> Result { + Ok(match self { + Self::Regular(loc) => Self::Regular(loc.rebase(base)?), + Self::Search { loc, domain } => { + Self::Search { loc: loc.rebase(base)?, domain: domain.clone() } + } + Self::Archive { loc, domain } => { + Self::Archive { loc: loc.rebase(base)?, domain: domain.clone() } + } + Self::Sftp { loc, domain } => { + Self::Sftp { loc: loc.rebase(base)?, domain: domain.clone() } + } + }) } } impl UrlBuf { - #[inline] - pub fn loc(&self) -> Loc<'_> { self.loc.as_loc() } - // --- Regular #[inline] pub fn is_regular(&self) -> bool { self.as_url().is_regular() } #[inline] - pub fn to_regular(&self) -> Self { self.as_url().into_regular().into() } + pub fn to_regular(&self) -> Result { Ok(self.as_url().as_regular()?.into()) } #[inline] - pub fn into_regular(mut self) -> Self { - self.loc = self.loc.into_path().into(); - self.scheme = Scheme::Regular; - self + pub fn into_regular(self) -> Result { + Ok(Self::Regular(self.into_loc().into_os()?.into())) } // --- Search #[inline] - pub fn is_search(&self) -> bool { matches!(self.scheme, Scheme::Search(_)) } + pub fn is_search(&self) -> bool { self.kind() == SchemeKind::Search } #[inline] - pub fn to_search(&self, domain: impl AsRef) -> Self { - Self { - loc: LocBuf::::zeroed(self.loc.to_path()), - scheme: Scheme::Search(Pool::::intern(domain)), - } + pub fn to_search(&self, domain: impl AsRef) -> Result { + Ok(Self::Search { + loc: LocBuf::::zeroed(self.loc().to_os_owned()?), + domain: Pool::::intern(domain), + }) } #[inline] - pub fn into_search(mut self, domain: impl AsRef) -> Self { - self.loc = LocBuf::::zeroed(self.loc.into_path()); - self.scheme = Scheme::Search(Pool::::intern(domain)); - self + pub fn into_search(self, domain: impl AsRef) -> Result { + Ok(Self::Search { + loc: LocBuf::::zeroed(self.into_loc().into_os()?), + domain: Pool::::intern(domain), + }) } // --- Archive #[inline] - pub fn is_archive(&self) -> bool { matches!(self.scheme, Scheme::Archive(_)) } + pub fn is_archive(&self) -> bool { self.kind() == SchemeKind::Archive } // --- Internal #[inline] pub fn is_internal(&self) -> bool { - match self.scheme { - Scheme::Regular | Scheme::Sftp(_) => true, - Scheme::Search(_) => !self.loc.uri().as_os_str().is_empty(), - Scheme::Archive(_) => false, + match self.kind() { + SchemeKind::Regular | SchemeKind::Sftp => true, + SchemeKind::Search => !self.uri().is_empty(), + SchemeKind::Archive => false, } } } @@ -165,12 +202,7 @@ impl Debug for UrlBuf { impl Serialize for UrlBuf { fn serialize(&self, serializer: S) -> Result { - let Self { scheme, loc } = self; - match (scheme.is_virtual(), loc.to_str()) { - (false, Some(s)) => serializer.serialize_str(s), - (true, Some(s)) => serializer.serialize_str(&format!("{}{s}", Encode::from(self))), - (_, None) => serializer.collect_str(&EncodeTilded::from(self)), - } + self.as_url().serialize(serializer) } } @@ -216,9 +248,12 @@ mod tests { for (base, path, expected) in cases { let base: UrlBuf = base.parse()?; #[cfg(unix)] - assert_eq!(format!("{:?}", base.join(path)), expected); + assert_eq!(format!("{:?}", base.try_join(path)?), expected); #[cfg(windows)] - assert_eq!(format!("{:?}", base.join(path)).replace(r"\", "/"), expected.replace(r"\", "/")); + assert_eq!( + format!("{:?}", base.try_join(path)?).replace(r"\", "/"), + expected.replace(r"\", "/") + ); } Ok(()) @@ -266,14 +301,14 @@ mod tests { let u: UrlBuf = "/root".parse()?; assert_eq!(format!("{u:?}"), "/root"); - let u = u.into_search("kw"); + let u = u.into_search("kw")?; assert_eq!(format!("{u:?}"), "search://kw//root"); assert_eq!(format!("{:?}", u.parent().unwrap()), "/"); - let u = u.join("examples"); + let u = u.try_join("examples")?; assert_eq!(format!("{u:?}"), format!("search://kw:1:1//root{S}examples")); - let u = u.join("README.md"); + let u = u.try_join("README.md")?; assert_eq!(format!("{u:?}"), format!("search://kw:2:2//root{S}examples{S}README.md")); let u = u.parent().unwrap(); diff --git a/yazi-shared/src/url/component.rs b/yazi-shared/src/url/component.rs index acc8f025..f7670adb 100644 --- a/yazi-shared/src/url/component.rs +++ b/yazi-shared/src/url/component.rs @@ -1,8 +1,10 @@ use std::{borrow::Cow, ffi::{OsStr, OsString}, iter::FusedIterator, ops::Not, path::{self, PathBuf, PrefixComponent}}; -use crate::{scheme::{Scheme, SchemeRef}, url::{Encode, Url, UrlBuf}}; +use anyhow::Result; -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +use crate::{path::{PathBufDyn, PathCow, PathLike}, scheme::SchemeRef, url::{Encode, Url, UrlBuf, UrlCow}}; + +#[derive(Clone, Copy, Debug, PartialEq)] pub enum Component<'a> { Scheme(SchemeRef<'a>), Prefix(PrefixComponent<'a>), @@ -24,12 +26,12 @@ impl<'a> From> for Component<'a> { } } -impl<'a> FromIterator> for UrlBuf { +impl<'a> FromIterator> for Result { fn from_iter>>(iter: I) -> Self { - let mut scheme = Scheme::Regular; let mut buf = PathBuf::new(); + let mut scheme = None; iter.into_iter().for_each(|c| match c { - Component::Scheme(s) => scheme = s.into(), + Component::Scheme(s) => scheme = Some(s), Component::Prefix(p) => buf.push(path::Component::Prefix(p)), Component::RootDir => buf.push(path::Component::RootDir), Component::CurDir => buf.push(path::Component::CurDir), @@ -37,7 +39,11 @@ impl<'a> FromIterator> for UrlBuf { Component::Normal(s) => buf.push(path::Component::Normal(s)), }); - Self { loc: buf.into(), scheme } + Ok(if let Some(s) = scheme { + UrlCow::try_from((s, PathCow::Owned(PathBufDyn::Os(buf))))?.into_owned() + } else { + buf.into() + }) } } @@ -66,18 +72,14 @@ pub struct Components<'a> { impl<'a> From> for Components<'a> { fn from(value: Url<'a>) -> Self { - Self { - inner: value.loc.as_path().components(), - url: value, - scheme_yielded: false, - } + Self { inner: value.loc().components(), url: value, scheme_yielded: false } } } impl<'a> Components<'a> { pub fn os_str(&self) -> Cow<'a, OsStr> { let path = self.inner.as_path(); - if self.url.scheme.is_local() || self.scheme_yielded { + if self.url.kind().is_local() || self.scheme_yielded { return path.as_os_str().into(); } @@ -90,7 +92,7 @@ impl<'a> Components<'a> { pub fn covariant(&self, other: &Self) -> bool { match (self.scheme_yielded, other.scheme_yielded) { (false, false) => {} - (true, true) if self.url.scheme.covariant(other.url.scheme) => {} + (true, true) if self.url.scheme().covariant(other.url.scheme()) => {} _ => return false, } self.inner == other.inner @@ -103,7 +105,7 @@ impl<'a> Iterator for Components<'a> { fn next(&mut self) -> Option { if !self.scheme_yielded { self.scheme_yielded = true; - Some(Component::Scheme(self.url.scheme)) + Some(Component::Scheme(self.url.scheme())) } else { self.inner.next().map(Into::into) } @@ -123,7 +125,7 @@ impl<'a> DoubleEndedIterator for Components<'a> { Some(comp.into()) } else if !self.scheme_yielded { self.scheme_yielded = true; - Some(Component::Scheme(self.url.scheme)) + Some(Component::Scheme(self.url.scheme())) } else { None } @@ -134,8 +136,8 @@ impl<'a> FusedIterator for Components<'a> {} impl<'a> PartialEq for Components<'a> { fn eq(&self, other: &Self) -> bool { - Some(self.url.scheme).filter(|_| !self.scheme_yielded) - == Some(other.url.scheme).filter(|_| !other.scheme_yielded) + Some(self.url.scheme()).filter(|_| !self.scheme_yielded) + == Some(other.url.scheme()).filter(|_| !other.scheme_yielded) && self.inner == other.inner } } @@ -145,31 +147,35 @@ impl<'a> PartialEq for Components<'a> { mod tests { use std::path::Path; + use anyhow::Result; + use super::*; - use crate::{pool::InternStr, url::UrlLike}; + use crate::url::UrlLike; #[test] - fn test_collect() { + fn test_collect() -> Result<()> { crate::init_tests(); - let search: UrlBuf = "search://keyword//root/projects/yazi".parse().unwrap(); - assert_eq!(search.loc.uri().as_os_str(), OsStr::new("")); - assert_eq!(search.scheme, Scheme::Search("keyword".intern())); + let search: UrlBuf = "search://keyword//root/projects/yazi".parse()?; + assert_eq!(search.uri(), ""); + assert_eq!(search.scheme(), SchemeRef::Search { domain: "keyword", uri: 0, urn: 0 }); - let item = search.join("main.rs"); - assert_eq!(item.loc.uri().as_os_str(), OsStr::new("main.rs")); - assert_eq!(item.scheme, Scheme::Search("keyword".intern())); + let item = search.try_join("main.rs")?; + assert_eq!(item.uri(), "main.rs"); + assert_eq!(item.scheme(), SchemeRef::Search { domain: "keyword", uri: 1, urn: 1 }); - let u: UrlBuf = item.components().take(4).collect(); - assert_eq!(u.scheme, Scheme::Search("keyword".intern())); - assert_eq!(u.loc.as_path(), Path::new("/root/projects")); + let u: UrlBuf = item.components().take(4).collect::>()?; + assert_eq!(u.scheme(), SchemeRef::Search { domain: "keyword", uri: 0, urn: 0 }); + assert_eq!(u.loc(), Path::new("/root/projects")); let u: UrlBuf = item .components() .take(5) .chain([Component::Normal(OsStr::new("target/release/yazi"))]) - .collect(); - assert_eq!(u.scheme, Scheme::Search("keyword".intern())); - assert_eq!(u.loc.as_path(), Path::new("/root/projects/yazi/target/release/yazi")); + .collect::>()?; + assert_eq!(u.scheme(), SchemeRef::Search { domain: "keyword", uri: 0, urn: 0 }); + assert_eq!(u.loc(), Path::new("/root/projects/yazi/target/release/yazi")); + + Ok(()) } } diff --git a/yazi-shared/src/url/cov.rs b/yazi-shared/src/url/cov.rs index d0160ede..f9439f85 100644 --- a/yazi-shared/src/url/cov.rs +++ b/yazi-shared/src/url/cov.rs @@ -24,9 +24,9 @@ impl PartialEq for UrlCov<'_> { impl Hash for UrlCov<'_> { fn hash(&self, state: &mut H) { - self.0.loc.hash(state); - if self.0.scheme.is_virtual() { - self.0.scheme.hash(state); + self.0.loc().hash(state); + if self.0.kind().is_virtual() { + self.0.scheme().hash(state); } } } diff --git a/yazi-shared/src/url/cow.rs b/yazi-shared/src/url/cow.rs index d18e6199..99fbadad 100644 --- a/yazi-shared/src/url/cow.rs +++ b/yazi-shared/src/url/cow.rs @@ -1,22 +1,37 @@ -use std::{borrow::Cow, path::{Path, PathBuf}}; +use std::{borrow::Cow, hash::{Hash, Hasher}, path::PathBuf}; -use anyhow::Result; -use percent_encoding::percent_decode; +use anyhow::{Result, anyhow}; +use serde::{Deserialize, Deserializer, Serialize}; -use crate::{IntoOsStr, loc::{Loc, LocBuf}, scheme::{AsScheme, SchemeCow, SchemeRef}, url::{AsUrl, Url, UrlBuf}}; +use crate::{loc::{Loc, LocBuf}, path::{PathBufDyn, PathCow, PathDyn}, pool::SymbolCow, scheme::{AsScheme, Scheme, SchemeCow, SchemeKind, SchemeRef}, url::{AsUrl, Url, UrlBuf}}; #[derive(Clone, Debug)] pub enum UrlCow<'a> { - Borrowed { loc: Loc<'a>, scheme: SchemeCow<'a> }, - Owned { loc: LocBuf, scheme: SchemeCow<'a> }, + Regular(LocBuf), + Search { loc: LocBuf, domain: SymbolCow<'a, str> }, + Archive { loc: LocBuf, domain: SymbolCow<'a, str> }, + Sftp { loc: LocBuf, domain: SymbolCow<'a, str> }, + + RegularRef(Loc<'a>), + SearchRef { loc: Loc<'a>, domain: SymbolCow<'a, str> }, + ArchiveRef { loc: Loc<'a>, domain: SymbolCow<'a, str> }, + SftpRef { loc: Loc<'a>, domain: SymbolCow<'a, str> }, } +// FIXME: remove impl Default for UrlCow<'_> { - fn default() -> Self { Self::Borrowed { loc: Default::default(), scheme: Default::default() } } + fn default() -> Self { Self::RegularRef(Default::default()) } } impl<'a> From> for UrlCow<'a> { - fn from(value: Url<'a>) -> Self { Self::Borrowed { loc: value.loc, scheme: value.scheme.into() } } + fn from(value: Url<'a>) -> Self { + match value { + Url::Regular(loc) => Self::RegularRef(loc), + Url::Search { loc, domain } => Self::SearchRef { loc, domain: domain.into() }, + Url::Archive { loc, domain } => Self::ArchiveRef { loc, domain: domain.into() }, + Url::Sftp { loc, domain } => Self::SftpRef { loc, domain: domain.into() }, + } + } } impl<'a, T> From<&'a T> for UrlCow<'a> @@ -27,7 +42,14 @@ where } impl From for UrlCow<'_> { - fn from(value: UrlBuf) -> Self { Self::Owned { loc: value.loc, scheme: value.scheme.into() } } + fn from(value: UrlBuf) -> Self { + match value { + UrlBuf::Regular(loc) => Self::Regular(loc), + UrlBuf::Search { loc, domain } => Self::Search { loc, domain: domain.into() }, + UrlBuf::Archive { loc, domain } => Self::Archive { loc, domain: domain.into() }, + UrlBuf::Sftp { loc, domain } => Self::Sftp { loc, domain: domain.into() }, + } + } } impl From for UrlCow<'_> { @@ -45,20 +67,7 @@ impl From<&UrlCow<'_>> for UrlBuf { impl<'a> TryFrom<&'a [u8]> for UrlCow<'a> { type Error = anyhow::Error; - fn try_from(value: &'a [u8]) -> Result { - let (scheme, path, port) = Self::parse(value)?; - - Ok(match (path, port) { - (Cow::Borrowed(p), None) => Self::Borrowed { loc: Loc::bare(p), scheme }, - (Cow::Borrowed(p), Some((uri, urn))) => { - Self::Borrowed { loc: Loc::with(p, uri, urn)?, scheme } - } - (Cow::Owned(p), None) => Self::Owned { loc: LocBuf::from(p), scheme }, - (Cow::Owned(p), Some((uri, urn))) => { - Self::Owned { loc: LocBuf::::with(p, uri, urn)?, scheme } - } - }) - } + fn try_from(value: &'a [u8]) -> Result { SchemeCow::parse(value)?.try_into() } } impl TryFrom> for UrlCow<'_> { @@ -94,69 +103,171 @@ impl<'a> TryFrom> for UrlCow<'a> { } } +impl<'a> TryFrom<(SchemeRef<'a>, PathCow<'a>)> for UrlCow<'a> { + type Error = anyhow::Error; + + fn try_from((scheme, path): (SchemeRef<'a>, PathCow<'a>)) -> Result { + (SchemeCow::Borrowed(scheme), path).try_into() + } +} + +impl TryFrom<(Scheme, PathBufDyn)> for UrlCow<'_> { + type Error = anyhow::Error; + + fn try_from((scheme, path): (Scheme, PathBufDyn)) -> Result { + (SchemeCow::Owned(scheme), path).try_into() + } +} + +impl<'a> TryFrom<(SchemeCow<'a>, PathCow<'a>)> for UrlCow<'a> { + type Error = anyhow::Error; + + fn try_from((scheme, path): (SchemeCow<'a>, PathCow<'a>)) -> Result { + match path { + PathCow::Borrowed(path) => (scheme, path).try_into(), + PathCow::Owned(path) => (scheme, path).try_into(), + } + } +} + +impl<'a> TryFrom<(SchemeCow<'a>, PathDyn<'a>)> for UrlCow<'a> { + type Error = anyhow::Error; + + fn try_from((scheme, path): (SchemeCow<'a>, PathDyn<'a>)) -> Result { + let kind = scheme.as_scheme().kind(); + let (uri, urn) = scheme.as_scheme().ports(); + let domain = scheme.into_domain(); + Ok(match kind { + SchemeKind::Regular => Self::RegularRef(Loc::bare(path.as_os()?)), + SchemeKind::Search => Self::SearchRef { + loc: Loc::with(path.as_os()?, uri, urn)?, + domain: domain.ok_or_else(|| anyhow!("missing domain for search scheme"))?, + }, + SchemeKind::Archive => Self::ArchiveRef { + loc: Loc::with(path.as_os()?, uri, urn)?, + domain: domain.ok_or_else(|| anyhow!("missing domain for archive scheme"))?, + }, + SchemeKind::Sftp => Self::SftpRef { + loc: Loc::with(path.as_os()?, uri, urn)?, + domain: domain.ok_or_else(|| anyhow!("missing domain for sftp scheme"))?, + }, + }) + } +} + +impl<'a> TryFrom<(SchemeCow<'a>, PathBufDyn)> for UrlCow<'a> { + type Error = anyhow::Error; + + fn try_from((scheme, path): (SchemeCow<'a>, PathBufDyn)) -> Result { + let kind = scheme.as_scheme().kind(); + let (uri, urn) = scheme.as_scheme().ports(); + let domain = scheme.into_domain(); + Ok(match kind { + SchemeKind::Regular => Self::Regular(path.into_os()?.into()), + SchemeKind::Search => Self::Search { + loc: LocBuf::::with(path.try_into()?, uri, urn)?, + domain: domain.ok_or_else(|| anyhow!("missing domain for search scheme"))?, + }, + SchemeKind::Archive => Self::Archive { + loc: LocBuf::::with(path.try_into()?, uri, urn)?, + domain: domain.ok_or_else(|| anyhow!("missing domain for archive scheme"))?, + }, + SchemeKind::Sftp => Self::Sftp { + loc: LocBuf::::with(path.try_into()?, uri, urn)?, + domain: domain.ok_or_else(|| anyhow!("missing domain for sftp scheme"))?, + }, + }) + } +} + // --- Eq +impl PartialEq for UrlCow<'_> { + fn eq(&self, other: &Self) -> bool { self.as_url() == other.as_url() } +} + impl PartialEq for UrlCow<'_> { fn eq(&self, other: &UrlBuf) -> bool { self.as_url() == other.as_url() } } +impl Eq for UrlCow<'_> {} + +// --- Hash +impl Hash for UrlCow<'_> { + fn hash(&self, state: &mut H) { self.as_url().hash(state); } +} + impl<'a> UrlCow<'a> { - #[inline] - pub fn loc(&self) -> Loc<'_> { + pub fn is_owned(&self) -> bool { match self { - UrlCow::Borrowed { loc, .. } => *loc, - UrlCow::Owned { loc, .. } => loc.as_loc(), + Self::Regular(_) | Self::Search { .. } | Self::Archive { .. } | Self::Sftp { .. } => true, + Self::RegularRef(_) + | Self::SearchRef { .. } + | Self::ArchiveRef { .. } + | Self::SftpRef { .. } => false, } } - #[inline] - pub fn scheme(&self) -> SchemeRef<'_> { - match self { - UrlCow::Borrowed { scheme, .. } => scheme.as_scheme(), - UrlCow::Owned { scheme, .. } => scheme.as_scheme(), - } - } - - #[inline] pub fn into_owned(self) -> UrlBuf { match self { - UrlCow::Borrowed { loc, scheme } => UrlBuf { loc: loc.into(), scheme: scheme.into() }, - UrlCow::Owned { loc, scheme } => UrlBuf { loc, scheme: scheme.into() }, + Self::Regular(loc) => UrlBuf::Regular(loc), + Self::Search { loc, domain } => UrlBuf::Search { loc, domain: domain.into() }, + Self::Archive { loc, domain } => UrlBuf::Archive { loc, domain: domain.into() }, + Self::Sftp { loc, domain } => UrlBuf::Sftp { loc, domain: domain.into() }, + + Self::RegularRef(loc) => UrlBuf::Regular(loc.into()), + Self::SearchRef { loc, domain } => { + UrlBuf::Search { loc: loc.into(), domain: domain.into() } + } + Self::ArchiveRef { loc, domain } => { + UrlBuf::Archive { loc: loc.into(), domain: domain.into() } + } + Self::SftpRef { loc, domain } => UrlBuf::Sftp { loc: loc.into(), domain: domain.into() }, } } - #[inline] pub fn into_scheme(self) -> SchemeCow<'a> { + let (uri, urn) = self.as_url().scheme().ports(); match self { - UrlCow::Borrowed { scheme, .. } => scheme, - UrlCow::Owned { scheme, .. } => scheme, + Self::Regular(_) => Scheme::Regular { uri, urn }.into(), + Self::RegularRef(_) => SchemeRef::Regular { uri, urn }.into(), + Self::Search { domain, .. } | Self::SearchRef { domain, .. } => match domain { + SymbolCow::Borrowed(domain) => SchemeRef::Search { domain, uri, urn }.into(), + SymbolCow::Owned(domain) => Scheme::Search { domain, uri, urn }.into(), + }, + Self::Archive { domain, .. } | Self::ArchiveRef { domain, .. } => match domain { + SymbolCow::Borrowed(domain) => SchemeRef::Archive { domain, uri, urn }.into(), + SymbolCow::Owned(domain) => Scheme::Archive { domain, uri, urn }.into(), + }, + Self::Sftp { domain, .. } | Self::SftpRef { domain, .. } => match domain { + SymbolCow::Borrowed(domain) => SchemeRef::Sftp { domain, uri, urn }.into(), + SymbolCow::Owned(domain) => Scheme::Sftp { domain, uri, urn }.into(), + }, } } #[inline] pub fn to_owned(&self) -> UrlBuf { self.as_url().into() } - - pub fn parse(bytes: &[u8]) -> Result<(SchemeCow<'_>, Cow<'_, Path>, Option<(usize, usize)>)> { - let mut skip = 0; - let (scheme, tilde, uri, urn) = SchemeCow::parse(bytes, &mut skip)?; - - let rest = if tilde { - Cow::from(percent_decode(&bytes[skip..])).into_os_str()? - } else { - bytes[skip..].into_os_str()? - }; - - let path: Cow<_> = match rest { - Cow::Borrowed(s) => Path::new(s).into(), - Cow::Owned(s) => PathBuf::from(s).into(), - }; - - let ports = scheme.normalize_ports(uri, urn, &path)?; - - Ok((scheme, path, ports)) - } } impl UrlCow<'_> { #[inline] pub fn is_regular(&self) -> bool { self.as_url().is_regular() } } + +impl Serialize for UrlCow<'_> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.as_url().serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for UrlCow<'_> { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + UrlBuf::deserialize(deserializer).map(UrlCow::from) + } +} diff --git a/yazi-shared/src/url/display.rs b/yazi-shared/src/url/display.rs index b9768530..020402a4 100644 --- a/yazi-shared/src/url/display.rs +++ b/yazi-shared/src/url/display.rs @@ -1,4 +1,4 @@ -use crate::url::{Encode, Url}; +use crate::{path::PathLike, scheme::Encode, url::Url}; pub struct Display<'a> { inner: Url<'a>, @@ -10,8 +10,8 @@ impl<'a> From> for Display<'a> { impl<'a> std::fmt::Display for Display<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let Url { loc, scheme } = self.inner; - if scheme.is_virtual() { + let (kind, loc) = (self.inner.kind(), self.inner.loc()); + if kind.is_virtual() { Encode(self.inner).fmt(f)?; } loc.display().fmt(f) diff --git a/yazi-shared/src/url/encode.rs b/yazi-shared/src/url/encode.rs index 3e578404..a857ea40 100644 --- a/yazi-shared/src/url/encode.rs +++ b/yazi-shared/src/url/encode.rs @@ -1,93 +1,29 @@ -use std::{fmt::{self, Display}, ops::Not}; +use std::fmt::{self, Display}; -use percent_encoding::{AsciiSet, CONTROLS, PercentEncode, percent_encode}; +use percent_encoding::{CONTROLS, percent_encode}; -use crate::{scheme::SchemeRef, url::{AsUrl, Url, UrlBuf}}; - -#[derive(Clone, Copy)] -pub struct Encode<'a>(pub Url<'a>); - -impl<'a> From<&'a UrlBuf> for Encode<'a> { - fn from(value: &'a UrlBuf) -> Self { Self(value.as_url()) } -} - -impl<'a> Encode<'a> { - #[inline] - pub fn domain<'s>(s: &'s str) -> PercentEncode<'s> { - const SET: &AsciiSet = &CONTROLS.add(b'/').add(b':'); - percent_encode(s.as_bytes(), SET) - } - - fn ports(self) -> impl Display { - struct D<'a>(Encode<'a>); - - impl Display for D<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - macro_rules! w { - ($default_uri:expr, $default_urn:expr) => {{ - let uri = self.0.0.loc.uri().components().count(); - let urn = self.0.0.loc.urn().components().count(); - match (uri != $default_uri, urn != $default_urn) { - (true, true) => write!(f, ":{uri}:{urn}"), - (true, false) => write!(f, ":{uri}"), - (false, true) => write!(f, "::{urn}"), - (false, false) => Ok(()), - } - }}; - } - - match self.0.0.scheme { - SchemeRef::Regular => Ok(()), - SchemeRef::Search(_) | SchemeRef::Archive(_) => w!(0, 0), - SchemeRef::Sftp(_) => w!( - self.0.0.loc.as_os_str().is_empty().not() as usize, - self.0.0.loc.file_name().is_some() as usize - ), - } - } - } - - D(self) - } -} - -impl Display for Encode<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use SchemeRef as S; - match self.0.scheme { - S::Regular => write!(f, "regular://"), - S::Search(d) => write!(f, "search://{}{}/", Self::domain(d), self.ports()), - S::Archive(d) => write!(f, "archive://{}{}/", Self::domain(d), self.ports()), - S::Sftp(d) => write!(f, "sftp://{}{}/", Self::domain(d), self.ports()), - } - } -} +use crate::{path::PathLike, url::Url}; // --- Tilded #[derive(Clone, Copy)] -pub struct EncodeTilded<'a>(pub Url<'a>); +pub struct Encode<'a>(pub Url<'a>); -impl<'a> From<&'a UrlBuf> for EncodeTilded<'a> { - fn from(value: &'a UrlBuf) -> Self { Self(value.as_url()) } -} - -impl<'a> From> for Encode<'a> { - fn from(value: EncodeTilded<'a>) -> Self { Self(value.0) } -} - -impl Display for EncodeTilded<'_> { +impl Display for Encode<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use Encode as E; - use SchemeRef as S; + use crate::scheme::Encode as E; - let loc = percent_encode(self.0.loc.as_os_str().as_encoded_bytes(), CONTROLS); - match self.0.scheme { - S::Regular => write!(f, "regular~://{loc}"), - S::Search(d) => write!(f, "search~://{}{}/{loc}", E::domain(d), E::ports((*self).into())), - S::Archive(d) => { - write!(f, "archive~://{}{}/{loc}", E::domain(d), E::ports((*self).into())) + let loc = percent_encode(self.0.loc().encoded_bytes(), CONTROLS); + match self.0 { + Url::Regular(_) => write!(f, "regular~://{loc}"), + Url::Search { domain, .. } => { + write!(f, "search~://{}{}/{loc}", E::domain(domain), E::ports((*self).into())) + } + Url::Archive { domain, .. } => { + write!(f, "archive~://{}{}/{loc}", E::domain(domain), E::ports((*self).into())) + } + Url::Sftp { domain, .. } => { + write!(f, "sftp~://{}{}/{loc}", E::domain(domain), E::ports((*self).into())) } - S::Sftp(d) => write!(f, "sftp~://{}{}/{loc}", E::domain(d), E::ports((*self).into())), } } } diff --git a/yazi-shared/src/url/traits.rs b/yazi-shared/src/url/traits.rs index 49fc2950..b8b8aacc 100644 --- a/yazi-shared/src/url/traits.rs +++ b/yazi-shared/src/url/traits.rs @@ -1,6 +1,8 @@ use std::{borrow::Cow, ffi::OsStr, path::{Path, PathBuf}}; -use crate::{loc::Loc, path::{AsPathDyn, PathDyn}, scheme::{AsScheme, SchemeRef}, url::{Components, Display, Url, UrlBuf, UrlCow}}; +use anyhow::Result; + +use crate::{loc::Loc, path::{AsPathRef, EndsWithError, JoinError, PathDyn, StartsWithError, StripPrefixError}, scheme::{SchemeKind, SchemeRef}, strand::{AsStrandDyn, Strand}, url::{Components, Display, Url, UrlBuf, UrlCow}}; // --- AsUrl pub trait AsUrl { @@ -9,7 +11,7 @@ pub trait AsUrl { impl AsUrl for Path { #[inline] - fn as_url(&self) -> Url<'_> { Url { loc: Loc::bare(self), scheme: SchemeRef::Regular } } + fn as_url(&self) -> Url<'_> { Url::Regular(Loc::bare(self)) } } impl AsUrl for &Path { @@ -43,7 +45,14 @@ impl AsUrl for Url<'_> { impl AsUrl for UrlBuf { #[inline] - fn as_url(&self) -> Url<'_> { Url { loc: self.loc.as_loc(), scheme: self.scheme.as_scheme() } } + fn as_url(&self) -> Url<'_> { + match self { + Self::Regular(loc) => Url::Regular(loc.as_loc()), + Self::Search { loc, domain } => Url::Search { loc: loc.as_loc(), domain }, + Self::Archive { loc, domain } => Url::Archive { loc: loc.as_loc(), domain }, + Self::Sftp { loc, domain } => Url::Sftp { loc: loc.as_loc(), domain }, + } + } } impl AsUrl for &UrlBuf { @@ -57,11 +66,17 @@ impl AsUrl for &mut UrlBuf { } impl AsUrl for UrlCow<'_> { - #[inline] fn as_url(&self) -> Url<'_> { match self { - UrlCow::Borrowed { loc, scheme } => Url { loc: *loc, scheme: scheme.as_scheme() }, - UrlCow::Owned { loc, scheme } => Url { loc: loc.as_loc(), scheme: scheme.as_scheme() }, + Self::Regular(loc) => Url::Regular(loc.as_loc()), + Self::Search { loc, domain } => Url::Search { loc: loc.as_loc(), domain }, + Self::Archive { loc, domain } => Url::Archive { loc: loc.as_loc(), domain }, + Self::Sftp { loc, domain } => Url::Sftp { loc: loc.as_loc(), domain }, + + Self::RegularRef(loc) => Url::Regular(*loc), + Self::SearchRef { loc, domain } => Url::Search { loc: *loc, domain }, + Self::ArchiveRef { loc, domain } => Url::Archive { loc: *loc, domain }, + Self::SftpRef { loc, domain } => Url::Sftp { loc: *loc, domain }, } } } @@ -90,9 +105,9 @@ pub trait UrlLike where Self: AsUrl + Sized, { - fn as_path(&self) -> Option<&Path> { self.as_url().as_path() } + fn as_local(&self) -> Option<&Path> { self.as_url().as_local() } - fn base(&self) -> Option> { self.as_url().base() } + fn base(&self) -> Url<'_> { self.as_url().base() } fn components(&self) -> Components<'_> { self.as_url().into() } @@ -100,9 +115,9 @@ where fn display(&self) -> Display<'_> { self.as_url().into() } - fn ends_with(&self, child: impl AsUrl) -> bool { self.as_url().ends_with(child) } + fn ext(&self) -> Option> { self.as_url().ext() } - fn ext(&self) -> Option<&OsStr> { self.as_url().ext() } + fn has_base(&self) -> bool { self.as_url().has_base() } fn has_root(&self) -> bool { self.as_url().has_root() } @@ -110,9 +125,11 @@ where fn is_absolute(&self) -> bool { self.as_url().is_absolute() } - fn join(&self, path: impl AsPathDyn) -> UrlBuf { self.as_url().join(path) } + fn kind(&self) -> SchemeKind { self.as_url().kind() } - fn name(&self) -> Option<&OsStr> { self.as_url().name() } + fn loc(&self) -> PathDyn<'_> { self.as_url().loc() } + + fn name(&self) -> Option> { self.as_url().name() } fn os_str(&self) -> Cow<'_, OsStr> { self.components().os_str() } @@ -120,12 +137,30 @@ where fn parent(&self) -> Option> { self.as_url().parent() } - fn starts_with(&self, base: impl AsUrl) -> bool { self.as_url().starts_with(base) } + fn scheme(&self) -> SchemeRef<'_> { self.as_url().scheme() } - fn stem(&self) -> Option<&OsStr> { self.as_url().stem() } + fn stem(&self) -> Option> { self.as_url().stem() } - fn strip_prefix(&self, base: impl AsUrl) -> Option> { - self.as_url().strip_prefix(base) + fn trail(&self) -> Url<'_> { self.as_url().trail() } + + fn try_ends_with(&self, child: impl AsUrl) -> Result { + self.as_url().try_ends_with(child) + } + + fn try_join(&self, path: impl AsStrandDyn) -> Result { + self.as_url().try_join(path) + } + + fn try_replace<'a>(&self, take: usize, path: impl AsPathRef<'a>) -> Result> { + self.as_url().try_replace(take, path) + } + + fn try_starts_with(&self, base: impl AsUrl) -> Result { + self.as_url().try_starts_with(base) + } + + fn try_strip_prefix(&self, base: impl AsUrl) -> Result, StripPrefixError> { + self.as_url().try_strip_prefix(base) } fn uri(&self) -> PathDyn<'_> { self.as_url().uri() } diff --git a/yazi-shared/src/url/url.rs b/yazi-shared/src/url/url.rs index 9c3e38a9..71f67e84 100644 --- a/yazi-shared/src/url/url.rs +++ b/yazi-shared/src/url/url.rs @@ -1,13 +1,18 @@ use std::{borrow::Cow, ffi::OsStr, fmt::{Debug, Formatter}, path::{Path, PathBuf}}; +use anyhow::Result; use hashbrown::Equivalent; +use serde::Serialize; -use crate::{loc::{Loc, LocBuf}, path::{AsPathDyn, PathDyn, PathLike}, scheme::SchemeRef, url::{AsUrl, Components, Encode, UrlBuf}}; +use super::Encode as EncodeUrl; +use crate::{loc::{Loc, LocBuf}, path::{AsPathDyn, AsPathRef, EndsWithError, JoinError, PathBufDyn, PathBufLike, PathDyn, PathDynError, PathLike, StartsWithError, StripPrefixError}, pool::InternStr, scheme::{Encode as EncodeScheme, SchemeCow, SchemeKind, SchemeRef}, strand::{AsStrandDyn, Strand}, url::{AsUrl, Components, UrlBuf, UrlCow}}; #[derive(Clone, Copy, Eq, Hash, PartialEq)] -pub struct Url<'a> { - pub loc: Loc<'a>, - pub scheme: SchemeRef<'a>, +pub enum Url<'a> { + Regular(Loc<'a>), + Search { loc: Loc<'a>, domain: &'a str }, + Archive { loc: Loc<'a>, domain: &'a str }, + Sftp { loc: Loc<'a>, domain: &'a str }, } // --- Eq @@ -23,170 +28,315 @@ impl Equivalent for Url<'_> { // --- Debug impl Debug for Url<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - if self.scheme == SchemeRef::Regular { - write!(f, "{}", self.loc.display()) + if self.is_regular() { + write!(f, "{}", self.loc().display()) } else { - write!(f, "{}{}", Encode(*self), self.loc.display()) + write!(f, "{}{}", EncodeScheme(*self), self.loc().display()) + } + } +} + +impl Serialize for Url<'_> { + fn serialize(&self, serializer: S) -> Result { + let (kind, loc) = (self.kind(), self.loc()); + match (kind.is_virtual(), loc.to_str()) { + (false, Ok(s)) => serializer.serialize_str(s), + (true, Ok(s)) => serializer.serialize_str(&format!("{}{s}", EncodeScheme(*self))), + (_, Err(_)) => serializer.collect_str(&EncodeUrl(*self)), } } } impl<'a> Url<'a> { #[inline] - pub fn regular + ?Sized>(path: &'a T) -> Self { - Self { loc: Loc::bare(path.as_ref()), scheme: SchemeRef::Regular } + pub fn kind(&self) -> SchemeKind { + match self { + Self::Regular(_) => SchemeKind::Regular, + Self::Search { .. } => SchemeKind::Search, + Self::Archive { .. } => SchemeKind::Archive, + Self::Sftp { .. } => SchemeKind::Sftp, + } } + // FIXME: add to UrlLike trait #[inline] - pub fn is_regular(self) -> bool { self.scheme == SchemeRef::Regular } - - #[inline] - pub fn into_regular(self) -> Self { - Self { loc: Loc::bare(self.loc.as_path()), scheme: SchemeRef::Regular } - } - - #[inline] - pub fn is_search(self) -> bool { matches!(self.scheme, SchemeRef::Search(_)) } - - #[inline] - pub fn is_absolute(self) -> bool { - use SchemeRef as S; - - match self.scheme { - S::Regular | S::Search(_) => self.loc.is_absolute(), - S::Archive(_) | S::Sftp(_) => self.loc.has_root(), + pub fn loc(self) -> PathDyn<'a> { + match self { + Self::Regular(loc) => loc.as_path_dyn(), + Self::Search { loc, .. } => loc.as_path_dyn(), + Self::Archive { loc, .. } => loc.as_path_dyn(), + Self::Sftp { loc, .. } => loc.as_path_dyn(), } } #[inline] - pub fn has_root(self) -> bool { self.loc.has_root() } + pub fn scheme(self) -> SchemeRef<'a> { + let (uri, urn) = SchemeCow::retrieve_ports(self); + match self { + Self::Regular(_) => SchemeRef::Regular { uri, urn }, + Self::Search { domain, .. } => SchemeRef::Search { domain, uri, urn }, + Self::Archive { domain, .. } => SchemeRef::Archive { domain, uri, urn }, + Self::Sftp { domain, .. } => SchemeRef::Sftp { domain, uri, urn }, + } + } + + #[inline] + pub fn regular + ?Sized>(path: &'a T) -> Self { + Self::Regular(Loc::bare(path.as_ref())) + } + + #[inline] + pub fn is_regular(self) -> bool { matches!(self, Self::Regular(_)) } + + #[inline] + pub fn as_regular(self) -> Result { + Ok(Self::Regular(Loc::bare(self.loc().as_os()?))) + } + + #[inline] + pub fn is_search(self) -> bool { matches!(self, Self::Search { .. }) } + + #[inline] + pub fn is_absolute(self) -> bool { self.loc().is_absolute() } + + #[inline] + pub fn has_root(self) -> bool { self.loc().has_root() } #[inline] pub fn to_owned(self) -> UrlBuf { self.into() } - pub fn join(self, path: impl AsPathDyn) -> UrlBuf { - use SchemeRef as S; + pub fn try_join(self, path: impl AsStrandDyn) -> Result { + let joined = self.loc().try_join(&path)?; - let joined = match path.as_path_dyn() { - PathDyn::Os(p) => self.loc.join(p), - }; - - let loc = match self.scheme { - S::Regular => joined.into(), - S::Search(_) => LocBuf::::new(joined, self.loc.base(), self.loc.base()), - S::Archive(_) => LocBuf::::floated(joined, self.loc.base()), - S::Sftp(_) => joined.into(), - }; - - UrlBuf { loc, scheme: self.scheme.into() } + Ok(match self { + Self::Regular(_) => UrlBuf::Regular(joined.into_os()?.into()), + Self::Search { loc, domain } => UrlBuf::Search { + loc: LocBuf::::new(joined.try_into()?, loc.base(), loc.base()), + domain: domain.intern(), + }, + Self::Archive { loc, domain } => UrlBuf::Archive { + loc: LocBuf::::floated(joined.try_into()?, loc.base()), + domain: domain.intern(), + }, + Self::Sftp { domain, .. } => { + UrlBuf::Sftp { loc: joined.into_os()?.into(), domain: domain.intern() } + } + }) } - pub fn strip_prefix(self, base: impl AsUrl) -> Option> { - use SchemeRef as S; + pub fn try_replace<'b>(self, take: usize, to: impl AsPathRef<'b>) -> Result> { + self.try_replace_impl(take, to.as_path_ref()) + } + + fn try_replace_impl<'b>(self, take: usize, rep: PathDyn<'b>) -> Result> { + let b = rep.encoded_bytes(); + if take == 0 { + return UrlCow::try_from(b); + } else if SchemeKind::parse(b)?.is_some() { + return UrlCow::try_from(b); + } + + let mut path = PathBufDyn::Os(self.loc().components().take(take - 1).collect()); // FIXME + path.try_push(rep)?; + + let url = match self { + Self::Regular(_) => UrlBuf::from(path.into_os()?), + + Self::Search { loc, domain } if path.try_starts_with(loc.trail())? => UrlBuf::Search { + loc: LocBuf::::new(path.into_os()?, loc.base(), loc.trail()), + domain: domain.intern(), + }, + Self::Archive { loc, domain } if path.try_starts_with(loc.trail())? => UrlBuf::Archive { + loc: LocBuf::::new(path.into_os()?, loc.base(), loc.trail()), + domain: domain.intern(), + }, + Self::Sftp { loc, domain } if path.try_starts_with(loc.trail())? => UrlBuf::Sftp { + loc: LocBuf::::new(path.into_os()?, loc.base(), loc.trail()), + domain: domain.intern(), + }, + + Self::Search { domain, .. } => UrlBuf::Search { + loc: LocBuf::::saturated(path.into_os()?, self.kind()), + domain: domain.intern(), + }, + Self::Archive { domain, .. } => UrlBuf::Archive { + loc: LocBuf::::saturated(path.into_os()?, self.kind()), + domain: domain.intern(), + }, + Self::Sftp { domain, .. } => UrlBuf::Sftp { + loc: LocBuf::::saturated(path.into_os()?, self.kind()), + domain: domain.intern(), + }, + }; + + Ok(url.into()) + } + + pub fn try_strip_prefix(self, base: impl AsUrl) -> Result, StripPrefixError> { + use StripPrefixError::{Exotic, NotPrefix}; + use Url as U; let base = base.as_url(); - let prefix = self.loc.strip_prefix(base.loc)?.into(); + let prefix = self.loc().try_strip_prefix(base.loc())?.into(); - match (self.scheme, base.scheme) { + match (self, base) { // Same scheme - (S::Regular, S::Regular) => Some(prefix), - (S::Search(_), S::Search(_)) => Some(prefix), - (S::Archive(a), S::Archive(b)) => Some(prefix).filter(|_| a == b), - (S::Sftp(a), S::Sftp(b)) => Some(prefix).filter(|_| a == b), + (U::Regular(_), U::Regular(_)) => Ok(prefix), + (U::Search { .. }, U::Search { .. }) => Ok(prefix), + (U::Archive { domain: a, .. }, U::Archive { domain: b, .. }) => { + Some(prefix).filter(|_| a == b).ok_or(Exotic) + } + (U::Sftp { domain: a, .. }, U::Sftp { domain: b, .. }) => { + Some(prefix).filter(|_| a == b).ok_or(Exotic) + } // Both are local files - (S::Regular, S::Search(_)) => Some(prefix), - (S::Search(_), S::Regular) => Some(prefix), + (U::Regular(_), U::Search { .. }) => Ok(prefix), + (U::Search { .. }, U::Regular(_)) => Ok(prefix), // Only the entry of archives is a local file - (S::Regular, S::Archive(_)) => Some(prefix).filter(|_| base.uri().is_empty()), - (S::Search(_), S::Archive(_)) => Some(prefix).filter(|_| base.uri().is_empty()), - (S::Archive(_), S::Regular) => Some(prefix).filter(|_| self.uri().is_empty()), - (S::Archive(_), S::Search(_)) => Some(prefix).filter(|_| self.uri().is_empty()), + (U::Regular(_), U::Archive { .. }) => { + Some(prefix).filter(|_| base.uri().is_empty()).ok_or(NotPrefix) + } + (U::Search { .. }, U::Archive { .. }) => { + Some(prefix).filter(|_| base.uri().is_empty()).ok_or(NotPrefix) + } + (U::Archive { .. }, U::Regular(_)) => { + Some(prefix).filter(|_| self.uri().is_empty()).ok_or(NotPrefix) + } + (U::Archive { .. }, U::Search { .. }) => { + Some(prefix).filter(|_| self.uri().is_empty()).ok_or(NotPrefix) + } // Independent virtual file space - (S::Regular, S::Sftp(_)) => None, - (S::Search(_), S::Sftp(_)) => None, - (S::Archive(_), S::Sftp(_)) => None, - (S::Sftp(_), S::Regular) => None, - (S::Sftp(_), S::Search(_)) => None, - (S::Sftp(_), S::Archive(_)) => None, + (U::Regular(_), U::Sftp { .. }) => Err(Exotic), + (U::Search { .. }, U::Sftp { .. }) => Err(Exotic), + (U::Archive { .. }, U::Sftp { .. }) => Err(Exotic), + (U::Sftp { .. }, U::Regular(_)) => Err(Exotic), + (U::Sftp { .. }, U::Search { .. }) => Err(Exotic), + (U::Sftp { .. }, U::Archive { .. }) => Err(Exotic), } } #[inline] - pub fn uri(self) -> PathDyn<'a> { self.loc.uri().into() } - - #[inline] - pub fn urn(self) -> PathDyn<'a> { self.loc.urn().into() } - - #[inline] - pub fn name(self) -> Option<&'a OsStr> { self.loc.name() } - - #[inline] - pub fn stem(self) -> Option<&'a OsStr> { self.loc.stem() } - - #[inline] - pub fn ext(self) -> Option<&'a OsStr> { self.loc.ext() } - - pub fn base(self) -> Option { - use SchemeRef as S; - - if !self.loc.has_base() { - return None; + pub fn uri(self) -> PathDyn<'a> { + match self { + Self::Regular(loc) => loc.uri().as_path_dyn(), + Self::Search { loc, .. } => loc.uri().as_path_dyn(), + Self::Archive { loc, .. } => loc.uri().as_path_dyn(), + Self::Sftp { loc, .. } => loc.uri().as_path_dyn(), } + } - let loc = Loc::bare(self.loc.base()); - Some(match self.scheme { - S::Regular => Self { loc, scheme: S::Regular }, - S::Search(_) => Self { loc, scheme: self.scheme }, - S::Archive(_) => Self { loc, scheme: self.scheme }, - S::Sftp(_) => Self { loc, scheme: self.scheme }, + #[inline] + pub fn urn(self) -> PathDyn<'a> { + match self { + Self::Regular(loc) => loc.urn().as_path_dyn(), + Self::Search { loc, .. } => loc.urn().as_path_dyn(), + Self::Archive { loc, .. } => loc.urn().as_path_dyn(), + Self::Sftp { loc, .. } => loc.urn().as_path_dyn(), + } + } + + #[inline] + pub fn name(self) -> Option> { + Some(match self { + Self::Regular(loc) => loc.name()?.as_strand_dyn(), + Self::Search { loc, .. } => loc.name()?.as_strand_dyn(), + Self::Archive { loc, .. } => loc.name()?.as_strand_dyn(), + Self::Sftp { loc, .. } => loc.name()?.as_strand_dyn(), }) } + #[inline] + pub fn stem(self) -> Option> { + Some(match self { + Self::Regular(loc) => loc.stem()?.as_strand_dyn(), + Self::Search { loc, .. } => loc.stem()?.as_strand_dyn(), + Self::Archive { loc, .. } => loc.stem()?.as_strand_dyn(), + Self::Sftp { loc, .. } => loc.stem()?.as_strand_dyn(), + }) + } + + #[inline] + pub fn ext(self) -> Option> { + Some(match self { + Self::Regular(loc) => loc.ext()?.as_strand_dyn(), + Self::Search { loc, .. } => loc.ext()?.as_strand_dyn(), + Self::Archive { loc, .. } => loc.ext()?.as_strand_dyn(), + Self::Sftp { loc, .. } => loc.ext()?.as_strand_dyn(), + }) + } + + 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::Sftp { loc, domain } => Self::Sftp { loc: Loc::bare(loc.base()), domain }, + } + } + + #[inline] + pub fn trail(self) -> Self { + 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::Sftp { loc, domain } => Self::Sftp { loc: Loc::bare(loc.trail()), domain }, + } + } + + pub fn triple(self) -> (PathDyn<'a>, PathDyn<'a>, PathDyn<'a>) { + match self { + Self::Regular(loc) | Self::Search { loc, .. } | Self::Archive { loc, .. } => { + let (base, rest, urn) = loc.triple(); + (base.as_path_dyn(), rest.as_path_dyn(), urn.as_path_dyn()) + } + Self::Sftp { loc, .. } => { + let (base, rest, urn) = loc.triple(); + (base.as_path_dyn(), rest.as_path_dyn(), urn.as_path_dyn()) + } + } + } + pub fn parent(self) -> Option { - use SchemeRef as S; + let uri = self.uri(); - let parent = self.loc.parent()?; - let uri = self.loc.uri(); - - Some(match self.scheme { + Some(match self { // Regular - S::Regular => Self { loc: Loc::bare(parent), scheme: S::Regular }, + Self::Regular(loc) => Self::regular(loc.parent()?), // Search - S::Search(_) if uri.as_os_str().is_empty() => { - Self { loc: Loc::bare(parent), scheme: S::Regular } - } - S::Search(_) => { - Self { loc: Loc::new(parent, self.loc.base(), self.loc.base()), scheme: self.scheme } + Self::Search { loc, .. } if uri.is_empty() => Self::regular(loc.parent()?), + Self::Search { loc, domain } => { + Self::Search { loc: Loc::new(loc.parent()?, loc.base(), loc.base()), domain } } // Archive - S::Archive(_) if uri.as_os_str().is_empty() => { - Self { loc: Loc::bare(parent), scheme: S::Regular } + Self::Archive { loc, .. } if uri.is_empty() => Self::regular(loc.parent()?), + Self::Archive { loc, domain } if uri.components().nth(1).is_none() => { + Self::Archive { loc: Loc::zeroed(loc.parent()?), domain } } - S::Archive(_) if uri.components().nth(1).is_none() => { - Self { loc: Loc::zeroed(parent), scheme: self.scheme } + Self::Archive { loc, domain } => { + Self::Archive { loc: Loc::floated(loc.parent()?, loc.base()), domain } } - S::Archive(_) => Self { loc: Loc::floated(parent, self.loc.base()), scheme: self.scheme }, // SFTP - S::Sftp(_) => Self { loc: Loc::bare(parent), scheme: self.scheme }, + Self::Sftp { loc, domain } => Self::Sftp { loc: Loc::bare(loc.parent()?), domain }, }) } #[inline] - pub fn starts_with(self, base: impl AsUrl) -> bool { + pub fn try_starts_with(self, base: impl AsUrl) -> Result { let base = base.as_url(); - self.scheme.covariant(base.scheme) && self.loc.starts_with(base.loc) + Ok(self.loc().try_starts_with(base.loc())? && self.scheme().covariant(base.scheme())) } #[inline] - pub fn ends_with(self, child: impl AsUrl) -> bool { + pub fn try_ends_with(self, child: impl AsUrl) -> Result { let child = child.as_url(); - self.scheme.covariant(child.scheme) && self.loc.ends_with(child.loc) + Ok(self.loc().try_ends_with(child.loc())? && self.scheme().covariant(child.scheme())) } #[inline] @@ -198,20 +348,34 @@ impl<'a> Url<'a> { #[inline] pub fn covariant(self, other: impl AsUrl) -> bool { let other = other.as_url(); - self.scheme.covariant(other.scheme) && self.loc == other.loc + self.loc() == other.loc() && self.scheme().covariant(other.scheme()) } #[inline] - pub fn pair(self) -> Option<(Self, PathDyn<'a>)> { Some((self.parent()?, self.loc.urn().into())) } + pub fn pair(self) -> Option<(Self, PathDyn<'a>)> { Some((self.parent()?, self.urn())) } #[inline] - pub fn as_path(self) -> Option<&'a Path> { - Some(self.loc.as_path()).filter(|_| self.scheme.is_local()) + pub fn as_local(self) -> Option<&'a Path> { + self.loc().as_os().ok().filter(|_| self.kind().is_local()) } #[inline] - pub fn has_base(self) -> bool { self.loc.has_base() } + pub fn has_base(self) -> bool { + match self { + Self::Regular(loc) => loc.has_base(), + Self::Search { loc, .. } => loc.has_base(), + Self::Archive { loc, .. } => loc.has_base(), + Self::Sftp { loc, .. } => loc.has_base(), + } + } #[inline] - pub fn has_trail(self) -> bool { self.loc.has_trail() } + pub fn has_trail(self) -> bool { + match self { + Self::Regular(loc) => loc.has_trail(), + Self::Search { loc, .. } => loc.has_trail(), + Self::Archive { loc, .. } => loc.has_trail(), + Self::Sftp { loc, .. } => loc.has_trail(), + } + } } diff --git a/yazi-shared/src/wtf8.rs b/yazi-shared/src/wtf8.rs new file mode 100644 index 00000000..4364f80a --- /dev/null +++ b/yazi-shared/src/wtf8.rs @@ -0,0 +1,72 @@ +use std::ffi::{OsStr, OsString}; + +use anyhow::Result; + +// --- AsWtf8 +pub trait AsWtf8 { + fn as_wtf8(&self) -> &[u8]; +} + +impl AsWtf8 for OsStr { + fn as_wtf8(&self) -> &[u8] { + #[cfg(unix)] + { + use std::os::unix::ffi::OsStrExt; + self.as_bytes() + } + #[cfg(windows)] + { + self.as_encoded_bytes() + } + } +} + +// --- FromWtf8 +pub trait FromWtf8 { + fn from_wtf8(wtf8: &[u8]) -> Result<&Self>; +} + +impl FromWtf8 for OsStr { + fn from_wtf8(wtf8: &[u8]) -> Result<&Self> { + #[cfg(unix)] + { + use std::os::unix::ffi::OsStrExt; + Ok(OsStr::from_bytes(wtf8)) + } + #[cfg(windows)] + { + // FIXME: validate WTF-8 + Ok(unsafe { OsStr::from_encoded_bytes_unchecked(wtf8) }) + } + } +} + +impl FromWtf8 for std::path::Path { + fn from_wtf8(wtf8: &[u8]) -> Result<&Self> { Ok(OsStr::from_wtf8(wtf8)?.as_ref()) } +} + +// --- FromWtf8Vec +pub trait FromWtf8Vec { + fn from_wtf8_vec(wtf8: Vec) -> Result + where + Self: Sized; +} + +impl FromWtf8Vec for OsString { + fn from_wtf8_vec(wtf8: Vec) -> Result { + #[cfg(unix)] + { + use std::os::unix::ffi::OsStringExt; + Ok(OsString::from_vec(wtf8)) + } + #[cfg(windows)] + { + // FIXME: validate WTF-8 + Ok(unsafe { OsString::from_encoded_bytes_unchecked(wtf8) }) + } + } +} + +impl FromWtf8Vec for std::path::PathBuf { + fn from_wtf8_vec(wtf8: Vec) -> Result { Ok(OsString::from_wtf8_vec(wtf8)?.into()) } +} diff --git a/yazi-term/src/lib.rs b/yazi-term/src/lib.rs index c743409c..1be38f4f 100644 --- a/yazi-term/src/lib.rs +++ b/yazi-term/src/lib.rs @@ -1,5 +1,3 @@ -#![allow(clippy::unit_arg)] - yazi_macro::mod_pub!(tty); yazi_macro::mod_flat!(background cursor r#if); diff --git a/yazi-vfs/src/fns.rs b/yazi-vfs/src/fns.rs index d6b035e4..5bf08c25 100644 --- a/yazi-vfs/src/fns.rs +++ b/yazi-vfs/src/fns.rs @@ -1,9 +1,9 @@ -use std::{ffi::OsString, io}; +use std::io; use tokio::{select, sync::{mpsc, oneshot}}; use yazi_fs::provider::Attrs; use yazi_macro::ok_or_not_found; -use yazi_shared::url::{AsUrl, Url, UrlBuf, UrlLike}; +use yazi_shared::{strand::{StrandBuf, StrandBufLike, StrandLike}, url::{AsUrl, Url, UrlBuf, UrlLike}}; use crate::provider; @@ -36,27 +36,30 @@ async fn _unique_name(mut url: UrlBuf, append: bool) -> io::Result { return Err(io::Error::new(io::ErrorKind::InvalidInput, "empty file stem")); }; - let dot_ext = url.ext().map_or_else(OsString::new, |e| { - let mut s = OsString::with_capacity(e.len() + 1); - s.push("."); - s.push(e); - s - }); + let dot_ext = match url.ext() { + Some(e) => { + let mut s = StrandBuf::with_capacity(url.kind(), e.len() + 1); + s.try_push(".")?; + s.try_push(e)?; + s + } + None => StrandBuf::default(), + }; - let mut name = OsString::with_capacity(stem.len() + dot_ext.len() + 5); + let mut name = StrandBuf::with_capacity(url.kind(), stem.len() + dot_ext.len() + 5); for i in 1u64.. { name.clear(); - name.push(&stem); + name.try_push(&stem)?; if append { - name.push(&dot_ext); - name.push(format!("_{i}")); + name.try_push(&dot_ext)?; + name.try_push(format!("_{i}"))?; } else { - name.push(format!("_{i}")); - name.push(&dot_ext); + name.try_push(format!("_{i}"))?; + name.try_push(&dot_ext)?; } - url.set_name(&name); + url.try_set_name(&name)?; ok_or_not_found!(provider::symlink_metadata(&url).await, break); } diff --git a/yazi-vfs/src/lib.rs b/yazi-vfs/src/lib.rs index 28b92170..1b3ab656 100644 --- a/yazi-vfs/src/lib.rs +++ b/yazi-vfs/src/lib.rs @@ -1,4 +1,4 @@ -#![allow(clippy::if_same_then_else, clippy::unit_arg)] +#![allow(clippy::if_same_then_else)] yazi_macro::mod_pub!(provider); diff --git a/yazi-vfs/src/provider/dir_entry.rs b/yazi-vfs/src/provider/dir_entry.rs index db1e0173..40b75e64 100644 --- a/yazi-vfs/src/provider/dir_entry.rs +++ b/yazi-vfs/src/provider/dir_entry.rs @@ -1,55 +1,46 @@ -use std::{borrow::Cow, ffi::OsStr, io, sync::Arc}; +use std::io; use yazi_fs::{cha::{Cha, ChaType}, provider::FileHolder}; -use yazi_shared::url::{UrlBuf, UrlLike}; +use yazi_shared::{path::PathBufDyn, strand::StrandCow, url::UrlBuf}; pub enum DirEntry { - Regular(yazi_fs::provider::local::DirEntry), - Search((Arc, yazi_fs::provider::local::DirEntry)), - Sftp((Arc, super::sftp::DirEntry)), + Local(yazi_fs::provider::local::DirEntry), + Sftp(super::sftp::DirEntry), } impl FileHolder for DirEntry { - fn path(&self) -> std::path::PathBuf { + async fn file_type(&self) -> io::Result { match self { - Self::Regular(ent) => ent.path(), - Self::Search((_, ent)) => ent.path(), - Self::Sftp((_, ent)) => ent.path(), - } - } - - fn name(&self) -> Cow<'_, OsStr> { - match self { - Self::Regular(ent) => ent.name(), - Self::Search((_, ent)) => ent.name(), - Self::Sftp((_, ent)) => ent.name(), + Self::Local(entry) => entry.file_type().await, + Self::Sftp(entry) => entry.file_type().await, } } async fn metadata(&self) -> io::Result { match self { - Self::Regular(ent) => ent.metadata().await, - Self::Search((_, ent)) => ent.metadata().await, - Self::Sftp((_, ent)) => ent.metadata().await, + Self::Local(entry) => entry.metadata().await, + Self::Sftp(entry) => entry.metadata().await, } } - async fn file_type(&self) -> io::Result { + fn name(&self) -> StrandCow<'_> { match self { - Self::Regular(ent) => ent.file_type().await, - Self::Search((_, ent)) => ent.file_type().await, - Self::Sftp((_, ent)) => ent.file_type().await, + Self::Local(entry) => entry.name(), + Self::Sftp(entry) => entry.name(), } } -} -impl DirEntry { - #[must_use] - pub fn url(&self) -> UrlBuf { + fn path(&self) -> PathBufDyn { match self { - Self::Regular(ent) => ent.path().into(), - Self::Search((dir, ent)) => dir.join(ent.name()), - Self::Sftp((dir, ent)) => dir.join(ent.name()), + Self::Local(entry) => entry.path(), + Self::Sftp(entry) => entry.path(), + } + } + + fn url(&self) -> UrlBuf { + match self { + Self::Local(entry) => entry.url(), + Self::Sftp(entry) => entry.url(), } } } diff --git a/yazi-vfs/src/provider/gate.rs b/yazi-vfs/src/provider/gate.rs index 8031e902..a62f9465 100644 --- a/yazi-vfs/src/provider/gate.rs +++ b/yazi-vfs/src/provider/gate.rs @@ -1,99 +1,96 @@ -use std::{io, path::Path}; +use std::io; use yazi_fs::provider::{Attrs, FileBuilder}; -use yazi_shared::scheme::SchemeRef; +use yazi_shared::{scheme::SchemeKind, url::AsUrl}; -pub enum Gate { - Local(yazi_fs::provider::local::Gate), - Sftp(super::sftp::Gate), -} - -impl From for Gate { - fn from(value: yazi_fs::provider::local::Gate) -> Self { Self::Local(value) } -} - -impl From for Gate { - fn from(value: super::sftp::Gate) -> Self { Self::Sftp(value) } +#[derive(Clone, Copy, Default)] +pub struct Gate { + pub(super) append: bool, + pub(super) attrs: Attrs, + pub(super) create: bool, + pub(super) create_new: bool, + pub(super) read: bool, + pub(super) truncate: bool, + pub(super) write: bool, } impl FileBuilder for Gate { type File = super::RwFile; fn append(&mut self, append: bool) -> &mut Self { - match self { - Self::Local(g) => _ = g.append(append), - Self::Sftp(g) => _ = g.append(append), - }; + self.append = append; self } fn attrs(&mut self, attrs: Attrs) -> &mut Self { - match self { - Self::Local(g) => _ = g.attrs(attrs), - Self::Sftp(g) => _ = g.attrs(attrs), - }; + self.attrs = attrs; self } fn create(&mut self, create: bool) -> &mut Self { - match self { - Self::Local(g) => _ = g.create(create), - Self::Sftp(g) => _ = g.create(create), - }; + self.create = create; self } fn create_new(&mut self, create_new: bool) -> &mut Self { - match self { - Self::Local(g) => _ = g.create_new(create_new), - Self::Sftp(g) => _ = g.create_new(create_new), - }; + self.create_new = create_new; self } - async fn new(scheme: SchemeRef<'_>) -> io::Result { - Ok(match scheme { - SchemeRef::Regular | SchemeRef::Search(_) => { - yazi_fs::provider::local::Gate::new(scheme).await?.into() + async fn open(&self, url: U) -> io::Result + where + U: AsUrl, + { + let url = url.as_url(); + Ok(match url.kind() { + SchemeKind::Regular | SchemeKind::Search => { + self.build::().open(url).await?.into() } - SchemeRef::Archive(_) => { + SchemeKind::Archive => { Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem: archive"))? } - SchemeRef::Sftp(_) => super::sftp::Gate::new(scheme).await?.into(), - }) - } - - async fn open

(&self, path: P) -> io::Result - where - P: AsRef, - { - Ok(match self { - Gate::Local(g) => g.open(path).await?.into(), - Gate::Sftp(g) => g.open(path).await?.into(), + SchemeKind::Sftp => self.build::().open(url).await?.into(), }) } fn read(&mut self, read: bool) -> &mut Self { - match self { - Self::Local(g) => _ = g.read(read), - Self::Sftp(g) => _ = g.read(read), - }; + self.read = read; self } fn truncate(&mut self, truncate: bool) -> &mut Self { - match self { - Self::Local(g) => _ = g.truncate(truncate), - Self::Sftp(g) => _ = g.truncate(truncate), - }; + self.truncate = truncate; self } fn write(&mut self, write: bool) -> &mut Self { - match self { - Self::Local(g) => _ = g.write(write), - Self::Sftp(g) => _ = g.write(write), - }; + self.write = write; self } } + +impl Gate { + fn build(self) -> T { + let mut gate = T::default(); + if self.append { + gate.append(true); + } + gate.attrs(self.attrs); + if self.create { + gate.create(true); + } + if self.create_new { + gate.create_new(true); + } + if self.read { + gate.read(true); + } + if self.truncate { + gate.truncate(true); + } + if self.write { + gate.write(true); + } + gate + } +} diff --git a/yazi-vfs/src/provider/provider.rs b/yazi-vfs/src/provider/provider.rs index 1ddddc99..99a57ead 100644 --- a/yazi-vfs/src/provider/provider.rs +++ b/yazi-vfs/src/provider/provider.rs @@ -1,8 +1,8 @@ -use std::{io, path::{Path, PathBuf}}; +use std::io; use tokio::io::{AsyncWriteExt, BufReader, BufWriter}; use yazi_fs::{cha::Cha, provider::{Attrs, Provider, local::Local}}; -use yazi_shared::{scheme::SchemeRef, url::{AsUrl, UrlBuf, UrlCow}}; +use yazi_shared::{path::{AsPathDyn, PathBufDyn}, url::{AsUrl, UrlBuf, UrlCow}}; use super::{Providers, ReadDir, RwFile}; @@ -10,7 +10,7 @@ pub async fn absolute<'a, U>(url: &'a U) -> io::Result> where U: AsUrl, { - Providers::new(url.as_url()).await?.absolute(url).await + Providers::new(url.as_url()).await?.absolute().await } pub async fn calculate(url: U) -> io::Result @@ -18,7 +18,7 @@ where U: AsUrl, { let url = url.as_url(); - if let Some(path) = url.as_path() { + if let Some(path) = url.as_local() { yazi_fs::provider::local::SizeCalculator::total(path).await } else { super::SizeCalculator::total(url).await @@ -29,32 +29,14 @@ pub async fn canonicalize(url: U) -> io::Result where U: AsUrl, { - let url = url.as_url(); - let canon = Providers::new(url).await?.canonicalize(url.loc).await?; - - Ok(match url.scheme { - SchemeRef::Regular | SchemeRef::Search(_) => canon.into(), - SchemeRef::Archive(_) => { - Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem: archive"))? - } - SchemeRef::Sftp(_) => UrlBuf { loc: canon.into(), scheme: url.scheme.into() }, - }) + Providers::new(url.as_url()).await?.canonicalize().await } pub async fn casefold(url: U) -> io::Result where U: AsUrl, { - let url = url.as_url(); - let fold = Providers::new(url).await?.casefold(url.loc).await?; - - Ok(match url.scheme { - SchemeRef::Regular | SchemeRef::Search(_) => fold.into(), - SchemeRef::Archive(_) => { - Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem: archive"))? - } - SchemeRef::Sftp(_) => UrlBuf { loc: fold.into(), scheme: url.scheme.into() }, - }) + Providers::new(url.as_url()).await?.casefold().await } pub async fn copy(from: U, to: V, attrs: Attrs) -> io::Result @@ -64,14 +46,14 @@ where { let (from, to) = (from.as_url(), to.as_url()); - match (from.as_path(), to.as_path()) { - (Some(from), Some(to)) => Local.copy(from, to, attrs).await, - (None, None) if from.scheme.covariant(to.scheme) => { - Providers::new(from).await?.copy(from.loc, to.loc, attrs).await + match (from.kind().is_local(), to.kind().is_local()) { + (true, true) => Local::new(from).await?.copy(to.loc(), attrs).await, + (false, false) if from.scheme().covariant(to.scheme()) => { + Providers::new(from).await?.copy(to.loc(), attrs).await } - (Some(_), None) | (None, Some(_)) | (None, None) => { - let src = Providers::new(from).await?.open(from.loc).await?; - let dist = Providers::new(to).await?.create(to.loc).await?; + (true, false) | (false, true) | (false, false) => { + let src = Providers::new(from).await?.open().await?; + let dist = Providers::new(to).await?.create().await?; let mut reader = BufReader::with_capacity(524288, src); let mut writer = BufWriter::with_capacity(524288, dist); @@ -89,24 +71,21 @@ pub async fn create(url: U) -> io::Result where U: AsUrl, { - let url = url.as_url(); - Providers::new(url).await?.create(url.loc).await + Providers::new(url.as_url()).await?.create().await } pub async fn create_dir(url: U) -> io::Result<()> where U: AsUrl, { - let url = url.as_url(); - Providers::new(url).await?.create_dir(url.loc).await + Providers::new(url.as_url()).await?.create_dir().await } pub async fn create_dir_all(url: U) -> io::Result<()> where U: AsUrl, { - let url = url.as_url(); - Providers::new(url).await?.create_dir_all(url.loc).await + Providers::new(url.as_url()).await?.create_dir_all().await } pub async fn hard_link(original: U, link: V) -> io::Result<()> @@ -115,8 +94,8 @@ where V: AsUrl, { let (original, link) = (original.as_url(), link.as_url()); - if original.scheme.covariant(link.scheme) { - Providers::new(original).await?.hard_link(original.loc, link.loc).await + if original.scheme().covariant(link.scheme()) { + Providers::new(original).await?.hard_link(link.loc()).await } else { Err(io::Error::from(io::ErrorKind::CrossesDevices)) } @@ -127,7 +106,7 @@ where U: AsUrl, V: AsUrl, { - if let (Some(a), Some(b)) = (a.as_url().as_path(), b.as_url().as_path()) { + if let (Some(a), Some(b)) = (a.as_url().as_local(), b.as_url().as_local()) { yazi_fs::provider::local::identical(a, b).await } else { Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem")) @@ -138,8 +117,7 @@ pub async fn metadata(url: U) -> io::Result where U: AsUrl, { - let url = url.as_url(); - Providers::new(url).await?.metadata(url.loc).await + Providers::new(url.as_url()).await?.metadata().await } pub async fn must_identical(a: U, b: V) -> bool @@ -154,48 +132,42 @@ pub async fn read_dir(url: U) -> io::Result where U: AsUrl, { - let url = url.as_url(); - Providers::new(url).await?.read_dir(url.loc).await + Providers::new(url.as_url()).await?.read_dir().await } -pub async fn read_link(url: U) -> io::Result +pub async fn read_link(url: U) -> io::Result where U: AsUrl, { - let url = url.as_url(); - Providers::new(url).await?.read_link(url.loc).await + Providers::new(url.as_url()).await?.read_link().await } pub async fn remove_dir(url: U) -> io::Result<()> where U: AsUrl, { - let url = url.as_url(); - Providers::new(url).await?.remove_dir(url.loc).await + Providers::new(url.as_url()).await?.remove_dir().await } pub async fn remove_dir_all(url: U) -> io::Result<()> where U: AsUrl, { - let url = url.as_url(); - Providers::new(url).await?.remove_dir_all(url.loc).await + Providers::new(url.as_url()).await?.remove_dir_all().await } pub async fn remove_dir_clean(url: U) -> io::Result<()> where U: AsUrl, { - let url = url.as_url(); - Ok(Providers::new(url).await?.remove_dir_clean(url.loc).await) + Ok(Providers::new(url.as_url()).await?.remove_dir_clean().await) } pub async fn remove_file(url: U) -> io::Result<()> where U: AsUrl, { - let url = url.as_url(); - Providers::new(url).await?.remove_file(url.loc).await + Providers::new(url.as_url()).await?.remove_file().await } pub async fn rename(from: U, to: V) -> io::Result<()> @@ -204,52 +176,55 @@ where V: AsUrl, { let (from, to) = (from.as_url(), to.as_url()); - if from.scheme.covariant(to.scheme) { - Providers::new(from).await?.rename(from.loc, to.loc).await + if from.scheme().covariant(to.scheme()) { + Providers::new(from).await?.rename(to.loc()).await } else { Err(io::Error::from(io::ErrorKind::CrossesDevices)) } } -pub async fn symlink(original: &Path, link: U, is_dir: F) -> io::Result<()> +pub async fn symlink(original: U, link: V, is_dir: F) -> io::Result<()> where U: AsUrl, + V: AsUrl, F: AsyncFnOnce() -> io::Result, { - let link = link.as_url(); - Providers::new(link).await?.symlink(original, link.loc, is_dir).await + let (original, link) = (original.as_url(), link.as_url()); + if original.scheme().covariant(link.scheme()) { + Providers::new(link).await?.symlink(original.loc(), is_dir).await + } else { + Err(io::Error::from(io::ErrorKind::CrossesDevices)) + } } -pub async fn symlink_dir(original: &Path, link: U) -> io::Result<()> +pub async fn symlink_dir(original: P, link: U) -> io::Result<()> where + P: AsPathDyn, U: AsUrl, { - let link = link.as_url(); - Providers::new(link).await?.symlink_dir(original, link.loc).await + Providers::new(link.as_url()).await?.symlink_dir(original).await } -pub async fn symlink_file(original: &Path, link: U) -> io::Result<()> +pub async fn symlink_file(original: P, link: U) -> io::Result<()> where + P: AsPathDyn, U: AsUrl, { - let link = link.as_url(); - Providers::new(link).await?.symlink_file(original, link.loc).await + Providers::new(link.as_url()).await?.symlink_file(original).await } pub async fn symlink_metadata(url: U) -> io::Result where U: AsUrl, { - let url = url.as_url(); - Providers::new(url).await?.symlink_metadata(url.loc).await + Providers::new(url.as_url()).await?.symlink_metadata().await } pub async fn trash(url: U) -> io::Result<()> where U: AsUrl, { - let url = url.as_url(); - Providers::new(url).await?.trash(url.loc).await + Providers::new(url.as_url()).await?.trash().await } pub async fn write(url: U, contents: C) -> io::Result<()> @@ -257,6 +232,5 @@ where U: AsUrl, C: AsRef<[u8]>, { - let url = url.as_url(); - Providers::new(url).await?.write(url.loc, contents).await + Providers::new(url.as_url()).await?.write(contents).await } diff --git a/yazi-vfs/src/provider/providers.rs b/yazi-vfs/src/provider/providers.rs index aac8c07e..e2065160 100644 --- a/yazi-vfs/src/provider/providers.rs +++ b/yazi-vfs/src/provider/providers.rs @@ -1,274 +1,213 @@ -use std::{io, path::{Path, PathBuf}, sync::Arc}; +use std::io; -use yazi_config::vfs::{ProviderSftp, Vfs}; -use yazi_fs::{cha::Cha, provider::{Attrs, Provider, local::Local}}; -use yazi_shared::{scheme::SchemeRef, url::{AsUrl, Url, UrlCow}}; +use yazi_fs::{cha::Cha, provider::{Attrs, Provider}}; +use yazi_shared::{path::{AsPathDyn, PathBufDyn}, url::{Url, UrlBuf, UrlCow}}; -pub(super) struct Providers<'a>(Inner<'a>); - -enum Inner<'a> { - Regular, - Search(Url<'a>), - Sftp((super::sftp::Sftp, Url<'a>)), +#[derive(Clone)] +pub(super) enum Providers<'a> { + Local(yazi_fs::provider::local::Local<'a>), + Sftp(super::sftp::Sftp<'a>), } -impl<'a> Providers<'a> { - pub(super) async fn new(url: Url<'a>) -> io::Result { - Ok(match url.scheme { - SchemeRef::Regular => Self(Inner::Regular), - SchemeRef::Search(_) => Self(Inner::Search(url)), - SchemeRef::Archive(_) => { - Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem: archive"))? - } - SchemeRef::Sftp(name) => { - Self(Inner::Sftp((Vfs::provider::<&ProviderSftp>(name).await?.into(), url))) - } - }) - } -} - -impl Provider for Providers<'_> { +impl<'a> Provider for Providers<'a> { type File = super::RwFile; type Gate = super::Gate; + type Me<'b> = Providers<'b>; type ReadDir = super::ReadDir; + type UrlCow = UrlCow<'a>; - async fn absolute<'a, U>(&self, url: &'a U) -> io::Result> - where - U: AsUrl, - { - match self.0 { - Inner::Regular | Inner::Search(_) => Local.absolute(url).await, - Inner::Sftp((p, _)) => p.absolute(url).await, + async fn absolute(&self) -> io::Result { + match self { + Self::Local(p) => p.absolute().await, + Self::Sftp(p) => p.absolute().await, } } - async fn canonicalize

(&self, path: P) -> io::Result - where - P: AsRef, - { - match self.0 { - Inner::Regular | Inner::Search(_) => Local.canonicalize(path).await, - Inner::Sftp((p, _)) => p.canonicalize(path).await, + async fn canonicalize(&self) -> io::Result { + match self { + Self::Local(p) => p.canonicalize().await, + Self::Sftp(p) => p.canonicalize().await, } } - async fn casefold

(&self, path: P) -> io::Result - where - P: AsRef, - { - match self.0 { - Inner::Regular | Inner::Search(_) => Local.casefold(path).await, - Inner::Sftp((p, _)) => p.casefold(path).await, + async fn casefold(&self) -> io::Result { + match self { + Self::Local(p) => p.casefold().await, + Self::Sftp(p) => p.casefold().await, } } - async fn copy(&self, from: P, to: Q, attrs: Attrs) -> io::Result + async fn copy

(&self, to: P, attrs: Attrs) -> io::Result where - P: AsRef, - Q: AsRef, + P: AsPathDyn, { - match self.0 { - Inner::Regular | Inner::Search(_) => Local.copy(from, to, attrs).await, - Inner::Sftp((p, _)) => p.copy(from, to, attrs).await, + match self { + Self::Local(p) => p.copy(to, attrs).await, + Self::Sftp(p) => p.copy(to, attrs).await, } } - async fn create

(&self, path: P) -> io::Result - where - P: AsRef, - { - Ok(match self.0 { - Inner::Regular | Inner::Search(_) => Local.create(path).await?.into(), - Inner::Sftp((p, _)) => p.create(path).await?.into(), + async fn create(&self) -> io::Result { + Ok(match self { + Self::Local(p) => p.create().await?.into(), + Self::Sftp(p) => p.create().await?.into(), }) } - async fn create_dir

(&self, path: P) -> io::Result<()> - where - P: AsRef, - { - match self.0 { - Inner::Regular | Inner::Search(_) => Local.create_dir(path).await, - Inner::Sftp((p, _)) => p.create_dir(path).await, + async fn create_dir(&self) -> io::Result<()> { + match self { + Self::Local(p) => p.create_dir().await, + Self::Sftp(p) => p.create_dir().await, } } - async fn create_dir_all

(&self, path: P) -> io::Result<()> - where - P: AsRef, - { - match self.0 { - Inner::Regular | Inner::Search(_) => Local.create_dir_all(path).await, - Inner::Sftp((p, _)) => p.create_dir_all(path).await, + async fn create_dir_all(&self) -> io::Result<()> { + match self { + Self::Local(p) => p.create_dir_all().await, + Self::Sftp(p) => p.create_dir_all().await, } } - async fn gate(&self) -> io::Result { - Ok(match self.0 { - Inner::Regular | Inner::Search(_) => Local.gate().await?.into(), - Inner::Sftp((p, _)) => p.gate().await?.into(), - }) - } - - async fn hard_link(&self, original: P, link: Q) -> io::Result<()> + async fn hard_link

(&self, to: P) -> io::Result<()> where - P: AsRef, - Q: AsRef, + P: AsPathDyn, { - match self.0 { - Inner::Regular | Inner::Search(_) => Local.hard_link(original, link).await, - Inner::Sftp((p, _)) => p.hard_link(original, link).await, + match self { + Self::Local(p) => p.hard_link(to).await, + Self::Sftp(p) => p.hard_link(to).await, } } - async fn metadata

(&self, path: P) -> io::Result - where - P: AsRef, - { - match self.0 { - Inner::Regular | Inner::Search(_) => Local.metadata(path).await, - Inner::Sftp((p, _)) => p.metadata(path).await, + async fn metadata(&self) -> io::Result { + match self { + Self::Local(p) => p.metadata().await, + Self::Sftp(p) => p.metadata().await, } } - async fn open

(&self, path: P) -> io::Result - where - P: AsRef, - { - Ok(match self.0 { - Inner::Regular | Inner::Search(_) => Local.open(path).await?.into(), - Inner::Sftp((p, _)) => p.open(path).await?.into(), - }) - } + async fn new<'b>(url: Url<'b>) -> io::Result> { + use yazi_shared::scheme::SchemeKind as K; - async fn read_dir

(&self, path: P) -> io::Result - where - P: AsRef, - { - Ok(match self.0 { - Inner::Regular => Self::ReadDir::Regular(Local.read_dir(path).await?), - Inner::Search(dir) => { - Self::ReadDir::Search((Arc::new(dir.to_owned()), Local.read_dir(path).await?)) - } - Inner::Sftp((p, dir)) => { - Self::ReadDir::Sftp((Arc::new(dir.to_owned()), p.read_dir(path).await?)) + Ok(match url.kind() { + K::Regular | K::Search => Self::Me::Local(yazi_fs::provider::local::Local::new(url).await?), + K::Archive => { + Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem: archive"))? } + K::Sftp => Self::Me::Sftp(super::sftp::Sftp::new(url).await?), }) } - async fn read_link

(&self, path: P) -> io::Result - where - P: AsRef, - { - match self.0 { - Inner::Regular | Inner::Search(_) => Local.read_link(path).await, - Inner::Sftp((p, _)) => p.read_link(path).await, + async fn open(&self) -> io::Result { + Ok(match self { + Self::Local(p) => p.open().await?.into(), + Self::Sftp(p) => p.open().await?.into(), + }) + } + + async fn read_dir(self) -> io::Result { + Ok(match self { + Self::Local(p) => Self::ReadDir::Local(p.read_dir().await?), + Self::Sftp(p) => Self::ReadDir::Sftp(p.read_dir().await?), + }) + } + + async fn read_link(&self) -> io::Result { + match self { + Self::Local(p) => p.read_link().await, + Self::Sftp(p) => p.read_link().await, } } - async fn remove_dir

(&self, path: P) -> io::Result<()> - where - P: AsRef, - { - match self.0 { - Inner::Regular | Inner::Search(_) => Local.remove_dir(path).await, - Inner::Sftp((p, _)) => p.remove_dir(path).await, + async fn remove_dir(&self) -> io::Result<()> { + match self { + Self::Local(p) => p.remove_dir().await, + Self::Sftp(p) => p.remove_dir().await, } } - async fn remove_dir_all

(&self, path: P) -> io::Result<()> - where - P: AsRef, - { - match self.0 { - Inner::Regular | Inner::Search(_) => Local.remove_dir_all(path).await, - Inner::Sftp((p, _)) => p.remove_dir_all(path).await, + async fn remove_dir_all(&self) -> io::Result<()> { + match self { + Self::Local(p) => p.remove_dir_all().await, + Self::Sftp(p) => p.remove_dir_all().await, } } - async fn remove_file

(&self, path: P) -> io::Result<()> - where - P: AsRef, - { - match self.0 { - Inner::Regular | Inner::Search(_) => Local.remove_file(path).await, - Inner::Sftp((p, _)) => p.remove_file(path).await, + async fn remove_file(&self) -> io::Result<()> { + match self { + Self::Local(p) => p.remove_file().await, + Self::Sftp(p) => p.remove_file().await, } } - async fn rename(&self, from: P, to: Q) -> io::Result<()> + async fn rename

(&self, to: P) -> io::Result<()> where - P: AsRef, - Q: AsRef, + P: AsPathDyn, { - match self.0 { - Inner::Regular | Inner::Search(_) => Local.rename(from, to).await, - Inner::Sftp((p, _)) => p.rename(from, to).await, + match self { + Self::Local(p) => p.rename(to).await, + Self::Sftp(p) => p.rename(to).await, } } - async fn symlink(&self, original: P, link: Q, is_dir: F) -> io::Result<()> + async fn symlink(&self, original: P, is_dir: F) -> io::Result<()> where - P: AsRef, - Q: AsRef, + P: AsPathDyn, F: AsyncFnOnce() -> io::Result, { - match self.0 { - Inner::Regular | Inner::Search(_) => Local.symlink(original, link, is_dir).await, - Inner::Sftp((p, _)) => p.symlink(original, link, is_dir).await, + match self { + Self::Local(p) => p.symlink(original, is_dir).await, + Self::Sftp(p) => p.symlink(original, is_dir).await, } } - async fn symlink_dir(&self, original: P, link: Q) -> io::Result<()> + async fn symlink_dir

(&self, original: P) -> io::Result<()> where - P: AsRef, - Q: AsRef, + P: AsPathDyn, { - match self.0 { - Inner::Regular | Inner::Search(_) => Local.symlink_dir(original, link).await, - Inner::Sftp((p, _)) => p.symlink_dir(original, link).await, + match self { + Self::Local(p) => p.symlink_dir(original).await, + Self::Sftp(p) => p.symlink_dir(original).await, } } - async fn symlink_file(&self, original: P, link: Q) -> io::Result<()> + async fn symlink_file

(&self, original: P) -> io::Result<()> where - P: AsRef, - Q: AsRef, + P: AsPathDyn, { - match self.0 { - Inner::Regular | Inner::Search(_) => Local.symlink_file(original, link).await, - Inner::Sftp((p, _)) => p.symlink_file(original, link).await, + match self { + Self::Local(p) => p.symlink_file(original).await, + Self::Sftp(p) => p.symlink_file(original).await, } } - async fn symlink_metadata

(&self, path: P) -> io::Result - where - P: AsRef, - { - match self.0 { - Inner::Regular | Inner::Search(_) => Local.symlink_metadata(path).await, - Inner::Sftp((p, _)) => p.symlink_metadata(path).await, + async fn symlink_metadata(&self) -> io::Result { + match self { + Self::Local(p) => p.symlink_metadata().await, + Self::Sftp(p) => p.symlink_metadata().await, } } - async fn trash

(&self, path: P) -> io::Result<()> - where - P: AsRef, - { - match self.0 { - Inner::Regular | Inner::Search(_) => Local.trash(path).await, - Inner::Sftp((p, _)) => p.trash(path).await, + async fn trash(&self) -> io::Result<()> { + match self { + Self::Local(p) => p.trash().await, + Self::Sftp(p) => p.trash().await, } } - async fn write(&self, path: P, contents: C) -> io::Result<()> + fn url(&self) -> Url<'_> { + match self { + Self::Local(p) => p.url(), + Self::Sftp(p) => p.url(), + } + } + + async fn write(&self, contents: C) -> io::Result<()> where - P: AsRef, C: AsRef<[u8]>, { - match self.0 { - Inner::Regular | Inner::Search(_) => Local.write(path, contents).await, - Inner::Sftp((p, _)) => p.write(path, contents).await, + match self { + Self::Local(p) => p.write(contents).await, + Self::Sftp(p) => p.write(contents).await, } } } diff --git a/yazi-vfs/src/provider/read_dir.rs b/yazi-vfs/src/provider/read_dir.rs index 303144a4..55e63159 100644 --- a/yazi-vfs/src/provider/read_dir.rs +++ b/yazi-vfs/src/provider/read_dir.rs @@ -1,12 +1,10 @@ -use std::{io, sync::Arc}; +use std::io; use yazi_fs::provider::DirReader; -use yazi_shared::url::UrlBuf; pub enum ReadDir { - Regular(yazi_fs::provider::local::ReadDir), - Search((Arc, yazi_fs::provider::local::ReadDir)), - Sftp((Arc, super::sftp::ReadDir)), + Local(yazi_fs::provider::local::ReadDir), + Sftp(super::sftp::ReadDir), } impl DirReader for ReadDir { @@ -14,13 +12,8 @@ impl DirReader for ReadDir { async fn next(&mut self) -> io::Result> { Ok(match self { - Self::Regular(reader) => reader.next().await?.map(Self::Entry::Regular), - Self::Search((dir, reader)) => { - reader.next().await?.map(|ent| Self::Entry::Search((dir.clone(), ent))) - } - Self::Sftp((dir, reader)) => { - reader.next().await?.map(|ent| Self::Entry::Sftp((dir.clone(), ent))) - } + Self::Local(reader) => reader.next().await?.map(Self::Entry::Local), + Self::Sftp(reader) => reader.next().await?.map(Self::Entry::Sftp), }) } } diff --git a/yazi-vfs/src/provider/sftp/conn.rs b/yazi-vfs/src/provider/sftp/conn.rs new file mode 100644 index 00000000..14761226 --- /dev/null +++ b/yazi-vfs/src/provider/sftp/conn.rs @@ -0,0 +1,176 @@ +use std::{io, sync::Arc}; + +use russh::keys::PrivateKeyWithHashAlg; +use yazi_config::vfs::ProviderSftp; +use yazi_fs::provider::local::Local; + +#[derive(Clone, Copy)] +pub(super) struct Conn { + pub(super) name: &'static str, + pub(super) config: &'static ProviderSftp, +} + +impl russh::client::Handler for Conn { + type Error = russh::Error; + + async fn check_server_key( + &mut self, + _server_public_key: &russh::keys::PublicKey, + ) -> Result { + Ok(true) + } +} + +impl deadpool::managed::Manager for Conn { + type Error = io::Error; + type Type = yazi_sftp::Operator; + + async fn create(&self) -> Result { + let channel = self.connect().await.map_err(|e| { + io::Error::other(format!("Failed to connect to SFTP server `{}`: {e}", self.name)) + })?; + + let mut op = yazi_sftp::Operator::make(channel.into_stream()); + op.init().await?; + Ok(op) + } + + async fn recycle( + &self, + obj: &mut Self::Type, + _metrics: &deadpool::managed::Metrics, + ) -> deadpool::managed::RecycleResult { + if obj.is_closed() { + Err(deadpool::managed::RecycleError::Message("Channel closed".into())) + } else { + Ok(()) + } + } +} + +impl Conn { + pub(super) async fn roll(self) -> io::Result> { + use deadpool::managed::PoolError; + + let pool = *super::CONN.lock().entry(self.config).or_insert_with(|| { + Box::leak(Box::new(deadpool::managed::Pool::builder(self).max_size(5).build().unwrap())) + }); + + pool.get().await.map_err(|e| match e { + PoolError::Timeout(_) => io::Error::new(io::ErrorKind::TimedOut, e.to_string()), + PoolError::Backend(e) => e, + PoolError::Closed | PoolError::NoRuntimeSpecified | PoolError::PostCreateHook(_) => { + io::Error::other(e.to_string()) + } + }) + } + + async fn connect(self) -> Result, russh::Error> { + let pref = Arc::new(russh::client::Config { + inactivity_timeout: Some(std::time::Duration::from_secs(30)), + keepalive_interval: Some(std::time::Duration::from_secs(10)), + nodelay: true, + ..Default::default() + }); + + let session = if self.config.password.is_some() { + self.connect_by_password(pref).await + } else if !self.config.key_file.as_os_str().is_empty() { + self.connect_by_key(pref).await + } else { + self.connect_by_agent(pref).await + }?; + + let channel = session.channel_open_session().await?; + channel.request_subsystem(true, "sftp").await?; + Ok(channel) + } + + async fn connect_by_password( + self, + pref: Arc, + ) -> Result, russh::Error> { + let Some(password) = &self.config.password else { + return Err(russh::Error::InvalidConfig("Password not provided".to_owned())); + }; + + let mut session = + russh::client::connect(pref, (self.config.host.as_str(), self.config.port), self).await?; + + if session.authenticate_password(&self.config.user, password).await?.success() { + Ok(session) + } else { + Err(russh::Error::InvalidConfig("Password authentication failed".to_owned())) + } + } + + async fn connect_by_key( + self, + pref: Arc, + ) -> Result, russh::Error> { + let key_file = &self.config.key_file; + if key_file.as_os_str().is_empty() { + return Err(russh::Error::InvalidConfig("Key file not provided".to_owned())); + }; + + let key = Local::regular(key_file) + .read_to_string() + .await + .map_err(|e| russh::Error::InvalidConfig(format!("Failed to read key file: {e}")))?; + + let key = russh::keys::decode_secret_key(&key, self.config.key_passphrase.as_deref())?; + + let mut session = + russh::client::connect(pref, (self.config.host.as_str(), self.config.port), self).await?; + + let result = session + .authenticate_publickey( + &self.config.user, + PrivateKeyWithHashAlg::new( + Arc::new(key), + session.best_supported_rsa_hash().await?.flatten(), + ), + ) + .await?; + + if result.success() { + Ok(session) + } else { + Err(russh::Error::InvalidConfig("Public key authentication failed".to_owned())) + } + } + + async fn connect_by_agent( + self, + pref: Arc, + ) -> Result, russh::Error> { + let identity_agent = &self.config.identity_agent; + if identity_agent.as_os_str().is_empty() { + return Err(russh::Error::InvalidConfig("Identity agent not provided".to_owned())); + }; + + #[cfg(unix)] + let mut agent = russh::keys::agent::client::AgentClient::connect_uds(identity_agent).await?; + #[cfg(windows)] + let mut agent = + russh::keys::agent::client::AgentClient::connect_named_pipe(identity_agent).await?; + + let keys = agent.request_identities().await?; + if keys.is_empty() { + return Err(russh::Error::InvalidConfig("No keys found in SSH agent".to_owned())); + } + + let mut session = + russh::client::connect(pref, (self.config.host.as_str(), self.config.port), self).await?; + + for key in keys { + match session.authenticate_publickey_with(&self.config.user, key, None, &mut agent).await { + Ok(result) if result.success() => return Ok(session), + Ok(_) => {} + Err(e) => tracing::error!("Identity agent authentication error: {e}"), + } + } + + Err(russh::Error::InvalidConfig("Public key authentication via agent failed".to_owned())) + } +} diff --git a/yazi-vfs/src/provider/sftp/gate.rs b/yazi-vfs/src/provider/sftp/gate.rs index 0f10ecb5..795c41b4 100644 --- a/yazi-vfs/src/provider/sftp/gate.rs +++ b/yazi-vfs/src/provider/sftp/gate.rs @@ -1,105 +1,90 @@ -use std::{io, path::Path}; +use std::io; use yazi_config::vfs::{ProviderSftp, Vfs}; use yazi_fs::provider::{Attrs, FileBuilder}; use yazi_sftp::fs::Flags; -use yazi_shared::scheme::SchemeRef; +use yazi_shared::url::{AsUrl, Url}; -pub struct Gate { - sftp: super::Sftp, +use crate::provider::sftp::Conn; - append: bool, - attrs: Attrs, - create: bool, - create_new: bool, - read: bool, - truncate: bool, - write: bool, +#[derive(Clone, Copy, Default)] +pub struct Gate(crate::provider::Gate); + +impl From for Flags { + fn from(Gate(g): Gate) -> Self { + let mut flags = Flags::empty(); + if g.append { + flags |= Flags::APPEND; + } + if g.create { + flags |= Flags::CREATE; + } + if g.create_new { + flags |= Flags::CREATE | Flags::EXCLUDE; + } + if g.read { + flags |= Flags::READ; + } + if g.truncate { + flags |= Flags::TRUNCATE; + } + if g.write { + flags |= Flags::WRITE; + } + flags + } } impl FileBuilder for Gate { type File = yazi_sftp::fs::File; fn append(&mut self, append: bool) -> &mut Self { - self.append = append; + self.0.append(append); self } fn attrs(&mut self, attrs: Attrs) -> &mut Self { - self.attrs = attrs; + self.0.attrs(attrs); self } fn create(&mut self, create: bool) -> &mut Self { - self.create = create; + self.0.create(create); self } fn create_new(&mut self, create_new: bool) -> &mut Self { - self.create_new = create_new; + self.0.create_new(create_new); self } - async fn new(scheme: SchemeRef<'_>) -> io::Result { - let sftp: super::Sftp = match scheme { - SchemeRef::Sftp(name) => Vfs::provider::<&ProviderSftp>(name).await?.into(), - _ => Err(io::Error::new(io::ErrorKind::InvalidInput, "Not an SFTP URL"))?, + async fn open(&self, url: U) -> io::Result + where + U: AsUrl, + { + let url = url.as_url(); + let (path, (name, config)) = match url { + Url::Sftp { loc, domain } => (*loc, Vfs::provider::<&ProviderSftp>(domain).await?), + _ => Err(io::Error::new(io::ErrorKind::InvalidInput, format!("Not an SFTP URL: {url:?}")))?, }; - Ok(Self { - sftp, - - append: false, - attrs: Attrs::default(), - create: false, - create_new: false, - read: false, - truncate: false, - write: false, - }) - } - - async fn open

(&self, path: P) -> io::Result - where - P: AsRef, - { - let mut flags = Flags::empty(); - if self.append { - flags |= Flags::APPEND; - } - if self.create { - flags |= Flags::CREATE; - } - if self.create_new { - flags |= Flags::CREATE | Flags::EXCLUDE; - } - if self.read { - flags |= Flags::READ; - } - if self.truncate { - flags |= Flags::TRUNCATE; - } - if self.write { - flags |= Flags::WRITE; - } - - let attrs = super::Attrs(self.attrs).into(); - - Ok(self.sftp.op().await?.open(&path, flags, &attrs).await?) + let flags = Flags::from(*self); + let attrs = super::Attrs(self.0.attrs).into(); + Ok(Conn { name, config }.roll().await?.open(path, flags, &attrs).await?) } fn read(&mut self, read: bool) -> &mut Self { - self.read = read; + self.0.read(read); self } fn truncate(&mut self, truncate: bool) -> &mut Self { - self.truncate = truncate; + self.0.truncate(truncate); self } fn write(&mut self, write: bool) -> &mut Self { - self.write = write; + self.0.write(write); self } } diff --git a/yazi-vfs/src/provider/sftp/mod.rs b/yazi-vfs/src/provider/sftp/mod.rs index f8766775..2d9809ae 100644 --- a/yazi-vfs/src/provider/sftp/mod.rs +++ b/yazi-vfs/src/provider/sftp/mod.rs @@ -1,10 +1,10 @@ -yazi_macro::mod_flat!(gate metadata read_dir sftp); +yazi_macro::mod_flat!(conn gate metadata read_dir sftp); -pub(super) static CONN: yazi_shared::RoCell< +static CONN: yazi_shared::RoCell< parking_lot::Mutex< hashbrown::HashMap< &'static yazi_config::vfs::ProviderSftp, - &'static deadpool::managed::Pool, + &'static deadpool::managed::Pool, >, >, > = yazi_shared::RoCell::new(); diff --git a/yazi-vfs/src/provider/sftp/read_dir.rs b/yazi-vfs/src/provider/sftp/read_dir.rs index 8be0a8b4..de55a16d 100644 --- a/yazi-vfs/src/provider/sftp/read_dir.rs +++ b/yazi-vfs/src/provider/sftp/read_dir.rs @@ -1,30 +1,41 @@ -use std::{borrow::Cow, ffi::OsStr, io, path::PathBuf}; +use std::{io, sync::Arc}; use yazi_fs::provider::{DirReader, FileHolder}; +use yazi_shared::{path::PathBufDyn, strand::StrandCow, url::{UrlBuf, UrlLike}}; use super::{Cha, ChaMode}; -pub struct ReadDir(pub(super) yazi_sftp::fs::ReadDir); +pub struct ReadDir { + pub(super) dir: Arc, + pub(super) reader: yazi_sftp::fs::ReadDir, +} impl DirReader for ReadDir { type Entry = DirEntry; async fn next(&mut self) -> io::Result> { - Ok(self.0.next().await?.map(DirEntry)) + Ok(self.reader.next().await?.map(|entry| DirEntry { dir: self.dir.clone(), entry })) } } // --- Entry -pub struct DirEntry(yazi_sftp::fs::DirEntry); +pub struct DirEntry { + dir: Arc, + entry: yazi_sftp::fs::DirEntry, +} impl FileHolder for DirEntry { - fn path(&self) -> PathBuf { self.0.path() } - - fn name(&self) -> Cow<'_, OsStr> { self.0.name() } - - async fn metadata(&self) -> io::Result { Ok(Cha::try_from(&self.0)?.0) } - async fn file_type(&self) -> io::Result { - Ok(ChaMode::try_from(self.0.attrs())?.0.into()) + Ok(ChaMode::try_from(self.entry.attrs())?.0.into()) + } + + async fn metadata(&self) -> io::Result { Ok(Cha::try_from(&self.entry)?.0) } + + fn name(&self) -> StrandCow<'_> { self.entry.name().into() } + + fn path(&self) -> PathBufDyn { self.entry.path().into() } + + fn url(&self) -> UrlBuf { + self.dir.try_join(self.entry.name()).expect("entry name is a valid component of the SFTP URL") } } diff --git a/yazi-vfs/src/provider/sftp/sftp.rs b/yazi-vfs/src/provider/sftp/sftp.rs index 560b4308..b521dfe6 100644 --- a/yazi-vfs/src/provider/sftp/sftp.rs +++ b/yazi-vfs/src/provider/sftp/sftp.rs @@ -1,92 +1,82 @@ -use std::{io, path::{Path, PathBuf}, sync::Arc}; +use std::{io, path::Path, sync::Arc}; -use russh::keys::PrivateKeyWithHashAlg; use tokio::io::{AsyncWriteExt, BufReader, BufWriter}; -use yazi_config::vfs::ProviderSftp; -use yazi_fs::provider::{DirReader, FileBuilder, FileHolder, Provider, local::Local}; +use yazi_config::vfs::{ProviderSftp, Vfs}; +use yazi_fs::provider::{DirReader, FileHolder, Provider}; use yazi_sftp::fs::{Attrs, Flags}; -use yazi_shared::{scheme::SchemeRef, url::{AsUrl, UrlBuf, UrlCow}}; +use yazi_shared::{path::{AsPathDyn, PathBufDyn}, pool::InternStr, strand::StrandLike, url::{Url, UrlBuf, UrlCow, UrlLike}}; use super::Cha; +use crate::provider::sftp::Conn; #[derive(Clone, Copy)] -pub struct Sftp { +pub struct Sftp<'a> { + url: Url<'a>, + path: &'a Path, + name: &'static str, config: &'static ProviderSftp, } -impl From<(&'static str, &'static ProviderSftp)> for Sftp { - fn from((name, config): (&'static str, &'static ProviderSftp)) -> Self { Self { name, config } } -} - -impl Provider for Sftp { +impl<'a> Provider for Sftp<'a> { type File = yazi_sftp::fs::File; type Gate = super::Gate; + type Me<'b> = Sftp<'b>; type ReadDir = super::ReadDir; + type UrlCow = UrlCow<'a>; - async fn absolute<'a, U>(&self, url: &'a U) -> io::Result> - where - U: AsUrl, - { - let url = url.as_url(); - Ok(if url.is_absolute() { - url.into() - } else if let SchemeRef::Sftp(_) = url.scheme { - UrlBuf { loc: self.canonicalize(url.loc).await?.into(), scheme: url.scheme.into() }.into() - } else { - Err(io::Error::new(io::ErrorKind::InvalidInput, "Not an SFTP URL"))? + async fn absolute(&self) -> io::Result { + Ok(if self.url.is_absolute() { self.url.into() } else { self.canonicalize().await?.into() }) + } + + async fn canonicalize(&self) -> io::Result { + Ok(UrlBuf::Sftp { + loc: self.op().await?.realpath(self.path).await?.into(), + domain: self.name.intern(), }) } - async fn canonicalize

(&self, path: P) -> io::Result - where - P: AsRef, - { - Ok(self.op().await?.realpath(&path).await?) - } - - async fn casefold

(&self, path: P) -> io::Result - where - P: AsRef, - { - let path = path.as_ref(); - let Some((parent, name)) = path.parent().zip(path.file_name()) else { - return Ok(path.to_owned()); + async fn casefold(&self) -> io::Result { + let Some((parent, name)) = self.url.parent().zip(self.url.name()) else { + return Ok(self.url.to_owned()); }; - if !self.symlink_metadata(path).await?.is_link() { - return match self.canonicalize(path).await?.file_name() { - Some(name) => Ok(parent.join(name)), - None => Err(io::Error::other("Cannot get filename")), - }; + if !self.symlink_metadata().await?.is_link() { + return Ok(match self.canonicalize().await?.name() { + Some(name) => parent.try_join(name)?, + None => Err(io::Error::other("Cannot get filename"))?, + }); } - let mut it = self.read_dir(parent).await?; + let mut it = Self::new(parent).await?.read_dir().await?; let mut similar = None; while let Some(entry) = it.next().await? { let s = entry.name(); - if !s.eq_ignore_ascii_case(name) { + if !name.eq_ignore_ascii_case(&s) { continue; } else if s == name { - return Ok(entry.path()); + return Ok(entry.url()); } else if similar.is_none() { similar = Some(s.into_owned()); } } - similar.map(|n| parent.join(n)).ok_or_else(|| io::Error::from(io::ErrorKind::NotFound)) + similar + .map(|n| parent.try_join(n)) + .transpose()? + .ok_or_else(|| io::Error::from(io::ErrorKind::NotFound)) } - async fn copy(&self, from: P, to: Q, attrs: yazi_fs::provider::Attrs) -> io::Result + async fn copy

(&self, to: P, attrs: yazi_fs::provider::Attrs) -> io::Result where - P: AsRef, - Q: AsRef, + P: AsPathDyn, { + let to = to.as_path_dyn().as_os()?; let attrs = Attrs::from(super::Attrs(attrs)); let op = self.op().await?; - let from = op.open(&from, Flags::READ, &Attrs::default()).await?; - let to = op.open(&to, Flags::WRITE | Flags::CREATE | Flags::TRUNCATE, &attrs).await?; + let from = op.open(self.path, Flags::READ, &Attrs::default()).await?; + let to = op.open(to, Flags::WRITE | Flags::CREATE | Flags::TRUNCATE, &attrs).await?; let mut reader = BufReader::with_capacity(524288, from); let mut writer = BufWriter::with_capacity(524288, to); @@ -98,268 +88,95 @@ impl Provider for Sftp { Ok(written) } - async fn create_dir

(&self, path: P) -> io::Result<()> - where - P: AsRef, - { - Ok(self.op().await?.mkdir(&path, Attrs::default()).await?) + async fn create_dir(&self) -> io::Result<()> { + Ok(self.op().await?.mkdir(self.path, Attrs::default()).await?) } - async fn gate(&self) -> io::Result { - super::Gate::new(SchemeRef::Sftp(self.name)).await + async fn hard_link

(&self, to: P) -> io::Result<()> + where + P: AsPathDyn, + { + let to = to.as_path_dyn().as_os()?; + + Ok(self.op().await?.hardlink(self.path, to).await?) } - async fn hard_link(&self, original: P, link: Q) -> io::Result<()> - where - P: AsRef, - Q: AsRef, - { - Ok(self.op().await?.hardlink(&original, &link).await?) + async fn metadata(&self) -> io::Result { + let attrs = self.op().await?.stat(self.path).await?; + Ok(Cha::try_from((self.path.file_name().unwrap_or_default(), &attrs))?.0) } - async fn metadata

(&self, path: P) -> io::Result - where - P: AsRef, - { - let path = path.as_ref(); - let attrs = self.op().await?.stat(path).await?; - Ok(Cha::try_from((path.file_name().unwrap_or_default(), &attrs))?.0) + async fn new<'b>(url: Url<'b>) -> io::Result> { + match url { + Url::Regular(_) | Url::Search { .. } | Url::Archive { .. } => { + Err(io::Error::new(io::ErrorKind::InvalidInput, format!("Not a SFTP URL: {url:?}"))) + } + Url::Sftp { loc, domain } => { + let (name, config) = Vfs::provider::<&ProviderSftp>(domain).await?; + Ok(Self::Me { url, path: loc.as_path(), name, config }) + } + } } - async fn read_dir

(&self, path: P) -> io::Result - where - P: AsRef, - { - Ok(super::ReadDir(self.op().await?.read_dir(&path).await?)) + async fn read_dir(self) -> io::Result { + Ok(Self::ReadDir { + dir: Arc::new(self.url.to_owned()), + reader: self.op().await?.read_dir(self.path).await?, + }) } - async fn read_link

(&self, path: P) -> io::Result - where - P: AsRef, - { - Ok(self.op().await?.readlink(&path).await?) + async fn read_link(&self) -> io::Result { + Ok(self.op().await?.readlink(self.path).await?.into()) } - async fn remove_dir

(&self, path: P) -> io::Result<()> - where - P: AsRef, - { - Ok(self.op().await?.rmdir(&path).await?) - } + async fn remove_dir(&self) -> io::Result<()> { Ok(self.op().await?.rmdir(self.path).await?) } - async fn remove_file

(&self, path: P) -> io::Result<()> - where - P: AsRef, - { - Ok(self.op().await?.remove(&path).await?) - } + async fn remove_file(&self) -> io::Result<()> { Ok(self.op().await?.remove(self.path).await?) } - async fn rename(&self, from: P, to: Q) -> io::Result<()> + async fn rename

(&self, to: P) -> io::Result<()> where - P: AsRef, - Q: AsRef, + P: AsPathDyn, { + let to = to.as_path_dyn().as_os()?; let op = self.op().await?; - match op.rename_posix(&from, &to).await { + + match op.rename_posix(self.path, &to).await { Ok(()) => {} Err(yazi_sftp::Error::Unsupported) => { op.remove(&to).await?; - op.rename(&from, &to).await?; + op.rename(self.path, &to).await?; } Err(e) => Err(e)?, } Ok(()) } - async fn symlink(&self, original: P, link: Q, _is_dir: F) -> io::Result<()> + async fn symlink(&self, original: P, _is_dir: F) -> io::Result<()> where - P: AsRef, - Q: AsRef, + P: AsPathDyn, F: AsyncFnOnce() -> io::Result, { - Ok(self.op().await?.symlink(&original, &link).await?) + let original = original.as_path_dyn().as_os()?; + + Ok(self.op().await?.symlink(&original, self.path).await?) } - async fn symlink_metadata

(&self, path: P) -> io::Result - where - P: AsRef, - { - let path = path.as_ref(); - let attrs = self.op().await?.lstat(path).await?; - Ok(Cha::try_from((path.file_name().unwrap_or_default(), &attrs))?.0) + async fn symlink_metadata(&self) -> io::Result { + let attrs = self.op().await?.lstat(self.path).await?; + Ok(Cha::try_from((self.path.file_name().unwrap_or_default(), &attrs))?.0) } - async fn trash

(&self, _path: P) -> io::Result<()> - where - P: AsRef, - { + async fn trash(&self) -> io::Result<()> { Err(io::Error::new(io::ErrorKind::Unsupported, "Trash not supported")) } + + #[inline] + fn url(&self) -> Url<'_> { self.url } } -impl Sftp { - pub(super) async fn op(self) -> io::Result> { - use deadpool::managed::PoolError; - - let pool = *super::CONN.lock().entry(self.config).or_insert_with(|| { - Box::leak(Box::new(deadpool::managed::Pool::builder(self).max_size(5).build().unwrap())) - }); - - pool.get().await.map_err(|e| match e { - PoolError::Timeout(_) => io::Error::new(io::ErrorKind::TimedOut, e.to_string()), - PoolError::Backend(e) => e, - PoolError::Closed | PoolError::NoRuntimeSpecified | PoolError::PostCreateHook(_) => { - io::Error::other(e.to_string()) - } - }) - } -} - -impl russh::client::Handler for Sftp { - type Error = russh::Error; - - async fn check_server_key( - &mut self, - _server_public_key: &russh::keys::PublicKey, - ) -> Result { - Ok(true) - } -} - -impl deadpool::managed::Manager for Sftp { - type Error = io::Error; - type Type = yazi_sftp::Operator; - - async fn create(&self) -> Result { - let channel = self.connect().await.map_err(|e| { - io::Error::other(format!("Failed to connect to SFTP server `{}`: {e}", self.name)) - })?; - - let mut op = yazi_sftp::Operator::make(channel.into_stream()); - op.init().await?; - Ok(op) - } - - async fn recycle( - &self, - obj: &mut Self::Type, - _metrics: &deadpool::managed::Metrics, - ) -> deadpool::managed::RecycleResult { - if obj.is_closed() { - Err(deadpool::managed::RecycleError::Message("Channel closed".into())) - } else { - Ok(()) - } - } -} - -impl Sftp { - async fn connect(self) -> Result, russh::Error> { - let pref = Arc::new(russh::client::Config { - inactivity_timeout: Some(std::time::Duration::from_secs(30)), - keepalive_interval: Some(std::time::Duration::from_secs(10)), - nodelay: true, - ..Default::default() - }); - - let session = if self.config.password.is_some() { - self.connect_by_password(pref).await - } else if !self.config.key_file.as_os_str().is_empty() { - self.connect_by_key(pref).await - } else { - self.connect_by_agent(pref).await - }?; - - let channel = session.channel_open_session().await?; - channel.request_subsystem(true, "sftp").await?; - Ok(channel) - } - - async fn connect_by_password( - self, - pref: Arc, - ) -> Result, russh::Error> { - let Some(password) = &self.config.password else { - return Err(russh::Error::InvalidConfig("Password not provided".to_owned())); - }; - - let mut session = - russh::client::connect(pref, (self.config.host.as_str(), self.config.port), self).await?; - - if session.authenticate_password(&self.config.user, password).await?.success() { - Ok(session) - } else { - Err(russh::Error::InvalidConfig("Password authentication failed".to_owned())) - } - } - - async fn connect_by_key( - self, - pref: Arc, - ) -> Result, russh::Error> { - let key_file = &self.config.key_file; - if key_file.as_os_str().is_empty() { - return Err(russh::Error::InvalidConfig("Key file not provided".to_owned())); - }; - - let key = Local - .read_to_string(key_file) - .await - .map_err(|e| russh::Error::InvalidConfig(format!("Failed to read key file: {e}")))?; - - let key = russh::keys::decode_secret_key(&key, self.config.key_passphrase.as_deref())?; - - let mut session = - russh::client::connect(pref, (self.config.host.as_str(), self.config.port), self).await?; - - let result = session - .authenticate_publickey( - &self.config.user, - PrivateKeyWithHashAlg::new( - Arc::new(key), - session.best_supported_rsa_hash().await?.flatten(), - ), - ) - .await?; - - if result.success() { - Ok(session) - } else { - Err(russh::Error::InvalidConfig("Public key authentication failed".to_owned())) - } - } - - async fn connect_by_agent( - self, - pref: Arc, - ) -> Result, russh::Error> { - let identity_agent = &self.config.identity_agent; - if identity_agent.as_os_str().is_empty() { - return Err(russh::Error::InvalidConfig("Identity agent not provided".to_owned())); - }; - - #[cfg(unix)] - let mut agent = russh::keys::agent::client::AgentClient::connect_uds(identity_agent).await?; - #[cfg(windows)] - let mut agent = - russh::keys::agent::client::AgentClient::connect_named_pipe(identity_agent).await?; - - let keys = agent.request_identities().await?; - if keys.is_empty() { - return Err(russh::Error::InvalidConfig("No keys found in SSH agent".to_owned())); - } - - let mut session = - russh::client::connect(pref, (self.config.host.as_str(), self.config.port), self).await?; - - for key in keys { - match session.authenticate_publickey_with(&self.config.user, key, None, &mut agent).await { - Ok(result) if result.success() => return Ok(session), - Ok(_) => {} - Err(e) => tracing::error!("Identity agent authentication error: {e}"), - } - } - - Err(russh::Error::InvalidConfig("Public key authentication via agent failed".to_owned())) +impl<'a> Sftp<'a> { + #[inline] + pub(super) async fn op(&self) -> io::Result> { + Conn { name: self.name, config: self.config }.roll().await } } diff --git a/yazi-watcher/src/backend.rs b/yazi-watcher/src/backend.rs index 1212de9e..10f6ca4e 100644 --- a/yazi-watcher/src/backend.rs +++ b/yazi-watcher/src/backend.rs @@ -30,7 +30,7 @@ impl Backend { pub(super) fn watch(&mut self, url: impl AsUrl) -> Result<()> { let url = url.as_url(); - if let Some(path) = url.as_path() { + if let Some(path) = url.as_local() { self.local.watch(path)?; } else { self.remote.watch(url)?; @@ -41,7 +41,7 @@ impl Backend { pub(super) fn unwatch(&mut self, url: impl AsUrl) -> Result<()> { let url = url.as_url(); - if let Some(path) = url.as_path() { + if let Some(path) = url.as_local() { self.local.unwatch(path)?; } else { self.remote.unwatch(url)?; diff --git a/yazi-watcher/src/local/linked.rs b/yazi-watcher/src/local/linked.rs index e8c9afb5..44a6c4de 100644 --- a/yazi-watcher/src/local/linked.rs +++ b/yazi-watcher/src/local/linked.rs @@ -26,7 +26,7 @@ impl Linked { U: Into>, { let url: Url = url.into(); - let Some(path) = url.as_path() else { + let Some(path) = url.as_local() else { return Box::new(iter::empty()); }; @@ -38,7 +38,7 @@ impl Linked { } pub fn from_file(&self, url: Url) -> Vec { - let Some(path) = url.as_path() else { return vec![] }; + let Some(path) = url.as_local() else { return vec![] }; if let Some((parent, name)) = path.parent().zip(path.file_name()) { self.from_dir(parent).map(|p| p.join(name)).collect() } else { diff --git a/yazi-watcher/src/local/local.rs b/yazi-watcher/src/local/local.rs index fd6725e4..67896007 100644 --- a/yazi-watcher/src/local/local.rs +++ b/yazi-watcher/src/local/local.rs @@ -74,7 +74,7 @@ impl Local { continue; }; - if let Some(p) = file.url.as_path() + if let Some(p) = file.url.as_local() && !provider::local::must_case_match(p).await { ops.push(FilesOp::Deleting(parent.into(), [urn.owned()].into())); diff --git a/yazi-watcher/src/reporter.rs b/yazi-watcher/src/reporter.rs index a9a1776c..9fc16c0f 100644 --- a/yazi-watcher/src/reporter.rs +++ b/yazi-watcher/src/reporter.rs @@ -1,5 +1,5 @@ use tokio::sync::mpsc; -use yazi_shared::{path::PathLike, scheme::SchemeRef, url::{AsUrl, Url, UrlBuf, UrlCow, UrlLike}}; +use yazi_shared::{path::PathLike, scheme::SchemeKind, url::{AsUrl, Url, UrlBuf, UrlCow, UrlLike}}; use crate::{WATCHED, local::LINKED}; @@ -16,10 +16,10 @@ impl Reporter { I::Item: Into>, { for url in urls.into_iter().map(Into::into) { - match url.as_url().scheme { - SchemeRef::Regular | SchemeRef::Search(_) => self.report_local(url), - SchemeRef::Archive(_) => {} - SchemeRef::Sftp(_) => self.report_remote(url), + match url.as_url().kind() { + SchemeKind::Regular | SchemeKind::Search => self.report_local(url), + SchemeKind::Archive => {} + SchemeKind::Sftp => self.report_remote(url), } } } @@ -37,14 +37,15 @@ impl Reporter { self.local_tx.send(url.to_owned()).ok(); self.local_tx.send(parent.to_owned()).ok(); } - if name.extension() == Some("%tmp".as_ref()) { + if name.ext().is_some_and(|e| e == "%tmp") { continue; } // SFTP caches - 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(); - } + // 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(); + // } } } diff --git a/yazi-watcher/src/watched.rs b/yazi-watcher/src/watched.rs index f8fcd889..bfe322c6 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::{scheme::SchemeRef, url::{AsUrl, UrlBuf, UrlLike}}; +use yazi_shared::url::{AsUrl, UrlBuf, UrlLike}; #[derive(Debug, Default)] pub struct Watched(HashSet); @@ -22,7 +22,7 @@ impl Watched { #[inline] pub(crate) fn paths(&self) -> impl Iterator { - self.0.iter().filter_map(|u| u.as_path()) + self.0.iter().filter_map(|u| u.as_local()) } #[inline] @@ -39,16 +39,17 @@ impl Watched { let loc = l2.percent_decode(); self.0.iter().find(|u| { - if u.scheme != SchemeRef::Sftp(&domain) { - return false; - } + 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 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() + // } }) } } diff --git a/yazi-widgets/src/clipboard.rs b/yazi-widgets/src/clipboard.rs index 3554d052..8b0d20ec 100644 --- a/yazi-widgets/src/clipboard.rs +++ b/yazi-widgets/src/clipboard.rs @@ -1,5 +1,3 @@ -use std::ffi::OsString; - use parking_lot::Mutex; use yazi_shared::RoCell; @@ -7,14 +5,12 @@ pub static CLIPBOARD: RoCell = RoCell::new(); #[derive(Default)] pub struct Clipboard { - content: Mutex, + content: Mutex>, } impl Clipboard { #[cfg(unix)] - pub async fn get(&self) -> OsString { - use std::os::unix::prelude::OsStringExt; - + pub async fn get(&self) -> Vec { use tokio::process::Command; use yazi_shared::in_ssh_connection; @@ -35,26 +31,26 @@ impl Clipboard { continue; }; if output.status.success() { - return OsString::from_vec(output.stdout); + return output.stdout; } } self.content.lock().clone() } #[cfg(windows)] - pub async fn get(&self) -> OsString { - use clipboard_win::{formats, get_clipboard}; + pub async fn get(&self) -> Vec { + use clipboard_win::get_clipboard_string; - let result = tokio::task::spawn_blocking(|| get_clipboard::(formats::Unicode)); + let result = tokio::task::spawn_blocking(get_clipboard_string); if let Ok(Ok(s)) = result.await { - return s.into(); + return s.into_bytes(); } self.content.lock().clone() } #[cfg(unix)] - pub async fn set(&self, s: impl AsRef) { + pub async fn set(&self, s: impl AsRef<[u8]>) { use std::process::Stdio; use crossterm::execute; @@ -84,7 +80,7 @@ impl Clipboard { let Ok(mut child) = cmd else { continue }; let mut stdin = child.stdin.take().unwrap(); - if stdin.write_all(s.as_ref().as_encoded_bytes()).await.is_err() { + if stdin.write_all(s.as_ref()).await.is_err() { continue; } drop(stdin); @@ -96,13 +92,13 @@ impl Clipboard { } #[cfg(windows)] - pub async fn set(&self, s: impl AsRef) { - use clipboard_win::{formats, set_clipboard}; + pub async fn set(&self, s: impl AsRef<[u8]>) { + use clipboard_win::set_clipboard_string; - let s = s.as_ref().to_owned(); - *self.content.lock() = s.clone(); + let b = s.as_ref().to_owned(); + *self.content.lock() = b.clone(); - tokio::task::spawn_blocking(move || set_clipboard(formats::Unicode, s.to_string_lossy())) + tokio::task::spawn_blocking(move || set_clipboard_string(&String::from_utf8_lossy(&b))) .await .ok(); } @@ -110,8 +106,6 @@ impl Clipboard { #[cfg(unix)] mod osc52 { - use std::ffi::OsStr; - use base64::{Engine, engine::general_purpose}; #[derive(Debug)] @@ -120,8 +114,8 @@ mod osc52 { } impl SetClipboard { - pub fn new(content: &OsStr) -> Self { - Self { content: general_purpose::STANDARD.encode(content.as_encoded_bytes()) } + pub fn new(content: &[u8]) -> Self { + Self { content: general_purpose::STANDARD.encode(content) } } } diff --git a/yazi-widgets/src/input/commands/paste.rs b/yazi-widgets/src/input/commands/paste.rs index 5f795ae8..dbda56db 100644 --- a/yazi-widgets/src/input/commands/paste.rs +++ b/yazi-widgets/src/input/commands/paste.rs @@ -18,7 +18,7 @@ impl Input { } act!(insert, self, !opt.before)?; - self.type_str(&s.to_string_lossy())?; + self.type_str(&String::from_utf8_lossy(&s))?; act!(escape, self)?; succ!(render!()); }