1use std::io;
7use std::path::{Path, PathBuf};
8
9use crate::ascii::cmp_ascii_case_insensitive;
10
11pub const TEMP_SUFFIX: &str = ".rakata-new";
19
20pub fn files_have_same_bytes(one: &Path, other: &Path) -> io::Result<bool> {
32 let open = |path: &Path| std::fs::File::open(path).map(io::BufReader::new);
33 fn fill(reader: &mut impl io::Read, buffer: &mut [u8]) -> io::Result<usize> {
37 let mut filled = 0;
38 while filled < buffer.len() {
39 match reader.read(&mut buffer[filled..])? {
40 0 => break,
41 took => filled += took,
42 }
43 }
44 Ok(filled)
45 }
46
47 let mut ours = open(one)?;
48 let mut theirs = open(other)?;
49 let mut here = vec![0_u8; 64 * 1024];
52 let mut there = vec![0_u8; 64 * 1024];
53 loop {
54 let took = fill(&mut ours, &mut here)?;
55 let got = fill(&mut theirs, &mut there)?;
56 if took != got || here[..took] != there[..got] {
57 return Ok(false);
58 }
59 if took == 0 {
60 return Ok(true);
61 }
62 }
63}
64
65pub fn files_beneath(directory: &Path) -> io::Result<Vec<PathBuf>> {
83 let mut found = Vec::new();
84 let mut pending = vec![directory.to_path_buf()];
85 while let Some(next) = pending.pop() {
86 for entry in std::fs::read_dir(&next)? {
87 let path = entry?.path();
88 if path.is_dir() {
89 pending.push(path);
90 } else {
91 found.push(path);
92 }
93 }
94 }
95 found.sort();
96 Ok(found)
97}
98
99pub fn find_case_insensitive_files(
115 directory: &Path,
116 expected_file_name: &str,
117) -> io::Result<Vec<PathBuf>> {
118 matching(directory, expected_file_name, Kind::File)
119}
120
121pub fn find_case_insensitive_directories(
131 directory: &Path,
132 expected_name: &str,
133) -> io::Result<Vec<PathBuf>> {
134 matching(directory, expected_name, Kind::Directory)
135}
136
137#[derive(Clone, Copy)]
139enum Kind {
140 File,
141 Directory,
142}
143
144impl Kind {
145 fn matches(self, path: &Path) -> bool {
147 match self {
148 Self::File => path.is_file(),
149 Self::Directory => path.is_dir(),
150 }
151 }
152}
153
154fn matching(directory: &Path, expected: &str, kind: Kind) -> io::Result<Vec<PathBuf>> {
156 let mut matches = Vec::new();
157 for entry in std::fs::read_dir(directory)? {
158 let entry = entry?;
159 let name = entry.file_name();
160 if !name
161 .to_str()
162 .is_some_and(|name| name.eq_ignore_ascii_case(expected))
163 {
164 continue;
165 }
166 let path = entry.path();
168 if kind.matches(&path) {
169 matches.push(path);
170 }
171 }
172 matches.sort_by(|a, b| {
173 let name_of = |path: &Path| {
174 path.file_name()
175 .and_then(|name| name.to_str())
176 .unwrap_or_default()
177 .to_owned()
178 };
179 let (a, b) = (name_of(a), name_of(b));
180 cmp_ascii_case_insensitive(&a, &b).then(a.cmp(&b))
181 });
182 Ok(matches)
183}
184
185pub fn find_case_insensitive_file(
196 directory: &Path,
197 expected_file_name: &str,
198) -> io::Result<Option<PathBuf>> {
199 Ok(find_case_insensitive_files(directory, expected_file_name)?
200 .into_iter()
201 .next())
202}
203
204pub fn find_case_insensitive_directory(
219 directory: &Path,
220 expected_name: &str,
221) -> io::Result<Option<PathBuf>> {
222 Ok(find_case_insensitive_directories(directory, expected_name)?
223 .into_iter()
224 .next())
225}
226
227#[cfg(test)]
228mod tests {
229 use super::{
230 files_beneath, files_have_same_bytes, find_case_insensitive_directories,
231 find_case_insensitive_directory, find_case_insensitive_file,
232 };
233
234 use std::fs;
235
236 use tempfile::TempDir;
237
238 #[test]
239 fn resolves_case_insensitive_match() {
240 let temp = TempDir::new().expect("create tempdir");
241 let root = temp.path();
242 let path = root.join("ChItIn.KeY");
243 fs::write(&path, b"key").expect("write fixture");
244
245 let found = find_case_insensitive_file(root, "chitin.key")
246 .expect("lookup should succeed")
247 .expect("file should be found");
248 assert_eq!(found, path);
249 }
250
251 #[test]
252 fn returns_none_when_no_match() {
253 let temp = TempDir::new().expect("create tempdir");
254
255 let found =
256 find_case_insensitive_file(temp.path(), "missing.txt").expect("lookup should run");
257 assert!(found.is_none());
258 }
259
260 #[test]
261 fn resolves_case_insensitive_directory_match() {
262 let temp = TempDir::new().expect("create tempdir");
263 let root = temp.path();
264 let path = root.join("OvErRiDe");
265 fs::create_dir(&path).expect("create fixture dir");
266
267 let found = find_case_insensitive_directory(root, "override")
268 .expect("lookup should succeed")
269 .expect("dir should be found");
270 assert_eq!(found, path);
271 }
272
273 #[test]
274 fn directory_lookup_ignores_files_with_matching_name() {
275 let temp = TempDir::new().expect("create tempdir");
277 let root = temp.path();
278 fs::write(root.join("override"), b"not a directory").expect("write fixture");
279
280 let found = find_case_insensitive_directory(root, "override").expect("lookup should run");
281 assert!(found.is_none());
282 }
283
284 #[test]
285 fn directory_lookup_returns_none_when_no_match() {
286 let temp = TempDir::new().expect("create tempdir");
287
288 let found =
289 find_case_insensitive_directory(temp.path(), "missing").expect("lookup should run");
290 assert!(found.is_none());
291 }
292
293 #[test]
295 fn files_beneath_reaches_every_depth() {
296 let temp = TempDir::new().expect("tempdir");
297 let deep = temp.path().join("Override").join("nested");
298 fs::create_dir_all(&deep).expect("create");
299 fs::write(temp.path().join("readme.txt"), b"a").expect("write");
300 fs::write(temp.path().join("Override").join("heads.2da"), b"b").expect("write");
301 fs::write(deep.join("appearance.2da"), b"c").expect("write");
302
303 let found = files_beneath(temp.path()).expect("the tree reads");
304 let names: Vec<&str> = found
305 .iter()
306 .filter_map(|path| path.file_name()?.to_str())
307 .collect();
308
309 assert_eq!(names, vec!["heads.2da", "appearance.2da", "readme.txt"]);
310 }
311
312 #[test]
314 fn files_beneath_an_empty_directory_is_empty() {
315 let temp = TempDir::new().expect("tempdir");
316 fs::create_dir(temp.path().join("empty")).expect("create");
317 assert!(files_beneath(temp.path()).expect("reads").is_empty());
318 }
319
320 #[test]
323 #[cfg_attr(
324 any(windows, target_os = "macos"),
325 ignore = "needs a filesystem that tells names apart by case"
326 )]
327 fn the_plural_finder_returns_both_spellings() {
328 let temp = TempDir::new().expect("tempdir");
329 fs::create_dir(temp.path().join("Override")).expect("create");
330 fs::create_dir(temp.path().join("override")).expect("create");
331
332 let all = find_case_insensitive_directories(temp.path(), "override").expect("reads");
333 assert_eq!(all.len(), 2, "{all:?}");
334 assert_eq!(
335 find_case_insensitive_directory(temp.path(), "override")
336 .expect("reads")
337 .as_ref(),
338 all.first(),
339 "the singular finder is not the first of the plural"
340 );
341 }
342
343 #[test]
348 fn a_difference_past_the_first_block_is_found() {
349 let temp = TempDir::new().expect("tempdir");
350 let ours = vec![b'a'; 200 * 1024];
351 let mut theirs = ours.clone();
352 let last = theirs.len() - 1;
353 theirs[last] = b'b';
354 let one = temp.path().join("one");
355 let other = temp.path().join("other");
356 fs::write(&one, &ours).expect("write");
357 fs::write(&other, &theirs).expect("write");
358
359 assert!(!files_have_same_bytes(&one, &other).expect("both read"));
360 assert!(files_have_same_bytes(&one, &one).expect("reads"));
361 }
362}