use std::fs;
use std::path::Path;
pub(super) fn one_entry(a: &Path, b: &Path) -> bool {
a != b && same_file(a, b) && case_only(a, b)
}
fn case_only(mut a: &Path, mut b: &Path) -> bool {
while a != b {
match (a.file_name(), b.file_name(), a.parent(), b.parent()) {
(Some(x), Some(y), Some(next_a), Some(next_b)) => {
if x != y && (!x.eq_ignore_ascii_case(y) || (listed(a) && listed(b))) {
return false;
}
(a, b) = (next_a, next_b);
}
_ => return false,
}
}
true
}
pub(super) fn has_exact_entry(dir: &Path, name: &str) -> bool {
fs::read_dir(dir).is_ok_and(|entries| {
entries
.filter_map(Result::ok)
.any(|entry| entry.file_name() == std::ffi::OsStr::new(name))
})
}
fn listed(path: &Path) -> bool {
let (Some(dir), Some(name)) = (path.parent(), path.file_name()) else {
return true;
};
fs::read_dir(dir).map_or(true, |entries| {
entries
.filter_map(Result::ok)
.any(|entry| entry.file_name() == name)
})
}
fn same_file(a: &Path, b: &Path) -> bool {
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt as _;
match (fs::metadata(a), fs::metadata(b)) {
(Ok(x), Ok(y)) => x.dev() == y.dev() && x.ino() == y.ino(),
_ => false,
}
}
#[cfg(not(unix))]
{
match (fs::canonicalize(a), fs::canonicalize(b)) {
(Ok(x), Ok(y)) => x == y,
_ => false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tmp() -> tempfile::TempDir {
tempfile::TempDir::new().unwrap()
}
#[test]
fn two_spellings_are_one_entry_only_on_a_case_insensitive_host() {
let dir = tmp();
let upper = dir.path().join("Foo.rs");
fs::write(&upper, "x").unwrap();
let lower = dir.path().join("foo.rs");
assert_eq!(one_entry(&upper, &lower), lower.exists());
assert_eq!(one_entry(&lower, &upper), lower.exists());
}
#[test]
fn two_spellings_of_a_directory_follow_the_same_rule() {
let dir = tmp();
let upper = dir.path().join("Widgets");
fs::create_dir(&upper).unwrap();
let lower = dir.path().join("widgets");
assert_eq!(one_entry(&upper, &lower), lower.exists());
}
#[test]
fn a_hard_link_is_two_entries() {
let dir = tmp();
let upper = dir.path().join("Foo.rs");
fs::write(&upper, "x").unwrap();
let lower = dir.path().join("foo.rs");
if lower.exists() || fs::hard_link(&upper, &lower).is_err() {
return; }
assert!(!one_entry(&upper, &lower));
}
#[cfg(unix)]
#[test]
fn a_symlink_is_two_entries() {
let dir = tmp();
let upper = dir.path().join("Foo.rs");
fs::write(&upper, "x").unwrap();
let lower = dir.path().join("foo.rs");
if lower.exists() {
return; }
std::os::unix::fs::symlink(&upper, &lower).unwrap();
assert!(!one_entry(&upper, &lower));
}
#[test]
fn a_file_in_two_spellings_of_a_directory_follows_the_same_rule() {
let dir = tmp();
let upper = dir.path().join("Widgets/foo.rs");
fs::create_dir(dir.path().join("Widgets")).unwrap();
fs::write(&upper, "x").unwrap();
let lower = dir.path().join("widgets/foo.rs");
assert_eq!(one_entry(&upper, &lower), lower.exists());
}
#[test]
fn two_equal_paths_are_not_two_spellings() {
let dir = tmp();
let file = dir.path().join("Foo.rs");
fs::write(&file, "x").unwrap();
assert!(!one_entry(&file, &file));
}
#[test]
fn a_missing_path_is_no_entry() {
let dir = tmp();
let upper = dir.path().join("Foo.rs");
fs::write(&upper, "x").unwrap();
assert!(!one_entry(&upper, &dir.path().join("Bar.rs")));
assert!(!one_entry(
&dir.path().join("Bar.rs"),
&dir.path().join("bar.rs")
));
}
#[test]
fn a_directory_that_cannot_be_read_lists_every_name() {
assert!(listed(Path::new("Foo.rs")), "the parent is \"\"");
assert!(listed(&tmp().path().join("gone/Foo.rs")));
}
#[test]
fn has_exact_entry_matches_bytes_not_case() {
let dir = tmp();
fs::write(dir.path().join("Foo.rs"), "x").unwrap();
assert!(has_exact_entry(dir.path(), "Foo.rs"));
assert!(!has_exact_entry(dir.path(), "foo.rs"));
}
}