feat: support VFS for join(), starts_with(), and ends_with() of Url (#3094)

This commit is contained in:
三咲雅 misaki masa 2025-08-23 10:26:20 +08:00 committed by sxyazi
parent 0054cf0b87
commit c27ef58116
No known key found for this signature in database
36 changed files with 699 additions and 449 deletions

59
Cargo.lock generated
View file

@ -255,9 +255,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.9.2"
version = "2.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29"
checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d"
dependencies = [
"serde",
]
@ -343,9 +343,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.33"
version = "1.2.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ee0f8803222ba5a7e2777dd72ca451868909b1ac410621b676adf07280e9b5f"
checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc"
dependencies = [
"jobserver",
"libc",
@ -569,7 +569,7 @@ version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
dependencies = [
"bitflags 2.9.2",
"bitflags 2.9.3",
"crossterm_winapi",
"mio",
"parking_lot",
@ -585,7 +585,7 @@ version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
dependencies = [
"bitflags 2.9.2",
"bitflags 2.9.3",
"crossterm_winapi",
"derive_more",
"document-features",
@ -1148,9 +1148,9 @@ checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408"
[[package]]
name = "indexmap"
version = "2.10.0"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9"
dependencies = [
"equivalent",
"hashbrown",
@ -1169,7 +1169,7 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3"
dependencies = [
"bitflags 2.9.2",
"bitflags 2.9.3",
"inotify-sys",
"libc",
]
@ -1209,11 +1209,11 @@ dependencies = [
[[package]]
name = "io-uring"
version = "0.7.9"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4"
checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b"
dependencies = [
"bitflags 2.9.2",
"bitflags 2.9.3",
"cfg-if",
"libc",
]
@ -1331,7 +1331,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3"
dependencies = [
"bitflags 2.9.2",
"bitflags 2.9.3",
"libc",
]
@ -1553,7 +1553,7 @@ version = "8.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3"
dependencies = [
"bitflags 2.9.2",
"bitflags 2.9.3",
"fsevent-sys",
"inotify",
"kqueue",
@ -1676,7 +1676,7 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c"
dependencies = [
"bitflags 2.9.2",
"bitflags 2.9.3",
"objc2",
]
@ -1707,7 +1707,7 @@ version = "6.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0"
dependencies = [
"bitflags 2.9.2",
"bitflags 2.9.3",
"libc",
"once_cell",
"onig_sys",
@ -1824,9 +1824,9 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "percent-encoding"
version = "2.3.1"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pin-project-lite"
@ -2062,7 +2062,7 @@ version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b"
dependencies = [
"bitflags 2.9.2",
"bitflags 2.9.3",
"cassowary",
"compact_str",
"crossterm 0.28.1",
@ -2142,7 +2142,7 @@ version = "0.5.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77"
dependencies = [
"bitflags 2.9.2",
"bitflags 2.9.3",
]
[[package]]
@ -2233,7 +2233,7 @@ version = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [
"bitflags 2.9.2",
"bitflags 2.9.3",
"errno",
"libc",
"linux-raw-sys 0.4.15",
@ -2246,7 +2246,7 @@ version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8"
dependencies = [
"bitflags 2.9.2",
"bitflags 2.9.3",
"errno",
"libc",
"linux-raw-sys 0.9.4",
@ -3427,9 +3427,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
[[package]]
name = "winnow"
version = "0.7.12"
version = "0.7.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95"
checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
dependencies = [
"memchr",
]
@ -3446,7 +3446,7 @@ version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags 2.9.2",
"bitflags 2.9.3",
]
[[package]]
@ -3593,7 +3593,7 @@ name = "yazi-config"
version = "25.6.11"
dependencies = [
"anyhow",
"bitflags 2.9.2",
"bitflags 2.9.3",
"crossterm 0.29.0",
"dirs",
"globset",
@ -3620,7 +3620,6 @@ dependencies = [
"futures",
"hashbrown",
"indexmap",
"libc",
"notify",
"parking_lot",
"ratatui",
@ -3724,7 +3723,7 @@ version = "25.6.11"
dependencies = [
"anyhow",
"arc-swap",
"bitflags 2.9.2",
"bitflags 2.9.3",
"core-foundation-sys",
"dirs",
"foldhash",
@ -3738,6 +3737,7 @@ dependencies = [
"serde",
"tokio",
"tracing",
"trash",
"uzers",
"windows-sys 0.60.2",
"yazi-ffi",
@ -3758,7 +3758,7 @@ name = "yazi-parser"
version = "25.6.11"
dependencies = [
"anyhow",
"bitflags 2.9.2",
"bitflags 2.9.3",
"crossterm 0.29.0",
"hashbrown",
"mlua",
@ -3842,7 +3842,6 @@ dependencies = [
"tokio",
"tokio-util",
"tracing",
"trash",
"yazi-config",
"yazi-dds",
"yazi-fs",

View file

@ -23,7 +23,7 @@ lto = false
ansi-to-tui = "7.0.0"
anyhow = "1.0.99"
base64 = "0.22.1"
bitflags = { version = "2.9.2", features = [ "serde" ] }
bitflags = { version = "2.9.3", features = [ "serde" ] }
clap = { version = "4.5.45", features = [ "derive" ] }
core-foundation-sys = "0.8.7"
crossterm = { version = "0.29.0", features = [ "event-stream" ] }
@ -32,7 +32,7 @@ foldhash = "0.1.5"
futures = "0.3.31"
globset = "0.4.16"
hashbrown = { version = "0.15.5", features = [ "serde" ] }
indexmap = { version = "2.10.0", features = [ "serde" ] }
indexmap = { version = "2.11.0", features = [ "serde" ] }
libc = "0.2.175"
lru = "0.16.0"
mlua = { version = "0.11.2", features = [ "anyhow", "async", "error-send", "lua54", "macros", "serde" ] }

View file

@ -186,6 +186,25 @@ If the problem persists, please file a new issue and complete the issue template
}
}
async function closeUnsupportedIssue(id) {
try {
await github.rest.issues.update({
...context.repo,
issue_number: id,
state : "closed",
state_reason: "not_planned",
})
await github.rest.issues.createComment({
...context.repo,
issue_number: id,
body : `Unsupported issue template.
Either the [Bug Report](https://github.com/sxyazi/yazi/issues/new?template=bug.yml) or [Feature Request](https://github.com/sxyazi/yazi/issues/new?template=feature.yml) template should be used.`,
})
} catch (e) {
core.error(`Error closing unsupported issue: ${e.message}`)
}
}
async function main() {
const hash = await nightlyHash()
if (!hash) return
@ -206,6 +225,8 @@ If the problem persists, please file a new issue and complete the issue template
} else if (await hasLabel(id, "feature")) {
const body = featureRequestBody(creator, content)
await updateLabels(id, !!body, body)
} else if (context.payload.action === "opened") {
await closeUnsupportedIssue(id)
}
}
}

View file

@ -7,7 +7,7 @@ use scopeguard::defer;
use tokio::io::AsyncWriteExt;
use yazi_config::{YAZI, opener::OpenerRule};
use yazi_dds::Pubsub;
use yazi_fs::{File, FilesOp, max_common_root, maybe_exists, path::skip_url, paths_to_same_file, provider::{self, local::{Gate, Local}}};
use yazi_fs::{File, FilesOp, max_common_root, maybe_exists, path::skip_url, provider::{self, local::{Gate, Local}}};
use yazi_macro::{err, succ};
use yazi_parser::VoidOpt;
use yazi_proxy::{AppProxy, HIDER, TasksProxy, WATCHER};
@ -119,7 +119,7 @@ impl BulkRename {
selected[n.0].components().take(root).chain([Component::Normal(&n)]).collect(),
);
if maybe_exists(&new).await && !paths_to_same_file(&old, &new).await {
if maybe_exists(&new).await && !provider::same(&old, &new).await.unwrap_or(false) {
failed.push((o, n, anyhow!("Destination already exists")));
} else if let Err(e) = provider::rename(&old, &new).await {
failed.push((o, n, e.into()));

View file

@ -17,9 +17,7 @@ impl Actor for Follow {
let Some(file) = cx.hovered() else { succ!() };
let Some(link_to) = &file.link_to else { succ!() };
if link_to.is_absolute() {
act!(mgr:reveal, cx, link_to.clone())
} else if let Some(p) = file.url.parent_url() {
if let Some(p) = file.url.parent_url() {
act!(mgr:reveal, cx, clean_url(p.join(link_to)))
} else {
succ!()

View file

@ -1,7 +1,7 @@
use anyhow::Result;
use yazi_config::popup::{ConfirmCfg, InputCfg};
use yazi_dds::Pubsub;
use yazi_fs::{File, FilesOp, maybe_exists, ok_or_not_found, paths_to_same_file, provider, realname};
use yazi_fs::{File, FilesOp, maybe_exists, ok_or_not_found, provider, realname};
use yazi_macro::{act, err, succ};
use yazi_parser::mgr::RenameOpt;
use yazi_proxy::{ConfirmProxy, InputProxy, MgrProxy, WATCHER};
@ -48,7 +48,8 @@ impl Actor for Rename {
}
let new = UrlBuf::from(old.parent().unwrap().join(name));
if opt.force || !maybe_exists(&new).await || paths_to_same_file(&old, &new).await {
if opt.force || !maybe_exists(&new).await || provider::same(&old, &new).await.unwrap_or(false)
{
Self::r#do(tab, old, new).await.ok();
} else if ConfirmProxy::show(ConfirmCfg::overwrite(&new)).await {
Self::r#do(tab, old, new).await.ok();

View file

@ -1,9 +1,9 @@
use std::{env, fmt::Display};
use std::{env, fmt::Display, path::Path};
use anyhow::Result;
use ratatui::layout::Rect;
use tracing::warn;
use yazi_shared::{env_exists, url::UrlBuf};
use yazi_shared::env_exists;
use crate::{Emulator, SHOWN, TMUX, drivers};
@ -35,18 +35,18 @@ impl Display for Adapter {
}
impl Adapter {
pub async fn image_show(self, url: &UrlBuf, max: Rect) -> Result<Rect> {
pub async fn image_show(self, path: &Path, max: Rect) -> Result<Rect> {
if max.is_empty() {
return Ok(Rect::default());
}
match self {
Self::Kgp => drivers::Kgp::image_show(url, max).await,
Self::KgpOld => drivers::KgpOld::image_show(url, max).await,
Self::Iip => drivers::Iip::image_show(url, max).await,
Self::Sixel => drivers::Sixel::image_show(url, max).await,
Self::X11 | Self::Wayland => drivers::Ueberzug::image_show(url, max).await,
Self::Chafa => drivers::Chafa::image_show(url, max).await,
Self::Kgp => drivers::Kgp::image_show(path, max).await,
Self::KgpOld => drivers::KgpOld::image_show(path, max).await,
Self::Iip => drivers::Iip::image_show(path, max).await,
Self::Sixel => drivers::Sixel::image_show(path, max).await,
Self::X11 | Self::Wayland => drivers::Ueberzug::image_show(path, max).await,
Self::Chafa => drivers::Chafa::image_show(path, max).await,
}
}

View file

@ -1,4 +1,4 @@
use std::{fmt::Write, io::Write as ioWrite};
use std::{fmt::Write, io::Write as ioWrite, path::Path};
use anyhow::Result;
use base64::{Engine, engine::{Config, general_purpose::STANDARD}};
@ -6,15 +6,14 @@ use crossterm::{cursor::MoveTo, queue};
use image::{DynamicImage, ExtendedColorType, ImageEncoder, codecs::{jpeg::JpegEncoder, png::PngEncoder}};
use ratatui::layout::Rect;
use yazi_config::YAZI;
use yazi_shared::url::UrlBuf;
use crate::{CLOSE, Emulator, Image, START, adapter::Adapter};
pub(crate) struct Iip;
impl Iip {
pub(crate) async fn image_show(url: &UrlBuf, max: Rect) -> Result<Rect> {
let img = Image::downscale(url, max).await?;
pub(crate) async fn image_show(path: &Path, max: Rect) -> Result<Rect> {
let img = Image::downscale(path, max).await?;
let area = Image::pixel_area((img.width(), img.height()), max);
let b = Self::encode(img).await?;

View file

@ -1,12 +1,11 @@
use core::str;
use std::io::Write;
use std::{io::Write, path::Path};
use anyhow::Result;
use base64::{Engine, engine::general_purpose};
use crossterm::{cursor::MoveTo, queue};
use image::DynamicImage;
use ratatui::layout::Rect;
use yazi_shared::url::UrlBuf;
use crate::{CLOSE, ESCAPE, Emulator, START, adapter::Adapter, image::Image};
@ -313,8 +312,8 @@ static DIACRITICS: [char; 297] = [
pub(crate) struct Kgp;
impl Kgp {
pub(crate) async fn image_show(url: &UrlBuf, max: Rect) -> Result<Rect> {
let img = Image::downscale(url, max).await?;
pub(crate) async fn image_show(path: &Path, max: Rect) -> Result<Rect> {
let img = Image::downscale(path, max).await?;
let area = Image::pixel_area((img.width(), img.height()), max);
let b1 = Self::encode(img).await?;

View file

@ -1,11 +1,10 @@
use core::str;
use std::io::Write;
use std::{io::Write, path::Path};
use anyhow::Result;
use base64::{Engine, engine::general_purpose};
use image::DynamicImage;
use ratatui::layout::Rect;
use yazi_shared::url::UrlBuf;
use yazi_term::tty::TTY;
use crate::{CLOSE, ESCAPE, Emulator, Image, START, adapter::Adapter};
@ -13,8 +12,8 @@ use crate::{CLOSE, ESCAPE, Emulator, Image, START, adapter::Adapter};
pub(crate) struct KgpOld;
impl KgpOld {
pub(crate) async fn image_show(url: &UrlBuf, max: Rect) -> Result<Rect> {
let img = Image::downscale(url, max).await?;
pub(crate) async fn image_show(path: &Path, max: Rect) -> Result<Rect> {
let img = Image::downscale(path, max).await?;
let area = Image::pixel_area((img.width(), img.height()), max);
let b = Self::encode(img).await?;

View file

@ -1,4 +1,4 @@
use std::io::Write;
use std::{io::Write, path::Path};
use anyhow::{Result, bail};
use crossterm::{cursor::MoveTo, queue};
@ -6,15 +6,14 @@ use image::{DynamicImage, GenericImageView, RgbImage};
use palette::{Srgb, cast::ComponentsAs};
use quantette::{ColorSlice, PaletteSize, QuantizeOutput, wu::UIntBinner};
use ratatui::layout::Rect;
use yazi_shared::url::UrlBuf;
use crate::{CLOSE, ESCAPE, Emulator, Image, START, adapter::Adapter};
pub(crate) struct Sixel;
impl Sixel {
pub(crate) async fn image_show(url: &UrlBuf, max: Rect) -> Result<Rect> {
let img = Image::downscale(url, max).await?;
pub(crate) async fn image_show(path: &Path, max: Rect) -> Result<Rect> {
let img = Image::downscale(path, max).await?;
let area = Image::pixel_area((img.width(), img.height()), max);
let b = Self::encode(img).await?;

View file

@ -1,16 +1,17 @@
use std::path::Path;
use anyhow::Result;
use image::{DynamicImage, ExtendedColorType, ImageDecoder, ImageEncoder, ImageError, ImageFormat, ImageReader, ImageResult, Limits, codecs::{jpeg::JpegEncoder, png::PngEncoder}, imageops::FilterType, metadata::Orientation};
use image::{DynamicImage, ExtendedColorType, ImageDecoder, ImageEncoder, ImageError, ImageReader, ImageResult, Limits, codecs::{jpeg::JpegEncoder, png::PngEncoder}, imageops::FilterType, metadata::Orientation};
use ratatui::layout::Rect;
use yazi_config::YAZI;
use yazi_fs::provider;
use yazi_shared::url::UrlBuf;
use yazi_fs::provider::local::Local;
use crate::Dimension;
pub struct Image;
impl Image {
pub async fn precache(src: &UrlBuf, cache: &UrlBuf) -> Result<()> {
pub async fn precache(src: &Path, cache: &Path) -> Result<()> {
let (mut img, orientation, icc) = Self::decode_from(src).await?;
let (w, h) = Self::flip_size(orientation, (YAZI.preview.max_width, YAZI.preview.max_height));
@ -38,11 +39,11 @@ impl Image {
})
.await??;
Ok(provider::write(cache, buf).await?)
Ok(Local::write(cache, buf).await?)
}
pub(super) async fn downscale(url: &UrlBuf, rect: Rect) -> Result<DynamicImage> {
let (mut img, orientation, _) = Self::decode_from(url).await?;
pub(super) async fn downscale(path: &Path, rect: Rect) -> Result<DynamicImage> {
let (mut img, orientation, _) = Self::decode_from(path).await?;
let (w, h) = Self::flip_size(orientation, Self::max_pixel(rect));
// Fast path.
@ -96,7 +97,7 @@ impl Image {
}
}
async fn decode_from(url: &UrlBuf) -> ImageResult<(DynamicImage, Orientation, Option<Vec<u8>>)> {
async fn decode_from(path: &Path) -> ImageResult<(DynamicImage, Orientation, Option<Vec<u8>>)> {
let mut limits = Limits::no_limits();
if YAZI.tasks.image_alloc > 0 {
limits.max_alloc = Some(YAZI.tasks.image_alloc as u64);
@ -108,13 +109,11 @@ impl Image {
limits.max_image_height = Some(YAZI.tasks.image_bound[1] as u32);
}
let mut reader = ImageReader::new(provider::open(url).await?.reader_sync().await);
if let Ok(format) = ImageFormat::from_path(url) {
reader.set_format(format);
}
reader.limits(limits);
let path = path.to_owned();
tokio::task::spawn_blocking(move || {
let mut reader = ImageReader::open(path)?;
reader.limits(limits);
let mut decoder = reader.with_guessed_format()?.into_decoder()?;
let orientation = decoder.orientation().unwrap_or(Orientation::NoTransforms);
let icc = decoder.icc_profile().unwrap_or_default();

View file

@ -1,7 +1,7 @@
use std::ops::Deref;
use mlua::{AnyUserData, ExternalError, FromLua, Lua, MetaMethod, UserData, UserDataFields, UserDataMethods, UserDataRef, Value};
use yazi_shared::url::{UrlBufCov, UrlCow};
use yazi_shared::{IntoOsStr, url::{UrlBufCov, UrlCow}};
use crate::{Urn, cached_field, deprecate};
@ -124,32 +124,38 @@ impl UserData for Url {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_method("join", |_, me, other: Value| {
Ok(Self::new(match other {
Value::String(s) => me.join(s.to_str()?.as_ref()),
Value::UserData(ud) => me.join(&ud.borrow::<Self>()?.inner),
_ => Err("must be a string or a Url".into_lua_err())?,
Value::String(s) => me.join(s.as_bytes().into_os_str()?),
Value::UserData(ud) => {
let url = ud.borrow::<Self>()?;
if !me.scheme.covariant(&url.scheme) {
return Err("cannot join Urls with different schemes".into_lua_err());
}
me.join(&url.loc)
}
_ => 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.starts_with(s.to_str()?.as_ref()),
Value::UserData(ud) => me.starts_with(&ud.borrow::<Self>()?.inner),
_ => Err("must be a string or a Url".into_lua_err())?,
Value::String(s) => me.loc.starts_with(s.as_bytes().into_os_str()?),
Value::UserData(ud) => me.starts_with(&*ud.borrow::<Self>()?),
_ => 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.ends_with(s.to_str()?.as_ref()),
Value::UserData(ud) => me.ends_with(&ud.borrow::<Self>()?.inner),
_ => Err("must be a string or a Url".into_lua_err())?,
Value::String(s) => me.loc.ends_with(s.as_bytes().into_os_str()?),
Value::UserData(ud) => me.ends_with(&*ud.borrow::<Self>()?),
_ => Err("must be a string or Url".into_lua_err())?,
})
});
methods.add_method("strip_prefix", |_, me, base: Value| {
let urn = match base {
Value::String(s) => me.strip_prefix(Self::try_from(s.as_bytes().as_ref())?),
Value::UserData(ud) => me.strip_prefix(&ud.borrow::<Self>()?.inner),
_ => Err("must be a string or a Url".into_lua_err())?,
let path = match base {
Value::String(s) => me.loc.strip_prefix(s.as_bytes().into_os_str()?).ok(),
Value::UserData(ud) => me.strip_prefix(&*ud.borrow::<Self>()?).map(AsRef::as_ref),
_ => Err("must be a string or Url".into_lua_err())?,
};
Ok(urn.map(Deref::deref).map(Self::new)) // TODO: return `Urn` instead of `Url`
Ok(path.map(Self::new)) // TODO: return `Path` instead of `Url`
});
methods.add_function_mut("into_search", |_, (ud, domain): (AnyUserData, mlua::String)| {

View file

@ -33,7 +33,7 @@ impl Mgr {
}
let home = UrlBuf::from(dirs::home_dir().unwrap_or_default());
let cwd = if let Some(u) = CWD.load().strip_prefix(home) {
let cwd = if let Some(u) = CWD.load().strip_prefix(&home) {
format!("~{}{}", std::path::MAIN_SEPARATOR, u.display())
} else {
format!("{}", CWD.load().display())

View file

@ -36,8 +36,5 @@ tokio-util = { workspace = true }
tracing = { workspace = true }
unicode-width = { workspace = true }
[target."cfg(unix)".dependencies]
libc = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] }

View file

@ -38,3 +38,6 @@ windows-sys = { version = "0.60.2", features = [ "Win32_Storage_FileSystem" ] }
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation-sys = { workspace = true }
objc = { workspace = true }
[target.'cfg(not(target_os = "android"))'.dependencies]
trash = "5.2.3"

View file

@ -1,4 +1,4 @@
use std::{ffi::OsStr, fs::{FileType, Metadata}, hash::{BuildHasher, Hash, Hasher}, ops::Deref};
use std::{ffi::OsStr, fs::{FileType, Metadata}, hash::{BuildHasher, Hash, Hasher}, ops::Deref, path::PathBuf};
use anyhow::Result;
use yazi_shared::url::{Uri, UrlBuf, UrlCow, Urn, UrnBuf};
@ -9,7 +9,7 @@ use crate::{cha::Cha, provider};
pub struct File {
pub url: UrlBuf,
pub cha: Cha,
pub link_to: Option<UrlBuf>,
pub link_to: Option<PathBuf>,
}
impl Deref for File {

View file

@ -31,59 +31,6 @@ pub fn ok_or_not_found<T: Default>(result: io::Result<T>) -> io::Result<T> {
}
}
#[inline]
pub async fn paths_to_same_file(a: impl AsRef<Path>, b: impl AsRef<Path>) -> bool {
_paths_to_same_file(a.as_ref(), b.as_ref()).await.unwrap_or(false)
}
#[cfg(unix)]
async fn _paths_to_same_file(a: &Path, b: &Path) -> io::Result<bool> {
use std::os::unix::fs::MetadataExt;
let (a_, b_) = (fs::symlink_metadata(a).await?, fs::symlink_metadata(b).await?);
Ok(
a_.ino() == b_.ino()
&& a_.dev() == b_.dev()
&& fs::canonicalize(a).await? == fs::canonicalize(b).await?,
)
}
#[cfg(windows)]
async fn _paths_to_same_file(a: &Path, b: &Path) -> std::io::Result<bool> {
use std::os::windows::{ffi::OsStringExt, io::AsRawHandle};
use windows_sys::Win32::{Foundation::{HANDLE, MAX_PATH}, Storage::FileSystem::{FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, GetFinalPathNameByHandleW, VOLUME_NAME_DOS}};
async fn final_name(p: &Path) -> std::io::Result<PathBuf> {
let file = fs::OpenOptions::new()
.access_mode(0)
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT)
.open(p)
.await?;
tokio::task::spawn_blocking(move || {
let mut buf = [0u16; MAX_PATH as usize];
let len = unsafe {
GetFinalPathNameByHandleW(
file.as_raw_handle() as HANDLE,
buf.as_mut_ptr(),
buf.len() as u32,
VOLUME_NAME_DOS,
)
};
if len == 0 {
Err(std::io::Error::last_os_error())
} else {
Ok(PathBuf::from(OsString::from_wide(&buf[0..len as usize])))
}
})
.await?
}
Ok(final_name(a).await? == final_name(b).await?)
}
pub async fn realname(u: &UrlBuf) -> Option<OsString> {
let name = u.file_name()?;
if *u == provider::canonicalize(u).await.ok()? {
@ -170,7 +117,7 @@ pub fn copy_with_progress(
tokio::spawn({
let (from, to) = (from.clone(), to.clone());
async move {
tick_tx.send(_copy_with_progress(from, to, cha).await).ok();
tick_tx.send(provider::copy(&from, &to, cha).await).ok();
}
});
@ -213,54 +160,6 @@ pub fn copy_with_progress(
rx
}
async fn _copy_with_progress(from: UrlBuf, to: UrlBuf, cha: Cha) -> io::Result<u64> {
let mut ft = std::fs::FileTimes::new();
cha.atime.map(|t| ft = ft.set_accessed(t));
cha.mtime.map(|t| ft = ft.set_modified(t));
#[cfg(target_os = "macos")]
{
use std::os::macos::fs::FileTimesExt;
cha.btime.map(|t| ft = ft.set_created(t));
}
#[cfg(windows)]
{
use std::os::windows::fs::FileTimesExt;
cha.btime.map(|t| ft = ft.set_created(t));
}
#[cfg(any(target_os = "linux", target_os = "android"))]
{
use std::os::{fd::AsRawFd, unix::fs::OpenOptionsExt};
tokio::task::spawn_blocking(move || {
let mut reader = std::fs::File::open(from)?;
let mut writer = std::fs::OpenOptions::new()
.mode(cha.mode as u32) // Do not remove `as u32`, https://github.com/termux/termux-packages/pull/22481
.write(true)
.create(true)
.truncate(true)
.open(to)?;
let written = std::io::copy(&mut reader, &mut writer)?;
unsafe { libc::fchmod(writer.as_raw_fd(), cha.mode) };
writer.set_times(ft).ok();
Ok(written)
})
.await?
}
#[cfg(not(any(target_os = "linux", target_os = "android")))]
{
tokio::task::spawn_blocking(move || {
let written = std::fs::copy(from, &to)?;
std::fs::File::options().write(true).open(to).and_then(|f| f.set_times(ft)).ok();
Ok(written)
})
.await?
}
}
pub async fn remove_dir_clean(dir: &UrlBuf) {
let Ok(mut it) = provider::read_dir(dir).await else { return };

View file

@ -1 +1 @@
yazi_macro::mod_flat!(clean expand path);
yazi_macro::mod_flat!(clean expand path relative);

View file

@ -1,7 +1,6 @@
use std::{borrow::Cow, ffi::{OsStr, OsString}, future::Future, io, path::PathBuf};
use std::{borrow::Cow, ffi::{OsStr, OsString}, future::Future, io};
use anyhow::{Result, bail};
use yazi_shared::{loc::LocBuf, url::{UrlBuf, UrlCow}};
use yazi_shared::url::UrlBuf;
use crate::provider;
@ -63,50 +62,6 @@ async fn _unique_name(mut url: UrlBuf, append: bool) -> io::Result<UrlBuf> {
Ok(url)
}
pub fn url_relative_to<'a>(
from: impl Into<UrlCow<'a>>,
to: impl Into<UrlCow<'a>>,
) -> Result<UrlCow<'a>> {
url_relative_to_impl(from.into(), to.into())
}
pub fn url_relative_to_impl<'a>(from: UrlCow<'a>, to: UrlCow<'a>) -> Result<UrlCow<'a>> {
use yazi_shared::url::Component::*;
if from.is_absolute() != to.is_absolute() {
return if to.is_absolute() {
Ok(to)
} else {
bail!("Urls must be both absolute or both relative: {from:?} and {to:?}");
};
}
if from.covariant(&to) {
return Ok(UrlBuf { loc: LocBuf::zeroed("."), scheme: to.scheme().clone() }.into());
}
let (mut f_it, mut t_it) = (from.components(), to.components());
let (f_head, t_head) = loop {
match (f_it.next(), t_it.next()) {
(Some(Scheme(a)), Some(Scheme(b))) if a.covariant(b) => {}
(Some(RootDir), Some(RootDir)) => {}
(Some(Prefix(a)), Some(Prefix(b))) if a == b => {}
(Some(Scheme(_) | Prefix(_) | RootDir), _) | (_, Some(Scheme(_) | Prefix(_) | RootDir)) => {
return Ok(to);
}
(None, None) => break (None, None),
(a, b) if a != b => break (a, b),
_ => (),
}
};
let dots = f_head.into_iter().chain(f_it).map(|_| ParentDir);
let rest = t_head.into_iter().chain(t_it);
let buf: PathBuf = dots.chain(rest).collect();
Ok(UrlBuf { loc: LocBuf::zeroed(buf), scheme: to.scheme().clone() }.into())
}
#[cfg(windows)]
pub fn backslash_to_slash(p: &std::path::Path) -> Cow<'_, std::path::Path> {
let bytes = p.as_os_str().as_encoded_bytes();
@ -125,63 +80,61 @@ pub fn backslash_to_slash(p: &std::path::Path) -> Cow<'_, std::path::Path> {
for &b in rest {
out.push(if b == b'\\' { b'/' } else { b });
}
Cow::Owned(PathBuf::from(unsafe { OsString::from_encoded_bytes_unchecked(out) }))
Cow::Owned(std::path::PathBuf::from(unsafe { OsString::from_encoded_bytes_unchecked(out) }))
}
#[cfg(test)]
mod tests {
use super::url_relative_to;
use yazi_shared::url::UrlCow;
use crate::path::url_relative_to;
#[test]
fn test_path_relative_to() {
fn test_url_relative_to() {
yazi_shared::init_tests();
fn assert(from: &str, to: &str, ret: &str) {
assert_eq!(
url_relative_to(&from.parse().unwrap(), &to.parse().unwrap()).unwrap(),
ret.parse().unwrap()
);
}
#[cfg(unix)]
{
let cases = [
// Same urls
assert("", "", ".");
assert(".", ".", ".");
assert("/a", "/a", ".");
assert("regular:///", "/", ".");
assert("regular://", "regular://", ".");
assert("regular://", "search://kw/", "search://kw/.");
assert("regular:///b", "search://kw//b", "search://kw/.");
("", "", "."),
(".", ".", "."),
("/a", "/a", "."),
("regular:///", "/", "."),
("regular://", "regular://", "."),
("regular://", "search://kw/", "search://kw/."),
("regular:///b", "search://kw//b", "search://kw/."),
// Relative urls
assert("foo", "bar", "../bar");
("foo", "bar", "../bar"),
// Absolute urls
assert("/a/b/c", "/a/b", "../");
assert("/a/b", "/a/b/c", "c");
assert("/a/b/d", "/a/b/c", "../c");
assert("/a/b/c", "/a", "../../");
assert("/a/b/b", "/a/a/b", "../../a/b");
assert("regular:///a/b", "regular:///a/b/c", "c");
assert("/a/b/c/", "search://kw//a/d/", "search://kw/../../d");
assert("search://kw//a/b/c", "search://kw//a/b", "search://kw/../");
("/a/b/c", "/a/b", ".."),
("/a/b", "/a/b/c", "c"),
("/a/b/d", "/a/b/c", "../c"),
("/a/b/c", "/a", "../.."),
("/a/b/b", "/a/a/b", "../../a/b"),
("regular:///a/b", "regular:///a/b/c", "c"),
("/a/b/c/", "search://kw//a/d/", "search://kw/../../d"),
("search://kw//a/b/c", "search://kw//a/b", "search://kw/.."),
// Different schemes
assert("", "sftp://test/", "sftp://test/");
assert("a", "sftp://test/", "sftp://test/");
assert("a", "sftp://test/b", "sftp://test/b");
assert("/a", "sftp://test//b", "sftp://test//b");
assert("sftp://test//a/b", "sftp://test//a/d", "sftp://test/../d");
}
("", "sftp://test/", "sftp://test/"),
("a", "sftp://test/", "sftp://test/"),
("a", "sftp://test/b", "sftp://test/b"),
("/a", "sftp://test//b", "sftp://test//b"),
("sftp://test//a/b", "sftp://test//a/d", "sftp://test:0:0/../d"),
];
#[cfg(windows)]
{
assert(r"C:\a\b\c", r"C:\a\b", r"..\");
assert(r"C:\a\b", r"C:\a\b\c", "c");
assert(r"C:\a\b\d", r"C:\a\b\c", r"..\c");
assert(r"C:\a\b\c", r"C:\a", r"..\..\");
assert(r"C:\a\b\b", r"C:\a\a\b", r"..\..\a\b");
let cases = [
(r"C:\a\b\c", r"C:\a\b", r".."),
(r"C:\a\b", r"C:\a\b\c", "c"),
(r"C:\a\b\d", r"C:\a\b\c", r"..\c"),
(r"C:\a\b\c", r"C:\a", r"..\.."),
(r"C:\a\b\b", r"C:\a\a\b", r"..\..\a\b"),
];
for (from, to, expected) in cases {
let from: UrlCow = from.try_into().unwrap();
let to: UrlCow = to.try_into().unwrap();
assert_eq!(format!("{:?}", url_relative_to(from, to).unwrap().as_url()), expected);
}
}
}

View file

@ -0,0 +1,51 @@
use std::{borrow::Cow, path::{Path, PathBuf}};
use anyhow::{Result, bail};
use yazi_shared::{loc::LocBuf, url::{Url, UrlBuf, UrlCow}};
pub fn path_relative_to<'a>(
from: impl AsRef<Path>,
to: &'a impl AsRef<Path>,
) -> Result<Cow<'a, Path>> {
Ok(match url_relative_to(Url::regular(&from).into(), Url::regular(to).into())? {
UrlCow::Borrowed(url) => Cow::Borrowed(url.loc.as_path()),
UrlCow::Owned(url) => Cow::Owned(url.loc.into_path()),
})
}
pub(super) fn url_relative_to<'a>(from: UrlCow<'_>, to: UrlCow<'a>) -> Result<UrlCow<'a>> {
use yazi_shared::url::Component::*;
if from.is_absolute() != to.is_absolute() {
return if to.is_absolute() {
Ok(to)
} else {
bail!("Urls must be both absolute or both relative: {from:?} and {to:?}");
};
}
if from.covariant(&to) {
return Ok(UrlBuf { loc: LocBuf::zeroed("."), scheme: to.scheme().clone() }.into());
}
let (mut f_it, mut t_it) = (from.components(), to.components());
let (f_head, t_head) = loop {
match (f_it.next(), t_it.next()) {
(Some(Scheme(a)), Some(Scheme(b))) if a.covariant(b) => {}
(Some(RootDir), Some(RootDir)) => {}
(Some(Prefix(a)), Some(Prefix(b))) if a == b => {}
(Some(Scheme(_) | Prefix(_) | RootDir), _) | (_, Some(Scheme(_) | Prefix(_) | RootDir)) => {
return Ok(to);
}
(None, None) => break (None, None),
(a, b) if a != b => break (a, b),
_ => (),
}
};
let dots = f_head.into_iter().chain(f_it).map(|_| ParentDir);
let rest = t_head.into_iter().chain(t_it);
let buf: PathBuf = dots.chain(rest).collect();
Ok(UrlBuf { loc: LocBuf::zeroed(buf), scheme: to.scheme().clone() }.into())
}

View file

@ -1,90 +1,270 @@
use std::{io, path::{Path, PathBuf}};
use crate::provider::local::{Gate, ReadDir, ReadDirSync, RwFile};
use crate::{cha::Cha, provider::local::{Gate, ReadDir, ReadDirSync, RwFile}};
pub struct Local;
impl Local {
#[inline]
pub async fn canonicalize(path: impl AsRef<Path>) -> io::Result<PathBuf> {
pub fn cache<P>(_: P) -> Option<PathBuf>
where
P: AsRef<Path>,
{
None
}
#[inline]
pub async fn canonicalize<P>(path: P) -> io::Result<PathBuf>
where
P: AsRef<Path>,
{
tokio::fs::canonicalize(path).await
}
#[inline]
pub async fn create(path: impl AsRef<Path>) -> io::Result<RwFile> {
pub async fn copy<P, Q>(from: P, to: Q, cha: Cha) -> io::Result<u64>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
let from = from.as_ref().to_owned();
let to = to.as_ref().to_owned();
Self::copy_impl(from, to, cha).await
}
async fn copy_impl(from: PathBuf, to: PathBuf, cha: Cha) -> io::Result<u64> {
let mut ft = std::fs::FileTimes::new();
cha.atime.map(|t| ft = ft.set_accessed(t));
cha.mtime.map(|t| ft = ft.set_modified(t));
#[cfg(target_os = "macos")]
{
use std::os::macos::fs::FileTimesExt;
cha.btime.map(|t| ft = ft.set_created(t));
}
#[cfg(windows)]
{
use std::os::windows::fs::FileTimesExt;
cha.btime.map(|t| ft = ft.set_created(t));
}
#[cfg(any(target_os = "linux", target_os = "android"))]
{
use std::os::{fd::AsRawFd, unix::fs::OpenOptionsExt};
tokio::task::spawn_blocking(move || {
let mut reader = std::fs::File::open(from)?;
let mut writer = std::fs::OpenOptions::new()
.mode(cha.mode as u32) // Do not remove `as u32`, https://github.com/termux/termux-packages/pull/22481
.write(true)
.create(true)
.truncate(true)
.open(to)?;
let written = std::io::copy(&mut reader, &mut writer)?;
unsafe { libc::fchmod(writer.as_raw_fd(), cha.mode) };
writer.set_times(ft).ok();
Ok(written)
})
.await?
}
#[cfg(not(any(target_os = "linux", target_os = "android")))]
{
tokio::task::spawn_blocking(move || {
let written = std::fs::copy(from, &to)?;
std::fs::File::options().write(true).open(to).and_then(|f| f.set_times(ft)).ok();
Ok(written)
})
.await?
}
}
#[inline]
pub async fn create<P>(path: P) -> io::Result<RwFile>
where
P: AsRef<Path>,
{
Gate::default().write(true).create(true).truncate(true).open(path).await.map(Into::into)
}
#[inline]
pub async fn create_dir(path: impl AsRef<Path>) -> io::Result<()> {
pub async fn create_dir<P>(path: P) -> io::Result<()>
where
P: AsRef<Path>,
{
tokio::fs::create_dir(path).await
}
#[inline]
pub async fn create_dir_all(path: impl AsRef<Path>) -> io::Result<()> {
pub async fn create_dir_all<P>(path: P) -> io::Result<()>
where
P: AsRef<Path>,
{
tokio::fs::create_dir_all(path).await
}
#[inline]
pub async fn hard_link(original: impl AsRef<Path>, link: impl AsRef<Path>) -> io::Result<()> {
pub async fn hard_link<P, Q>(original: P, link: Q) -> io::Result<()>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
tokio::fs::hard_link(original, link).await
}
#[inline]
pub async fn metadata(url: impl AsRef<Path>) -> io::Result<std::fs::Metadata> {
pub async fn metadata<P>(url: P) -> io::Result<std::fs::Metadata>
where
P: AsRef<Path>,
{
tokio::fs::metadata(url).await
}
#[inline]
pub async fn open(path: impl AsRef<Path>) -> io::Result<RwFile> {
pub async fn open<P>(path: P) -> io::Result<RwFile>
where
P: AsRef<Path>,
{
Gate::default().read(true).open(path).await.map(Into::into)
}
#[inline]
pub async fn read(path: impl AsRef<Path>) -> io::Result<Vec<u8>> { tokio::fs::read(path).await }
pub async fn read<P>(path: P) -> io::Result<Vec<u8>>
where
P: AsRef<Path>,
{
tokio::fs::read(path).await
}
#[inline]
pub async fn read_dir(path: impl AsRef<Path>) -> io::Result<ReadDir> {
pub async fn read_dir<P>(path: P) -> io::Result<ReadDir>
where
P: AsRef<Path>,
{
tokio::fs::read_dir(path).await.map(Into::into)
}
#[inline]
pub fn read_dir_sync(path: impl AsRef<Path>) -> io::Result<ReadDirSync> {
pub fn read_dir_sync<P>(path: P) -> io::Result<ReadDirSync>
where
P: AsRef<Path>,
{
std::fs::read_dir(path).map(Into::into)
}
#[inline]
pub async fn read_link(url: impl AsRef<Path>) -> io::Result<PathBuf> {
pub async fn read_link<P>(url: P) -> io::Result<PathBuf>
where
P: AsRef<Path>,
{
tokio::fs::read_link(url).await
}
#[inline]
pub async fn read_to_string(path: impl AsRef<Path>) -> io::Result<String> {
pub async fn read_to_string<P>(path: P) -> io::Result<String>
where
P: AsRef<Path>,
{
tokio::fs::read_to_string(path).await
}
#[inline]
pub async fn remove_dir(path: impl AsRef<Path>) -> io::Result<()> {
pub async fn remove_dir<P>(path: P) -> io::Result<()>
where
P: AsRef<Path>,
{
tokio::fs::remove_dir(path).await
}
#[inline]
pub async fn remove_dir_all(path: impl AsRef<Path>) -> io::Result<()> {
pub async fn remove_dir_all<P>(path: P) -> io::Result<()>
where
P: AsRef<Path>,
{
tokio::fs::remove_dir_all(path).await
}
#[inline]
pub async fn remove_file(path: impl AsRef<Path>) -> io::Result<()> {
pub async fn remove_file<P>(path: P) -> io::Result<()>
where
P: AsRef<Path>,
{
tokio::fs::remove_file(path).await
}
#[inline]
pub async fn rename(from: impl AsRef<Path>, to: impl AsRef<Path>) -> io::Result<()> {
pub async fn rename<P, Q>(from: P, to: Q) -> io::Result<()>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
tokio::fs::rename(from, to).await
}
#[inline]
pub async fn symlink_dir(original: impl AsRef<Path>, link: impl AsRef<Path>) -> io::Result<()> {
pub async fn same<P, Q>(a: P, b: Q) -> io::Result<bool>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
Self::same_impl(a.as_ref(), b.as_ref()).await
}
#[cfg(unix)]
async fn same_impl(a: &Path, b: &Path) -> io::Result<bool> {
use std::os::unix::fs::MetadataExt;
let (a_, b_) = (tokio::fs::symlink_metadata(a).await?, tokio::fs::symlink_metadata(b).await?);
Ok(
a_.ino() == b_.ino()
&& a_.dev() == b_.dev()
&& tokio::fs::canonicalize(a).await? == tokio::fs::canonicalize(b).await?,
)
}
#[cfg(windows)]
async fn same_impl(a: &Path, b: &Path) -> io::Result<bool> {
use std::{ffi::OsString, os::windows::{ffi::OsStringExt, fs::OpenOptionsExt, io::AsRawHandle}};
use windows_sys::Win32::{Foundation::{HANDLE, MAX_PATH}, Storage::FileSystem::{FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, GetFinalPathNameByHandleW, VOLUME_NAME_DOS}};
async fn final_name(path: &Path) -> io::Result<PathBuf> {
let path = path.to_owned();
tokio::task::spawn_blocking(move || {
let file = std::fs::OpenOptions::new()
.access_mode(0)
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT)
.open(path)?;
let mut buf = [0u16; MAX_PATH as usize];
let len = unsafe {
GetFinalPathNameByHandleW(
file.as_raw_handle() as HANDLE,
buf.as_mut_ptr(),
buf.len() as u32,
VOLUME_NAME_DOS,
)
};
if len == 0 {
Err(io::Error::last_os_error())
} else {
Ok(PathBuf::from(OsString::from_wide(&buf[0..len as usize])))
}
})
.await?
}
Ok(final_name(a).await? == final_name(b).await?)
}
#[inline]
pub async fn symlink_dir<P, Q>(original: P, link: Q) -> io::Result<()>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
#[cfg(unix)]
{
tokio::fs::symlink(original, link).await
@ -96,7 +276,11 @@ impl Local {
}
#[inline]
pub async fn symlink_file(original: impl AsRef<Path>, link: impl AsRef<Path>) -> io::Result<()> {
pub async fn symlink_file<P, Q>(original: P, link: Q) -> io::Result<()>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
#[cfg(unix)]
{
tokio::fs::symlink(original, link).await
@ -108,17 +292,52 @@ impl Local {
}
#[inline]
pub async fn symlink_metadata(path: impl AsRef<Path>) -> io::Result<std::fs::Metadata> {
pub async fn symlink_metadata<P>(path: P) -> io::Result<std::fs::Metadata>
where
P: AsRef<Path>,
{
tokio::fs::symlink_metadata(path).await
}
#[inline]
pub fn symlink_metadata_sync(path: impl AsRef<Path>) -> io::Result<std::fs::Metadata> {
pub fn symlink_metadata_sync<P>(path: P) -> io::Result<std::fs::Metadata>
where
P: AsRef<Path>,
{
std::fs::symlink_metadata(path)
}
pub async fn trash<P>(path: P) -> io::Result<()>
where
P: AsRef<Path>,
{
let path = path.as_ref().to_owned();
tokio::task::spawn_blocking(move || {
#[cfg(target_os = "android")]
{
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported OS for trash operation"))
}
#[cfg(target_os = "macos")]
{
use trash::{TrashContext, macos::{DeleteMethod, TrashContextExtMacos}};
let mut ctx = TrashContext::default();
ctx.set_delete_method(DeleteMethod::NsFileManager);
ctx.delete(path).map_err(io::Error::other)
}
#[cfg(all(not(target_os = "macos"), not(target_os = "android")))]
{
trash::delete(path).map_err(io::Error::other)
}
})
.await?
}
#[inline]
pub async fn write(path: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> io::Result<()> {
pub async fn write<P, C>(path: P, contents: C) -> io::Result<()>
where
P: AsRef<Path>,
C: AsRef<[u8]>,
{
tokio::fs::write(path, contents).await
}
}

View file

@ -15,9 +15,4 @@ impl From<tokio::fs::File> for crate::provider::RwFile {
impl RwFile {
#[inline]
pub fn reader(self) -> tokio::io::BufReader<tokio::fs::File> { tokio::io::BufReader::new(self.0) }
#[inline]
pub async fn reader_sync(self) -> std::io::BufReader<std::fs::File> {
std::io::BufReader::new(self.0.into_std().await)
}
}

View file

@ -1,11 +1,22 @@
use std::io;
use std::{io, path::{Path, PathBuf}};
use yazi_shared::url::{Url, UrlBuf};
use crate::provider::{ReadDir, ReadDirSync, RwFile, local::Local};
use crate::{cha::Cha, provider::{ReadDir, ReadDirSync, RwFile, local::Local}};
#[inline]
pub async fn canonicalize<'a>(url: impl Into<Url<'a>>) -> io::Result<UrlBuf> {
pub fn cache<'a, U>(url: U) -> Option<PathBuf>
where
U: Into<Url<'a>>,
{
if let Some(path) = url.into().as_path() { Local::cache(path) } else { None }
}
#[inline]
pub async fn canonicalize<'a, U>(url: U) -> io::Result<UrlBuf>
where
U: Into<Url<'a>>,
{
if let Some(path) = url.into().as_path() {
Local::canonicalize(path).await.map(Into::into)
} else {
@ -14,7 +25,23 @@ pub async fn canonicalize<'a>(url: impl Into<Url<'a>>) -> io::Result<UrlBuf> {
}
#[inline]
pub async fn create<'a>(url: impl Into<Url<'a>>) -> io::Result<RwFile> {
pub async fn copy<'a, U, V>(from: U, to: V, cha: Cha) -> io::Result<u64>
where
U: Into<Url<'a>>,
V: Into<Url<'a>>,
{
if let (Some(from), Some(to)) = (from.into().as_path(), to.into().as_path()) {
Local::copy(from, to, cha).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
}
#[inline]
pub async fn create<'a, U>(url: U) -> io::Result<RwFile>
where
U: Into<Url<'a>>,
{
if let Some(path) = url.into().as_path() {
Local::create(path).await.map(Into::into)
} else {
@ -23,7 +50,10 @@ pub async fn create<'a>(url: impl Into<Url<'a>>) -> io::Result<RwFile> {
}
#[inline]
pub async fn create_dir<'a>(url: impl Into<Url<'a>>) -> io::Result<()> {
pub async fn create_dir<'a, U>(url: U) -> io::Result<()>
where
U: Into<Url<'a>>,
{
if let Some(path) = url.into().as_path() {
Local::create_dir(path).await
} else {
@ -32,7 +62,10 @@ pub async fn create_dir<'a>(url: impl Into<Url<'a>>) -> io::Result<()> {
}
#[inline]
pub async fn create_dir_all<'a>(url: impl Into<Url<'a>>) -> io::Result<()> {
pub async fn create_dir_all<'a, U>(url: U) -> io::Result<()>
where
U: Into<Url<'a>>,
{
if let Some(path) = url.into().as_path() {
Local::create_dir_all(path).await
} else {
@ -41,10 +74,11 @@ pub async fn create_dir_all<'a>(url: impl Into<Url<'a>>) -> io::Result<()> {
}
#[inline]
pub async fn hard_link<'a>(
original: impl Into<Url<'a>>,
link: impl Into<Url<'a>>,
) -> io::Result<()> {
pub async fn hard_link<'a, U, V>(original: U, link: V) -> io::Result<()>
where
U: Into<Url<'a>>,
V: Into<Url<'a>>,
{
if let (Some(original), Some(link)) = (original.into().as_path(), link.into().as_path()) {
Local::hard_link(original, link).await
} else {
@ -53,7 +87,10 @@ pub async fn hard_link<'a>(
}
#[inline]
pub async fn metadata<'a>(url: impl Into<Url<'a>>) -> io::Result<std::fs::Metadata> {
pub async fn metadata<'a, U>(url: U) -> io::Result<std::fs::Metadata>
where
U: Into<Url<'a>>,
{
if let Some(path) = url.into().as_path() {
Local::metadata(path).await
} else {
@ -62,7 +99,10 @@ pub async fn metadata<'a>(url: impl Into<Url<'a>>) -> io::Result<std::fs::Metada
}
#[inline]
pub async fn open<'a>(url: impl Into<Url<'a>>) -> io::Result<RwFile> {
pub async fn open<'a, U>(url: U) -> io::Result<RwFile>
where
U: Into<Url<'a>>,
{
if let Some(path) = url.into().as_path() {
Local::open(path).await.map(Into::into)
} else {
@ -71,7 +111,10 @@ pub async fn open<'a>(url: impl Into<Url<'a>>) -> io::Result<RwFile> {
}
#[inline]
pub async fn read_dir<'a>(url: impl Into<Url<'a>>) -> io::Result<ReadDir> {
pub async fn read_dir<'a, U>(url: U) -> io::Result<ReadDir>
where
U: Into<Url<'a>>,
{
if let Some(path) = url.into().as_path() {
Local::read_dir(path).await.map(Into::into)
} else {
@ -80,7 +123,10 @@ pub async fn read_dir<'a>(url: impl Into<Url<'a>>) -> io::Result<ReadDir> {
}
#[inline]
pub fn read_dir_sync<'a>(url: impl Into<Url<'a>>) -> io::Result<ReadDirSync> {
pub fn read_dir_sync<'a, U>(url: U) -> io::Result<ReadDirSync>
where
U: Into<Url<'a>>,
{
if let Some(path) = url.into().as_path() {
Local::read_dir_sync(path).map(Into::into)
} else {
@ -89,16 +135,22 @@ pub fn read_dir_sync<'a>(url: impl Into<Url<'a>>) -> io::Result<ReadDirSync> {
}
#[inline]
pub async fn read_link<'a>(url: impl Into<Url<'a>>) -> io::Result<UrlBuf> {
pub async fn read_link<'a, U>(url: U) -> io::Result<PathBuf>
where
U: Into<Url<'a>>,
{
if let Some(path) = url.into().as_path() {
Local::read_link(path).await.map(Into::into)
Local::read_link(path).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
}
#[inline]
pub async fn remove_dir<'a>(url: impl Into<Url<'a>>) -> io::Result<()> {
pub async fn remove_dir<'a, U>(url: U) -> io::Result<()>
where
U: Into<Url<'a>>,
{
if let Some(path) = url.into().as_path() {
Local::remove_dir(path).await
} else {
@ -107,7 +159,10 @@ pub async fn remove_dir<'a>(url: impl Into<Url<'a>>) -> io::Result<()> {
}
#[inline]
pub async fn remove_dir_all<'a>(url: impl Into<Url<'a>>) -> io::Result<()> {
pub async fn remove_dir_all<'a, U>(url: U) -> io::Result<()>
where
U: Into<Url<'a>>,
{
if let Some(path) = url.into().as_path() {
Local::remove_dir_all(path).await
} else {
@ -116,7 +171,10 @@ pub async fn remove_dir_all<'a>(url: impl Into<Url<'a>>) -> io::Result<()> {
}
#[inline]
pub async fn remove_file<'a>(url: impl Into<Url<'a>>) -> io::Result<()> {
pub async fn remove_file<'a, U>(url: U) -> io::Result<()>
where
U: Into<Url<'a>>,
{
if let Some(path) = url.into().as_path() {
Local::remove_file(path).await
} else {
@ -125,7 +183,11 @@ pub async fn remove_file<'a>(url: impl Into<Url<'a>>) -> io::Result<()> {
}
#[inline]
pub async fn rename<'a>(from: impl Into<Url<'a>>, to: impl Into<Url<'a>>) -> io::Result<()> {
pub async fn rename<'a, U, V>(from: U, to: V) -> io::Result<()>
where
U: Into<Url<'a>>,
V: Into<Url<'a>>,
{
if let (Some(from), Some(to)) = (from.into().as_path(), to.into().as_path()) {
Local::rename(from, to).await
} else {
@ -134,11 +196,24 @@ pub async fn rename<'a>(from: impl Into<Url<'a>>, to: impl Into<Url<'a>>) -> io:
}
#[inline]
pub async fn symlink_dir<'a>(
original: impl Into<Url<'a>>,
link: impl Into<Url<'a>>,
) -> io::Result<()> {
if let (Some(original), Some(link)) = (original.into().as_path(), link.into().as_path()) {
pub async fn same<'a, U, V>(a: U, b: V) -> io::Result<bool>
where
U: Into<Url<'a>>,
V: Into<Url<'a>>,
{
if let (Some(a), Some(b)) = (a.into().as_path(), b.into().as_path()) {
Local::same(a, b).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
}
#[inline]
pub async fn symlink_dir<'a, U>(original: &Path, link: U) -> io::Result<()>
where
U: Into<Url<'a>>,
{
if let Some(link) = link.into().as_path() {
Local::symlink_dir(original, link).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
@ -146,11 +221,11 @@ pub async fn symlink_dir<'a>(
}
#[inline]
pub async fn symlink_file<'a>(
original: impl Into<Url<'a>>,
link: impl Into<Url<'a>>,
) -> io::Result<()> {
if let (Some(original), Some(link)) = (original.into().as_path(), link.into().as_path()) {
pub async fn symlink_file<'a, U>(original: &Path, link: U) -> io::Result<()>
where
U: Into<Url<'a>>,
{
if let Some(link) = link.into().as_path() {
Local::symlink_file(original, link).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
@ -158,7 +233,10 @@ pub async fn symlink_file<'a>(
}
#[inline]
pub async fn symlink_metadata<'a>(url: impl Into<Url<'a>>) -> io::Result<std::fs::Metadata> {
pub async fn symlink_metadata<'a, U>(url: U) -> io::Result<std::fs::Metadata>
where
U: Into<Url<'a>>,
{
if let Some(path) = url.into().as_path() {
Local::symlink_metadata(path).await
} else {
@ -166,7 +244,11 @@ pub async fn symlink_metadata<'a>(url: impl Into<Url<'a>>) -> io::Result<std::fs
}
}
pub fn symlink_metadata_sync<'a>(url: impl Into<Url<'a>>) -> io::Result<std::fs::Metadata> {
#[inline]
pub fn symlink_metadata_sync<'a, U>(url: U) -> io::Result<std::fs::Metadata>
where
U: Into<Url<'a>>,
{
if let Some(path) = url.into().as_path() {
Local::symlink_metadata_sync(path)
} else {
@ -175,7 +257,23 @@ pub fn symlink_metadata_sync<'a>(url: impl Into<Url<'a>>) -> io::Result<std::fs:
}
#[inline]
pub async fn write<'a>(url: impl Into<Url<'a>>, contents: impl AsRef<[u8]>) -> io::Result<()> {
pub async fn trash<'a, U>(url: U) -> io::Result<()>
where
U: Into<Url<'a>>,
{
if let Some(path) = url.into().as_path() {
Local::trash(path).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
}
#[inline]
pub async fn write<'a, U, C>(url: U, contents: C) -> io::Result<()>
where
U: Into<Url<'a>>,
C: AsRef<[u8]>,
{
if let Some(path) = url.into().as_path() {
Local::write(path, contents).await
} else {

View file

@ -1,4 +1,4 @@
use crate::provider::{BufRead, BufReadSync};
use crate::provider::BufRead;
pub enum RwFile {
Local(super::local::RwFile),
@ -11,11 +11,4 @@ impl RwFile {
RwFile::Local(local) => Box::new(local.reader()),
}
}
#[inline]
pub async fn reader_sync(self) -> Box<dyn BufReadSync> {
match self {
RwFile::Local(local) => Box::new(local.reader_sync().await),
}
}
}

View file

@ -32,6 +32,3 @@ tracing = { workspace = true }
[target."cfg(unix)".dependencies]
libc = { workspace = true }
[target.'cfg(not(target_os = "android"))'.dependencies]
trash = "5.2.3"

View file

@ -1,10 +1,10 @@
use std::collections::VecDeque;
use std::{borrow::Cow, collections::VecDeque};
use anyhow::{Result, anyhow};
use anyhow::{Result, anyhow, bail};
use tokio::{io::{self, ErrorKind::{AlreadyExists, NotFound}}, sync::mpsc};
use tracing::warn;
use yazi_config::YAZI;
use yazi_fs::{SizeCalculator, cha::Cha, copy_with_progress, maybe_exists, ok_or_not_found, path::{skip_url, url_relative_to}, provider::{self, DirEntry}};
use yazi_fs::{SizeCalculator, cha::Cha, copy_with_progress, maybe_exists, ok_or_not_found, path::{path_relative_to, skip_url}, provider::{self, DirEntry}};
use yazi_shared::{Id, url::{UrlBuf, UrlCow}};
use super::{FileIn, FileInDelete, FileInHardlink, FileInLink, FileInPaste, FileInTrash};
@ -61,21 +61,23 @@ impl File {
FileIn::Link(task) => {
let cha = task.cha.unwrap();
let src = if task.resolve {
let src: Cow<_> = if task.resolve {
match provider::read_link(&task.from).await {
Ok(u) => UrlCow::from(u),
Ok(p) => p.into(),
Err(e) if e.kind() == NotFound => {
warn!("Link task partially done: {task:?}");
return Ok(self.prog.send(TaskProg::Adv(task.id, 1, cha.len))?);
}
Err(e) => Err(e)?,
}
} else if task.from.scheme.covariant(&task.to.scheme) {
task.from.loc.as_path().into()
} else {
UrlCow::from(&task.from)
bail!("Source and target must be on the same filesystem: {task:?}")
};
let src = if task.relative {
url_relative_to(provider::canonicalize(&task.to.parent_url().unwrap()).await?, src)?
path_relative_to(provider::canonicalize(&task.to.parent_url().unwrap()).await?.loc, &src)?
} else {
src
};
@ -123,21 +125,7 @@ impl File {
self.prog.send(TaskProg::Adv(task.id, 1, task.length))?
}
FileIn::Trash(task) => {
tokio::task::spawn_blocking(move || {
#[cfg(target_os = "macos")]
{
use trash::{TrashContext, macos::{DeleteMethod, TrashContextExtMacos}};
let mut ctx = TrashContext::default();
ctx.set_delete_method(DeleteMethod::NsFileManager);
ctx.delete(&task.target)?;
}
#[cfg(all(not(target_os = "macos"), not(target_os = "android")))]
{
trash::delete(&task.target)?;
}
Ok::<_, anyhow::Error>(())
})
.await??;
provider::trash(&task.target).await?;
self.prog.send(TaskProg::Adv(task.id, 1, task.length))?;
}
}

View file

@ -20,7 +20,7 @@ impl Process {
AppProxy::stop().await;
let (id, cmd) = (task.id, task.cmd.clone());
let result = super::shell(task.into());
let result = super::shell(task.into()).await;
if let Err(e) = result {
AppProxy::notify_warn(&cmd.to_string_lossy(), format!("Failed to start process: {e}"));
return self.succ(id);
@ -41,7 +41,7 @@ impl Process {
pub async fn orphan(&self, task: ProcessInOrphan) -> Result<()> {
let id = task.id;
match super::shell(task.into()) {
match super::shell(task.into()).await {
Ok(_) => self.succ(id)?,
Err(e) => {
self.prog.send(TaskProg::New(id, 0))?;
@ -60,7 +60,8 @@ impl Process {
args: task.args,
piped: true,
orphan: false,
})?;
})
.await?;
let mut stdout = BufReader::new(child.stdout.take().unwrap()).lines();
let mut stderr = BufReader::new(child.stderr.take().unwrap()).lines();

View file

@ -1,7 +1,8 @@
use std::{ffi::OsString, process::Stdio};
use std::{borrow::Cow, ffi::OsString, process::Stdio};
use anyhow::Result;
use anyhow::{Result, bail};
use tokio::process::{Child, Command};
use yazi_fs::provider;
use yazi_shared::url::UrlBuf;
pub struct ShellOpt {
@ -25,41 +26,51 @@ impl ShellOpt {
}
}
pub fn shell(opt: ShellOpt) -> Result<Child> {
#[cfg(unix)]
return Ok(unsafe {
Command::new("sh")
.arg("-c")
.stdin(opt.stdio())
.stdout(opt.stdio())
.stderr(opt.stdio())
.arg(opt.cmd)
.args(opt.args)
.current_dir(opt.cwd)
.kill_on_drop(!opt.orphan)
.pre_exec(move || {
if (opt.piped || opt.orphan) && libc::setsid() < 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
})
.spawn()?
});
pub async fn shell(opt: ShellOpt) -> Result<Child> {
tokio::task::spawn_blocking(move || {
let cwd: Cow<_> = if let Some(path) = opt.cwd.as_path() {
path.into()
} else if let Some(cache) = provider::cache(&opt.cwd) {
std::fs::create_dir_all(&cache).ok();
cache.into()
} else {
bail!("failed to determine a working directory");
};
#[cfg(windows)]
{
Ok(
#[cfg(unix)]
return Ok(unsafe {
Command::new("sh")
.arg("-c")
.stdin(opt.stdio())
.stdout(opt.stdio())
.stderr(opt.stdio())
.arg(opt.cmd)
.args(opt.args)
.current_dir(cwd)
.kill_on_drop(!opt.orphan)
.pre_exec(move || {
if (opt.piped || opt.orphan) && libc::setsid() < 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
})
.spawn()?
});
#[cfg(windows)]
return Ok(
Command::new("cmd.exe")
.raw_arg("/C")
.raw_arg(parser::parse(&opt.cmd, &opt.args))
.stdin(opt.stdio())
.stdout(opt.stdio())
.stderr(opt.stdio())
.current_dir(opt.cwd)
.current_dir(cwd)
.kill_on_drop(!opt.orphan)
.spawn()?,
)
}
);
})
.await?
}
#[cfg(windows)]

View file

@ -77,7 +77,7 @@ impl Scheduler {
let mut ongoing = self.ongoing.lock();
let id = ongoing.add(TaskKind::User, format!("Cut {} to {}", from.display(), to.display()));
if to.starts_with(&from) && to != from {
if to.starts_with(&from) && !to.covariant(&from) {
self.new_and_fail(id, "Cannot cut directory into itself").ok();
return;
}
@ -110,7 +110,7 @@ impl Scheduler {
.lock()
.add(TaskKind::User, format!("Copy {} to {}", from.display(), to.display()));
if to.starts_with(&from) && to != from {
if to.starts_with(&from) && !to.covariant(&from) {
self.new_and_fail(id, "Cannot copy directory into itself").ok();
return;
}
@ -147,7 +147,7 @@ impl Scheduler {
.lock()
.add(TaskKind::User, format!("Hardlink {} to {}", from.display(), to.display()));
if to.starts_with(&from) && to != from {
if to.starts_with(&from) && !to.covariant(&from) {
self.new_and_fail(id, "Cannot hardlink directory into itself").ok();
return;
}

View file

@ -21,7 +21,7 @@ hashbrown = { workspace = true }
memchr = "2.7.5"
ordered-float = { workspace = true }
parking_lot = { workspace = true }
percent-encoding = "2.3.1"
percent-encoding = "2.3.2"
serde = { workspace = true }
tokio = { workspace = true }

View file

@ -259,15 +259,15 @@ mod tests {
crate::init_tests();
let cases = [
// Regular
("/", "a", "regular:///a"),
("/a/b", "c", "regular:///a/c"),
("/", "a", "/a"),
("/a/b", "c", "/a/c"),
// Archive
("archive:////", "a.zip", "archive:////a.zip"),
("archive:////a.zip/b", "c", "archive:////a.zip/c"),
("archive://:2//a.zip/b", "c", "archive://:2//a.zip/c"),
("archive://:2:1//a.zip/b", "c", "archive://:2:1//a.zip/c"),
// Empty
("/a", "", "regular:///"),
("/a", "", "/"),
("archive:////a.zip", "", "archive:////"),
("archive:////a.zip/b", "", "archive:////a.zip"),
("archive://:1:1//a.zip", "", "archive:////"),

View file

@ -17,6 +17,10 @@ impl Deref for Loc<'_> {
fn deref(&self) -> &Self::Target { self.inner }
}
impl AsRef<Path> for Loc<'_> {
fn as_ref(&self) -> &Path { self.inner }
}
impl<'a> From<&'a LocBuf> for Loc<'a> {
fn from(value: &'a LocBuf) -> Self {
Self { inner: &value.inner, uri: value.uri, urn: value.urn }

View file

@ -64,11 +64,6 @@ impl AsRef<UrlBuf> for UrlBuf {
fn as_ref(&self) -> &UrlBuf { self }
}
// FIXME: remove
impl AsRef<Path> for UrlBuf {
fn as_ref(&self) -> &Path { &self.loc }
}
impl<'a> From<&'a UrlBuf> for Cow<'a, UrlBuf> {
fn from(url: &'a UrlBuf) -> Self { Cow::Borrowed(url) }
}
@ -104,16 +99,24 @@ impl UrlBuf {
pub fn display(&self) -> Display<'_> { Display::new(self) }
#[inline]
pub fn covariant(&self, other: &Self) -> bool { self.as_url().covariant(other.as_url()) }
pub fn covariant(&self, other: &Self) -> bool { self.as_url().covariant(other) }
#[inline]
pub fn parent_url(&self) -> Option<Url<'_>> { self.as_url().parent_url() }
pub fn strip_prefix(&self, base: impl AsRef<UrlBuf>) -> Option<&Urn> {
#[inline]
pub fn starts_with<'a>(&self, base: impl Into<Url<'a>>) -> bool {
self.as_url().starts_with(base)
}
#[inline]
pub fn ends_with<'a>(&self, child: impl Into<Url<'a>>) -> bool { self.as_url().ends_with(child) }
pub fn strip_prefix<'a>(&self, base: impl Into<Url<'a>>) -> Option<&Urn> {
use Scheme as S;
let base = base.as_ref();
let prefix = self.loc.strip_prefix(&base.loc).ok()?;
let base = base.into();
let prefix = self.loc.strip_prefix(base.loc).ok()?;
Some(Urn::new(match (&self.scheme, &base.scheme) {
// Same scheme
@ -268,7 +271,7 @@ mod tests {
crate::init_tests();
let cases = [
// Regular
("/a", "b/c", "regular:///a/b/c"),
("/a", "b/c", "/a/b/c"),
// Search
("search://kw//a", "b/c", "search://kw:2:2//a/b/c"),
("search://kw:2:2//a/b/c", "d/e", "search://kw:4:4//a/b/c/d/e"),
@ -300,16 +303,16 @@ mod tests {
crate::init_tests();
let cases = [
// Regular
("/a", Some("regular:///")),
("/a", Some("/")),
("/", None),
// Search
("search://kw:2:2//a/b/c", Some("search://kw:1:1//a/b")),
("search://kw:1:1//a/b", Some("search://kw//a")),
("search://kw//a", Some("regular:///")),
("search://kw//a", Some("/")),
// Archive
("archive://:2:1//a/b.zip/c/d", Some("archive://:1:1//a/b.zip/c")),
("archive://:1:1//a/b.zip/c", Some("archive:////a/b.zip")),
("archive:////a/b.zip", Some("regular:///a")),
("archive:////a/b.zip", Some("/a")),
// SFTP
("sftp://remote:1:1//a/b", Some("sftp://remote//a")),
("sftp://remote:1:1//a", Some("sftp://remote//")),
@ -335,11 +338,11 @@ mod tests {
const S: char = std::path::MAIN_SEPARATOR;
let u: UrlBuf = "/root".parse()?;
assert_eq!(format!("{u:?}"), "regular:///root");
assert_eq!(format!("{u:?}"), "/root");
let u = u.into_search("kw");
assert_eq!(format!("{u:?}"), "search://kw//root");
assert_eq!(format!("{:?}", u.parent_url().unwrap()), "regular:///");
assert_eq!(format!("{:?}", u.parent_url().unwrap()), "/");
let u = u.join("examples");
assert_eq!(format!("{u:?}"), format!("search://kw:1:1//root{S}examples"));
@ -354,7 +357,7 @@ mod tests {
assert_eq!(format!("{u:?}"), "search://kw//root");
let u = u.parent_url().unwrap();
assert_eq!(format!("{u:?}"), "regular:///");
assert_eq!(format!("{u:?}"), "/");
Ok(())
}

View file

@ -27,7 +27,7 @@ impl<'a> Encode<'a> {
percent_encode(s.as_bytes(), SET)
}
fn urn(&self) -> impl Display {
fn ports(&self) -> impl Display {
struct D<'a>(&'a Encode<'a>);
impl Display for D<'_> {
@ -64,9 +64,9 @@ impl Display for Encode<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.scheme {
Scheme::Regular => write!(f, "regular://"),
Scheme::Search(d) => write!(f, "search://{}{}/", Self::domain(d), self.urn()),
Scheme::Archive(d) => write!(f, "archive://{}{}/", Self::domain(d), self.urn()),
Scheme::Sftp(d) => write!(f, "sftp://{}{}/", Self::domain(d), self.urn()),
Scheme::Search(d) => write!(f, "search://{}{}/", Self::domain(d), self.ports()),
Scheme::Archive(d) => write!(f, "archive://{}{}/", Self::domain(d), self.ports()),
Scheme::Sftp(d) => write!(f, "sftp://{}{}/", Self::domain(d), self.ports()),
}
}
}
@ -85,8 +85,8 @@ impl<'a> From<&'a UrlBuf> for EncodeTilded<'a> {
fn from(url: &'a UrlBuf) -> Self { Self { loc: url.loc.as_loc(), scheme: &url.scheme } }
}
impl<'a> From<&'a EncodeTilded<'a>> for Encode<'a> {
fn from(value: &'a EncodeTilded<'a>) -> Self { Self::new(value.loc, value.scheme) }
impl<'a> From<&EncodeTilded<'a>> for Encode<'a> {
fn from(value: &EncodeTilded<'a>) -> Self { Self::new(value.loc, value.scheme) }
}
impl Display for EncodeTilded<'_> {
@ -96,9 +96,11 @@ impl Display for EncodeTilded<'_> {
let loc = percent_encode(self.loc.as_os_str().as_encoded_bytes(), CONTROLS);
match self.scheme {
Scheme::Regular => write!(f, "regular~://{loc}"),
Scheme::Search(d) => write!(f, "search~://{}{}/{loc}", E::domain(d), E::urn(&self.into())),
Scheme::Archive(d) => write!(f, "archive~://{}{}/{loc}", E::domain(d), E::urn(&self.into())),
Scheme::Sftp(d) => write!(f, "sftp~://{}{}/{loc}", E::domain(d), E::urn(&self.into())),
Scheme::Search(d) => write!(f, "search~://{}{}/{loc}", E::domain(d), E::ports(&self.into())),
Scheme::Archive(d) => {
write!(f, "archive~://{}{}/{loc}", E::domain(d), E::ports(&self.into()))
}
Scheme::Sftp(d) => write!(f, "sftp~://{}{}/{loc}", E::domain(d), E::ports(&self.into())),
}
}
}

View file

@ -44,7 +44,11 @@ impl Equivalent<UrlBuf> for Url<'_> {
// --- Debug
impl Debug for Url<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}{}", Encode::from(self), self.loc.display())
if self.scheme == Scheme::Regular {
write!(f, "{}", self.loc.display())
} else {
write!(f, "{}{}", Encode::from(self), self.loc.display())
}
}
}
@ -133,6 +137,18 @@ impl<'a> Url<'a> {
})
}
#[inline]
pub fn starts_with<'b>(&self, base: impl Into<Url<'b>>) -> bool {
let base: Url = base.into();
self.scheme.covariant(&base.scheme) && self.loc.starts_with(base.loc)
}
#[inline]
pub fn ends_with<'b>(&self, child: impl Into<Url<'b>>) -> bool {
let child: Url = child.into();
self.scheme.covariant(&child.scheme) && self.loc.ends_with(child.loc)
}
#[inline]
pub fn components(&self) -> Components<'_> { Components::from(self) }