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

Code before:
		} else if let m = entry.metadata()?
			&& m.ino() == meta.ino()
			&& m.dev() == meta.dev()
		{
			names.push(n);
		}
	}

	if names.len() == 1 {
		// No hardlink that shares the same inode
		Ok(parent.join(&names[0]))
	} else if let mut it = names.iter().enumerate().filter(|&(_, n)| n.eq_ignore_ascii_case(name))
		&& let Some((i, _)) = it.next()
		&& it.next().is_none()
	{
		// Case-insensitive match
		Ok(parent.join(&names[i]))
	} else {
		Err(io::Error::from(io::ErrorKind::NotFound))
	}
}

#[cfg(any(
	target_os = "macos",
	target_os = "netbsd",
	target_os = "openbsd",
	target_os = "freebsd"
))]
fn final_path(path: &Path) -> io::Result<PathBuf> {
	use std::{ffi::{CStr, CString, OsString}, os::{fd::{AsRawFd, FromRawFd, OwnedFd}, unix::ffi::{OsStrExt, OsStringExt}}};

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

	let cstr = CString::new(path.as_os_str().as_bytes())?;
	let fd = match unsafe { libc::open(cstr.as_ptr(), O_RDONLY | O_SYMLINK) } {
		ret if ret < 0 => return Err(io::Error::last_os_error()),
		ret => unsafe { OwnedFd::from_raw_fd(ret) },
	};

	let mut buf = [0u8; PATH_MAX as usize];
	if unsafe { libc::fcntl(fd.as_raw_fd(), F_GETPATH, buf.as_mut_ptr()) } < 0 {
		return Err(io::Error::last_os_error());
	}

	let cstr = unsafe { CStr::from_ptr(buf.as_ptr() as *const i8) };
	Ok(OsString::from_vec(cstr.to_bytes().to_vec()).into())
}

#[cfg(target_os = "windows")]
fn final_path(path: &Path) -> io::Result<PathBuf> {
	use std::{ffi::OsString, fs::File, os::windows::{ffi::OsStringExt, fs::OpenOptionsExt, io::AsRawHandle}};

Review comment (bug):
Potential file descriptor leak when `fd >= 0` but `try_from_fd()` and `path.canonicalize()` both fail. If the open succeeds (fd >= 0) but the conditions at lines 121 and 123 both fail, the function returns early at line 126 without closing the file descriptor. The file is wrapped in a `File` but never gets used after line 122 returns None and line 123 fails. Consider using a match expression on the open result similar to the Linux implementation at line 55-58, or ensure the file is properly dropped before the early return.

Code after:
		&& it.next().is_none()
	{
		// Case-insensitive match
		Ok(parent.join(&names[i]))
	} else {
		Err(io::Error::from(io::ErrorKind::NotFound))
	}
}

#[cfg(any(target_os = "netbsd", target_os = "openbsd"))]
#[allow(irrefutable_let_patterns)]
fn casefold_impl(path: PathBuf) -> io::Result<PathBuf> {
	use std::{ffi::{CString, OsStr, OsString}, fs::File, os::{fd::{AsRawFd, FromRawFd}, unix::{ffi::{OsStrExt, OsStringExt}, fs::MetadataExt}}};

	use libc::{O_NOFOLLOW, O_RDONLY};

	let cstr = CString::new(path.into_os_string().into_vec())?;
	let path = Path::new(OsStr::from_bytes(cstr.as_bytes()));
	let Some((parent, name)) = path.parent().zip(path.file_name()) else {
		return Ok(PathBuf::from(OsString::from_vec(cstr.into_bytes())));
	};

	// Fast path: if it's not a symlink
	if let fd = unsafe { libc::open(cstr.as_ptr(), O_RDONLY | O_NOFOLLOW) }
		&& fd >= 0
		&& let file = unsafe { File::from_raw_fd(fd) }
	{
		if let Some(p) = try_from_fd(file.as_raw_fd(), path) {
			return Ok(p);
		} else if let Some(n) = path.canonicalize()?.file_name() {
			return Ok(parent.join(n));
		} else {
			return Err(io::Error::other("Cannot get filename"));
		}
	};

	// Fallback: scan the directory for matching inodes
	let (meta, mut names) = (std::fs::symlink_metadata(path)?, vec![]);
	for entry in std::fs::read_dir(parent)? {
		let entry = entry?;
		let n = entry.file_name(); // TODO: use `file_name_ref()` when stabilized

		if n == name {
			return Ok(PathBuf::from(OsString::from_vec(cstr.into_bytes())));
		} else if let m = entry.metadata()?
			&& m.ino() == meta.ino()
			&& m.dev() == meta.dev()
		{
			names.push(n);
		}
	}