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 path = entry?.path();
159 if !kind.matches(&path) {
160 continue;
161 }
162 let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
163 continue;
164 };
165 if name.eq_ignore_ascii_case(expected) {
166 matches.push(path);
167 }
168 }
169 matches.sort_by(|a, b| {
170 let name_of = |path: &Path| {
171 path.file_name()
172 .and_then(|name| name.to_str())
173 .unwrap_or_default()
174 .to_owned()
175 };
176 let (a, b) = (name_of(a), name_of(b));
177 cmp_ascii_case_insensitive(&a, &b).then(a.cmp(&b))
178 });
179 Ok(matches)
180}
181
182pub fn find_case_insensitive_file(
193 directory: &Path,
194 expected_file_name: &str,
195) -> io::Result<Option<PathBuf>> {
196 Ok(find_case_insensitive_files(directory, expected_file_name)?
197 .into_iter()
198 .next())
199}
200
201pub fn find_case_insensitive_directory(
216 directory: &Path,
217 expected_name: &str,
218) -> io::Result<Option<PathBuf>> {
219 Ok(find_case_insensitive_directories(directory, expected_name)?
220 .into_iter()
221 .next())
222}
223
224#[cfg(test)]
225mod tests {
226 use super::{
227 files_beneath, files_have_same_bytes, find_case_insensitive_directories,
228 find_case_insensitive_directory, find_case_insensitive_file,
229 };
230
231 use std::fs;
232
233 use tempfile::TempDir;
234
235 #[test]
236 fn resolves_case_insensitive_match() {
237 let temp = TempDir::new().expect("create tempdir");
238 let root = temp.path();
239 let path = root.join("ChItIn.KeY");
240 fs::write(&path, b"key").expect("write fixture");
241
242 let found = find_case_insensitive_file(root, "chitin.key")
243 .expect("lookup should succeed")
244 .expect("file should be found");
245 assert_eq!(found, path);
246 }
247
248 #[test]
249 fn returns_none_when_no_match() {
250 let temp = TempDir::new().expect("create tempdir");
251
252 let found =
253 find_case_insensitive_file(temp.path(), "missing.txt").expect("lookup should run");
254 assert!(found.is_none());
255 }
256
257 #[test]
258 fn resolves_case_insensitive_directory_match() {
259 let temp = TempDir::new().expect("create tempdir");
260 let root = temp.path();
261 let path = root.join("OvErRiDe");
262 fs::create_dir(&path).expect("create fixture dir");
263
264 let found = find_case_insensitive_directory(root, "override")
265 .expect("lookup should succeed")
266 .expect("dir should be found");
267 assert_eq!(found, path);
268 }
269
270 #[test]
271 fn directory_lookup_ignores_files_with_matching_name() {
272 let temp = TempDir::new().expect("create tempdir");
274 let root = temp.path();
275 fs::write(root.join("override"), b"not a directory").expect("write fixture");
276
277 let found = find_case_insensitive_directory(root, "override").expect("lookup should run");
278 assert!(found.is_none());
279 }
280
281 #[test]
282 fn directory_lookup_returns_none_when_no_match() {
283 let temp = TempDir::new().expect("create tempdir");
284
285 let found =
286 find_case_insensitive_directory(temp.path(), "missing").expect("lookup should run");
287 assert!(found.is_none());
288 }
289
290 #[test]
292 fn files_beneath_reaches_every_depth() {
293 let temp = TempDir::new().expect("tempdir");
294 let deep = temp.path().join("Override").join("nested");
295 fs::create_dir_all(&deep).expect("create");
296 fs::write(temp.path().join("readme.txt"), b"a").expect("write");
297 fs::write(temp.path().join("Override").join("heads.2da"), b"b").expect("write");
298 fs::write(deep.join("appearance.2da"), b"c").expect("write");
299
300 let found = files_beneath(temp.path()).expect("the tree reads");
301 let names: Vec<&str> = found
302 .iter()
303 .filter_map(|path| path.file_name()?.to_str())
304 .collect();
305
306 assert_eq!(names, vec!["heads.2da", "appearance.2da", "readme.txt"]);
307 }
308
309 #[test]
311 fn files_beneath_an_empty_directory_is_empty() {
312 let temp = TempDir::new().expect("tempdir");
313 fs::create_dir(temp.path().join("empty")).expect("create");
314 assert!(files_beneath(temp.path()).expect("reads").is_empty());
315 }
316
317 #[test]
320 fn the_plural_finder_returns_both_spellings() {
321 let temp = TempDir::new().expect("tempdir");
322 fs::create_dir(temp.path().join("Override")).expect("create");
323 fs::create_dir(temp.path().join("override")).expect("create");
324
325 let all = find_case_insensitive_directories(temp.path(), "override").expect("reads");
326 assert_eq!(all.len(), 2, "{all:?}");
327 assert_eq!(
328 find_case_insensitive_directory(temp.path(), "override")
329 .expect("reads")
330 .as_ref(),
331 all.first(),
332 "the singular finder is not the first of the plural"
333 );
334 }
335
336 #[test]
341 fn a_difference_past_the_first_block_is_found() {
342 let temp = TempDir::new().expect("tempdir");
343 let ours = vec![b'a'; 200 * 1024];
344 let mut theirs = ours.clone();
345 let last = theirs.len() - 1;
346 theirs[last] = b'b';
347 let one = temp.path().join("one");
348 let other = temp.path().join("other");
349 fs::write(&one, &ours).expect("write");
350 fs::write(&other, &theirs).expect("write");
351
352 assert!(!files_have_same_bytes(&one, &other).expect("both read"));
353 assert!(files_have_same_bytes(&one, &one).expect("reads"));
354 }
355}