1use std::io;
7use std::path::{Path, PathBuf};
8
9use crate::ascii::cmp_ascii_case_insensitive;
10
11pub fn find_case_insensitive_file(
18 directory: &Path,
19 expected_file_name: &str,
20) -> io::Result<Option<PathBuf>> {
21 let mut matches = Vec::new();
22 let entries = std::fs::read_dir(directory)?;
23 for entry in entries {
24 let entry = entry?;
25 let path = entry.path();
26 if !path.is_file() {
27 continue;
28 }
29 let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
30 continue;
31 };
32 if name.eq_ignore_ascii_case(expected_file_name) {
33 matches.push(path);
34 }
35 }
36
37 if matches.is_empty() {
38 return Ok(None);
39 }
40
41 matches.sort_by(|a, b| {
42 let a_name = a
43 .file_name()
44 .and_then(|name| name.to_str())
45 .unwrap_or_default();
46 let b_name = b
47 .file_name()
48 .and_then(|name| name.to_str())
49 .unwrap_or_default();
50 cmp_ascii_case_insensitive(a_name, b_name).then(a_name.cmp(b_name))
51 });
52
53 Ok(matches.into_iter().next())
54}
55
56#[cfg(test)]
57mod tests {
58 use super::find_case_insensitive_file;
59
60 use std::fs;
61 use std::path::PathBuf;
62 use std::sync::atomic::{AtomicU64, Ordering};
63 use std::time::{SystemTime, UNIX_EPOCH};
64
65 fn unique_test_dir() -> PathBuf {
66 static COUNTER: AtomicU64 = AtomicU64::new(0);
67 let nanos = SystemTime::now()
68 .duration_since(UNIX_EPOCH)
69 .expect("system clock should be after unix epoch")
70 .as_nanos();
71 let sequence = COUNTER.fetch_add(1, Ordering::Relaxed);
72 std::env::temp_dir().join(format!(
73 "rakata_core_fs_test_{}_{}_{}",
74 std::process::id(),
75 nanos,
76 sequence
77 ))
78 }
79
80 #[test]
81 fn resolves_case_insensitive_match() {
82 let root = unique_test_dir();
83 fs::create_dir_all(&root).expect("create test dir");
84 let path = root.join("ChItIn.KeY");
85 fs::write(&path, b"key").expect("write fixture");
86
87 let found = find_case_insensitive_file(&root, "chitin.key")
88 .expect("lookup should succeed")
89 .expect("file should be found");
90 assert_eq!(found, path);
91
92 fs::remove_dir_all(root).expect("cleanup");
93 }
94
95 #[test]
96 fn returns_none_when_no_match() {
97 let root = unique_test_dir();
98 fs::create_dir_all(&root).expect("create test dir");
99
100 let found = find_case_insensitive_file(&root, "missing.txt").expect("lookup should run");
101 assert!(found.is_none());
102
103 fs::remove_dir_all(root).expect("cleanup");
104 }
105}