Skip to content

Commit c2a94da

Browse files
authored
feat: casefold (#3114)
1 parent 12172b7 commit c2a94da

File tree

8 files changed

+109
-136
lines changed

8 files changed

+109
-136
lines changed

cspell.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"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"],"version":"0.2"}
1+
{"version":"0.2","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"]}

yazi-actor/src/mgr/create.rs

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
use anyhow::Result;
1+
use anyhow::{Result, bail};
22
use yazi_config::popup::{ConfirmCfg, InputCfg};
3-
use yazi_fs::{File, FilesOp, maybe_exists, ok_or_not_found, provider, realname};
3+
use yazi_fs::{File, FilesOp, maybe_exists, ok_or_not_found, provider};
44
use yazi_macro::succ;
55
use yazi_parser::mgr::CreateOpt;
66
use yazi_proxy::{ConfirmProxy, InputProxy, MgrProxy};
7-
use yazi_shared::{event::Data, url::{UrlBuf, UrnBuf}};
7+
use yazi_shared::{event::Data, url::UrlBuf};
88
use yazi_watcher::WATCHER;
99

1010
use crate::{Actor, Ctx};
@@ -42,25 +42,32 @@ impl Actor for Create {
4242

4343
impl Create {
4444
async fn r#do(new: UrlBuf, dir: bool) -> Result<()> {
45-
let Some(parent) = new.parent_url() else { return Ok(()) };
4645
let _permit = WATCHER.acquire().await.unwrap();
4746

4847
if dir {
4948
provider::create_dir_all(&new).await?;
50-
} else if let Some(real) = realname(&new).await {
49+
} else if let Ok(real) = provider::casefold(&new).await
50+
&& let Some((parent, urn)) = real.pair()
51+
{
5152
ok_or_not_found(provider::remove_file(&new).await)?;
52-
FilesOp::Deleting(parent.to_owned(), [UrnBuf::from(real)].into()).emit();
53+
FilesOp::Deleting(parent.into(), [urn].into()).emit();
5354
provider::create(&new).await?;
54-
} else {
55+
} else if let Some(parent) = new.parent_url() {
5556
provider::create_dir_all(&parent).await.ok();
5657
ok_or_not_found(provider::remove_file(&new).await)?;
5758
provider::create(&new).await?;
59+
} else {
60+
bail!("Cannot create file at root");
5861
}
5962

60-
if let Ok(f) = File::new(&new).await {
61-
FilesOp::Upserting(parent.into(), [(f.urn().to_owned(), f)].into()).emit();
62-
MgrProxy::reveal(&new)
63+
if let Ok(real) = provider::casefold(&new).await
64+
&& let Some((parent, urn)) = real.pair()
65+
{
66+
let file = File::new(&real).await?;
67+
FilesOp::Upserting(parent.into(), [(urn, file)].into()).emit();
68+
MgrProxy::reveal(&real);
6369
}
70+
6471
Ok(())
6572
}
6673
}

yazi-actor/src/mgr/rename.rs

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
use anyhow::Result;
22
use yazi_config::popup::{ConfirmCfg, InputCfg};
33
use yazi_dds::Pubsub;
4-
use yazi_fs::{File, FilesOp, maybe_exists, ok_or_not_found, provider, realname};
4+
use yazi_fs::{File, FilesOp, maybe_exists, ok_or_not_found, provider};
55
use yazi_macro::{act, err, succ};
66
use yazi_parser::mgr::RenameOpt;
77
use yazi_proxy::{ConfirmProxy, InputProxy, MgrProxy};
8-
use yazi_shared::{Id, event::Data, url::{UrlBuf, UrnBuf}};
8+
use yazi_shared::{Id, event::Data, url::UrlBuf};
99
use yazi_watcher::WATCHER;
1010

1111
use crate::{Actor, Ctx};
@@ -61,24 +61,29 @@ impl Actor for Rename {
6161

6262
impl Rename {
6363
async fn r#do(tab: Id, old: UrlBuf, new: UrlBuf) -> Result<()> {
64-
let Some((p_old, n_old)) = old.pair() else { return Ok(()) };
65-
let Some((p_new, n_new)) = new.pair() else { return Ok(()) };
64+
let Some((old_p, old_n)) = old.pair() else { return Ok(()) };
65+
let Some(_) = new.pair() else { return Ok(()) };
6666
let _permit = WATCHER.acquire().await.unwrap();
6767

68-
let overwritten = realname(&new).await;
68+
let overwritten = provider::casefold(&new).await;
6969
provider::rename(&old, &new).await?;
7070

71-
if let Some(o) = overwritten {
72-
ok_or_not_found(provider::rename(&p_new.join(&o), &new).await)?;
73-
FilesOp::Deleting(p_new.to_owned(), [UrnBuf::from(o)].into()).emit();
71+
if let Ok(u) = overwritten
72+
&& let Some((parent, urn)) = u.pair()
73+
{
74+
ok_or_not_found(provider::rename(&u, &new).await)?;
75+
FilesOp::Deleting(parent.to_owned(), [urn].into()).emit();
7476
}
7577

78+
let new = provider::casefold(&new).await?;
79+
let Some((new_p, new_n)) = new.pair() else { return Ok(()) };
80+
7681
let file = File::new(&new).await?;
77-
if p_new == p_old {
78-
FilesOp::Upserting(p_old.into(), [(n_old, file)].into()).emit();
82+
if new_p == old_p {
83+
FilesOp::Upserting(old_p.into(), [(old_n, file)].into()).emit();
7984
} else {
80-
FilesOp::Deleting(p_old.into(), [n_old].into()).emit();
81-
FilesOp::Upserting(p_new.into(), [(n_new, file)].into()).emit();
85+
FilesOp::Deleting(old_p.into(), [old_n].into()).emit();
86+
FilesOp::Upserting(new_p.into(), [(new_n, file)].into()).emit();
8287
}
8388

8489
MgrProxy::reveal(&new);

yazi-fs/src/fns.rs

Lines changed: 4 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
// FIXME: VFS
22

3-
use std::{borrow::Cow, ffi::{OsStr, OsString}, path::{Path, PathBuf}};
4-
5-
use anyhow::{Result, bail};
6-
use hashbrown::{HashMap, HashSet};
7-
use tokio::{fs, io, select, sync::{mpsc, oneshot}, time};
3+
use anyhow::Result;
4+
use tokio::{io, select, sync::{mpsc, oneshot}, time};
85
use yazi_shared::url::{Component, Url, UrlBuf};
96

10-
use crate::{cha::Cha, provider::{self, local::Local}};
7+
use crate::{cha::Cha, provider};
118

129
#[inline]
1310
pub async fn maybe_exists<'a>(url: impl Into<Url<'a>>) -> bool {
@@ -31,81 +28,6 @@ pub fn ok_or_not_found<T: Default>(result: io::Result<T>) -> io::Result<T> {
3128
}
3229
}
3330

34-
pub async fn realname(url: &UrlBuf) -> Option<OsString> {
35-
let (path, name) = (url.as_path()?, url.name()?);
36-
if path == Local::canonicalize(path).await.ok()? {
37-
return None;
38-
}
39-
40-
realname_unchecked(path, &mut HashMap::new())
41-
.await
42-
.ok()
43-
.filter(|s| s != name)
44-
.map(|s| s.into_owned())
45-
}
46-
47-
#[cfg(unix)]
48-
#[tokio::test]
49-
async fn test_realname_unchecked() -> Result<()> {
50-
use crate::provider::local::Local;
51-
52-
Local::remove_dir_all("/tmp/issue-1173").await.ok();
53-
Local::create_dir_all("/tmp/issue-1173/real-dir").await?;
54-
Local::create("/tmp/issue-1173/A").await?;
55-
Local::create("/tmp/issue-1173/b").await?;
56-
Local::create("/tmp/issue-1173/real-dir/C").await?;
57-
Local::symlink_file("/tmp/issue-1173/b", "/tmp/issue-1173/D").await?;
58-
Local::symlink_dir("real-dir", "/tmp/issue-1173/link-dir").await?;
59-
60-
let c = &mut HashMap::new();
61-
async fn check(a: &str, b: &str, c: &mut HashMap<PathBuf, HashSet<OsString>>) {
62-
assert_eq!(realname_unchecked(Path::new(a), c).await.ok(), Some(OsStr::new(b).into()));
63-
}
64-
65-
check("/tmp/issue-1173/a", "A", c).await;
66-
check("/tmp/issue-1173/A", "A", c).await;
67-
68-
check("/tmp/issue-1173/b", "b", c).await;
69-
check("/tmp/issue-1173/B", "b", c).await;
70-
71-
check("/tmp/issue-1173/link-dir/c", "C", c).await;
72-
check("/tmp/issue-1173/link-dir/C", "C", c).await;
73-
74-
check("/tmp/issue-1173/d", "D", c).await;
75-
check("/tmp/issue-1173/D", "D", c).await;
76-
Ok(())
77-
}
78-
79-
// realpath(3) without resolving symlinks. This is useful for case-insensitive
80-
// filesystems.
81-
//
82-
// Make sure the file of the path exists.
83-
pub async fn realname_unchecked<'a>(
84-
path: &'a Path,
85-
cached: &'a mut HashMap<PathBuf, HashSet<OsString>>,
86-
) -> Result<Cow<'a, OsStr>> {
87-
let Some(name) = path.file_name() else { bail!("no filename") };
88-
let Some(parent) = path.parent() else { return Ok(Cow::Borrowed(name)) };
89-
90-
if !cached.contains_key(parent) {
91-
let mut set = HashSet::new();
92-
let mut it = fs::read_dir(parent).await?;
93-
while let Some(entry) = it.next_entry().await? {
94-
set.insert(entry.file_name());
95-
}
96-
cached.insert(parent.to_owned(), set);
97-
}
98-
99-
let c = &cached[parent];
100-
if c.contains(name) {
101-
Ok(Cow::Borrowed(name))
102-
} else if let Some(n) = c.iter().find(|&n| n.eq_ignore_ascii_case(name)) {
103-
Ok(Cow::Borrowed(n))
104-
} else {
105-
bail!("no such file")
106-
}
107-
}
108-
10931
pub fn copy_with_progress(
11032
from: &UrlBuf,
11133
to: &UrlBuf,
@@ -270,7 +192,7 @@ pub fn max_common_root(urls: &[UrlBuf]) -> usize {
270192
#[test]
271193
fn test_max_common_root() {
272194
fn assert(input: &[&str], expected: &str) {
273-
use std::str::FromStr;
195+
use std::{ffi::OsStr, str::FromStr};
274196
let urls: Vec<_> = input.iter().copied().map(UrlBuf::from_str).collect::<Result<_>>().unwrap();
275197

276198
let mut comp = urls[0].components();
Lines changed: 53 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
use std::{io, path::{Path, PathBuf}};
22

3-
pub async fn valid_name_case(path: impl AsRef<Path>) -> io::Result<bool> {
3+
#[inline]
4+
pub async fn casefold(path: impl AsRef<Path>) -> io::Result<PathBuf> {
45
let path = path.as_ref().to_owned();
5-
tokio::task::spawn_blocking(move || valid_name_case_impl(path)).await?
6+
tokio::task::spawn_blocking(move || casefold_impl(path)).await?
7+
}
8+
9+
#[inline]
10+
pub async fn must_case_match(path: impl AsRef<Path>) -> bool {
11+
let path = path.as_ref();
12+
casefold(path).await.is_ok_and(|p| p == path)
613
}
714

815
#[cfg(any(
@@ -11,16 +18,12 @@ pub async fn valid_name_case(path: impl AsRef<Path>) -> io::Result<bool> {
1118
target_os = "openbsd",
1219
target_os = "freebsd"
1320
))]
14-
fn valid_name_case_impl(path: PathBuf) -> io::Result<bool> {
15-
use std::{ffi::{CStr, CString, OsStr}, os::{fd::{AsRawFd, FromRawFd, OwnedFd}, unix::ffi::OsStrExt}};
21+
fn casefold_impl(path: PathBuf) -> io::Result<PathBuf> {
22+
use std::{ffi::{CStr, CString, OsString}, os::{fd::{AsRawFd, FromRawFd, OwnedFd}, unix::ffi::OsStringExt}};
1623

1724
use libc::{F_GETPATH, O_RDONLY, O_SYMLINK, PATH_MAX};
1825

19-
let cstr = CString::new(path.into_os_string().into_encoded_bytes())?;
20-
let Some(name) = Path::new(OsStr::from_bytes(cstr.as_bytes())).file_name() else {
21-
return Ok(true);
22-
};
23-
26+
let cstr = CString::new(path.into_os_string().into_vec())?;
2427
let fd = match unsafe { libc::open(cstr.as_ptr(), O_RDONLY | O_SYMLINK) } {
2528
ret if ret < 0 => return Err(io::Error::last_os_error()),
2629
ret => unsafe { OwnedFd::from_raw_fd(ret) },
@@ -31,43 +34,67 @@ fn valid_name_case_impl(path: PathBuf) -> io::Result<bool> {
3134
return Err(io::Error::last_os_error());
3235
}
3336

34-
Ok(
35-
unsafe { CStr::from_ptr(buf.as_ptr() as *const i8) }
36-
.to_bytes()
37-
.ends_with(name.as_encoded_bytes()),
38-
)
37+
let cstr = unsafe { CStr::from_ptr(buf.as_ptr() as *const i8) };
38+
Ok(OsString::from_vec(cstr.to_bytes().to_vec()).into())
3939
}
4040

4141
#[cfg(any(target_os = "linux", target_os = "android"))]
42-
fn valid_name_case_impl(path: PathBuf) -> io::Result<bool> {
43-
use std::{ffi::{CString, OsStr}, fs::File, os::{fd::{AsRawFd, FromRawFd}, unix::ffi::OsStrExt}};
42+
fn casefold_impl(path: PathBuf) -> io::Result<PathBuf> {
43+
use std::{ffi::{CString, OsStr, OsString}, fs::File, os::{fd::{AsRawFd, FromRawFd}, unix::{ffi::{OsStrExt, OsStringExt}, fs::MetadataExt}}};
4444

4545
use libc::{O_NOFOLLOW, O_PATH};
4646

47-
let cstr = CString::new(path.into_os_string().into_encoded_bytes())?;
47+
let cstr = CString::new(path.into_os_string().into_vec())?;
4848
let path = Path::new(OsStr::from_bytes(cstr.as_bytes()));
49-
let Some(name) = path.file_name() else { return Ok(true) };
49+
let Some(parent) = path.parent() else {
50+
return Ok(PathBuf::from(OsString::from_vec(cstr.into_bytes())));
51+
};
5052

5153
let file = match unsafe { libc::open(cstr.as_ptr(), O_PATH | O_NOFOLLOW) } {
5254
ret if ret < 0 => return Err(io::Error::last_os_error()),
5355
ret => unsafe { File::from_raw_fd(ret) },
5456
};
5557

56-
Ok(if file.metadata()?.is_symlink() {
57-
std::fs::read_link(format!("/proc/self/fd/{}", file.as_raw_fd()))?.starts_with(path)
58+
// Fast path: if the `/proc/self/fd/N` matches
59+
let oss = path.as_os_str();
60+
if let Ok(p) = std::fs::read_link(format!("/proc/self/fd/{}", file.as_raw_fd()))
61+
&& let Some(b) = p.as_os_str().as_bytes().get(..oss.len())
62+
&& b.eq_ignore_ascii_case(oss.as_bytes())
63+
{
64+
let mut b = p.into_os_string().into_vec();
65+
b.truncate(oss.len());
66+
return Ok(PathBuf::from(OsString::from_vec(b)));
67+
}
68+
69+
// Fallback: scan the directory for matching inodes
70+
let meta = file.metadata()?;
71+
let mut entries: Vec<_> = std::fs::read_dir(parent)?
72+
.filter_map(Result::ok)
73+
.filter_map(|e| e.metadata().ok().map(|m| (e, m)))
74+
.filter(|(_, m)| m.dev() == meta.dev() && m.ino() == meta.ino())
75+
.map(|(e, _)| e.path())
76+
.collect();
77+
78+
if entries.len() == 1 {
79+
// No hardlink that shares the same inode
80+
Ok(entries.remove(0))
81+
} else if let Some(i) = entries.iter().position(|p| p == path) {
82+
// Exact match
83+
Ok(entries.swap_remove(i))
84+
} else if let Some(i) = entries.iter().position(|p| p.as_os_str().eq_ignore_ascii_case(oss)) {
85+
// Case-insensitive match
86+
Ok(entries.swap_remove(i))
5887
} else {
59-
std::fs::canonicalize(path)?.file_name() == Some(name)
60-
})
88+
Err(io::Error::new(io::ErrorKind::NotFound, "file not found"))
89+
}
6190
}
6291

6392
#[cfg(target_os = "windows")]
64-
fn valid_name_case_impl(path: PathBuf) -> io::Result<bool> {
93+
fn casefold_impl(path: PathBuf) -> io::Result<PathBuf> {
6594
use std::{ffi::OsString, os::windows::{ffi::OsStringExt, fs::OpenOptionsExt, io::AsRawHandle}};
6695

6796
use windows_sys::Win32::{Foundation::{HANDLE, MAX_PATH}, Storage::FileSystem::{FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, GetFinalPathNameByHandleW, VOLUME_NAME_DOS}};
6897

69-
let Some(name) = path.file_name() else { return Ok(true) };
70-
7198
let file = std::fs::OpenOptions::new()
7299
.access_mode(0)
73100
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT)
@@ -86,6 +113,6 @@ fn valid_name_case_impl(path: PathBuf) -> io::Result<bool> {
86113
if len == 0 {
87114
Err(io::Error::last_os_error())
88115
} else {
89-
Ok(PathBuf::from(OsString::from_wide(&buf[0..len as usize])).file_name() == Some(name))
116+
Ok(PathBuf::from(OsString::from_wide(&buf[0..len as usize])))
90117
}
91118
}

yazi-fs/src/provider/local/local.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,11 @@ impl Local {
114114
}
115115

116116
#[inline]
117-
pub async fn metadata<P>(url: P) -> io::Result<std::fs::Metadata>
117+
pub async fn metadata<P>(path: P) -> io::Result<std::fs::Metadata>
118118
where
119119
P: AsRef<Path>,
120120
{
121-
tokio::fs::metadata(url).await
121+
tokio::fs::metadata(path).await
122122
}
123123

124124
#[inline]
@@ -154,11 +154,11 @@ impl Local {
154154
}
155155

156156
#[inline]
157-
pub async fn read_link<P>(url: P) -> io::Result<PathBuf>
157+
pub async fn read_link<P>(path: P) -> io::Result<PathBuf>
158158
where
159159
P: AsRef<Path>,
160160
{
161-
tokio::fs::read_link(url).await
161+
tokio::fs::read_link(path).await
162162
}
163163

164164
#[inline]

yazi-fs/src/provider/provider.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,18 @@ where
2424
}
2525
}
2626

27+
#[inline]
28+
pub async fn casefold<'a, U>(url: U) -> io::Result<UrlBuf>
29+
where
30+
U: Into<Url<'a>>,
31+
{
32+
if let Some(path) = url.into().as_path() {
33+
local::casefold(path).await.map(Into::into)
34+
} else {
35+
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
36+
}
37+
}
38+
2739
#[inline]
2840
pub async fn copy<'a, U, V>(from: U, to: V, cha: Cha) -> io::Result<u64>
2941
where

0 commit comments

Comments
 (0)