Repository: sxyazi/yazi
File: yazi-fs/src/provider/local/casefold.rs (Rust)

Code before:
}

#[cfg(target_os = "windows")]
fn valid_name_case_impl(path: PathBuf) -> 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}};

	let Some(name) = path.file_name() else { return Ok(true) };

	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])).file_name() == Some(name))
	}
}


Review comment (security):
Reading the entire directory and filtering by inode could be inefficient for large directories. This creates an O(n) operation for each casefold call. Consider caching directory contents or using a more efficient lookup strategy.
```suggestion
	let mut inode_map: HashMap<(u64, u64), Vec<PathBuf>> = HashMap::new();
	for entry in std::fs::read_dir(parent)? {
		if let Ok(e) = entry {
			if let Ok(m) = e.metadata() {
				let key = (m.dev(), m.ino());
				inode_map.entry(key).or_default().push(e.path());
			}
		}
	}
	let key = (meta.dev(), meta.ino());
	let entries = inode_map.get_mut(&key);
	if let Some(entries) = entries {
		if entries.len() == 1 {
			// No hardlink that shares the same inode
			Ok(entries.remove(0))
		} else if let Some(i) = entries.iter().position(|p| p == path) {
			// Exact match
			Ok(entries.swap_remove(i))
		} else if let Some(i) = entries.iter().position(|p| p.as_os_str().eq_ignore_ascii_case(oss)) {
			// Case-insensitive match
			Ok(entries.swap_remove(i))
		} else {
			Err(io::Error::new(io::ErrorKind::NotFound, "file not found"))
		}
```

Code after:
		&& let Some(b) = p.as_os_str().as_bytes().get(..oss.len())
		&& b.eq_ignore_ascii_case(oss.as_bytes())
	{
		let mut b = p.into_os_string().into_vec();
		b.truncate(oss.len());
		return Ok(PathBuf::from(OsString::from_vec(b)));
	}

	// Fallback: scan the directory for matching inodes
	let meta = file.metadata()?;
	let mut entries: Vec<_> = std::fs::read_dir(parent)?
		.filter_map(Result::ok)
		.filter_map(|e| e.metadata().ok().map(|m| (e, m)))
		.filter(|(_, m)| m.dev() == meta.dev() && m.ino() == meta.ino())
		.map(|(e, _)| e.path())
		.collect();

	if entries.len() == 1 {
		// No hardlink that shares the same inode
		Ok(entries.remove(0))
	} else if let Some(i) = entries.iter().position(|p| p == path) {
		// Exact match
		Ok(entries.swap_remove(i))
	} else if let Some(i) = entries.iter().position(|p| p.as_os_str().eq_ignore_ascii_case(oss)) {
		// Case-insensitive match
		Ok(entries.swap_remove(i))
	} else {
		Err(io::Error::new(io::ErrorKind::NotFound, "file not found"))
	}
}

#[cfg(target_os = "windows")]
fn casefold_impl(path: PathBuf) -> io::Result<PathBuf> {
	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}};

	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,
		)
	};