Skip to main content

rakata_core/
fs.rs

1//! Filesystem helpers shared across KotOR crates.
2//!
3//! This module provides deterministic case-insensitive file-name lookup
4//! utilities for cross-platform behavior.
5
6use std::io;
7use std::path::{Path, PathBuf};
8
9use crate::ascii::cmp_ascii_case_insensitive;
10
11/// Finds one file in `directory` with ASCII case-insensitive name matching.
12///
13/// When multiple entries differ only by case, this function returns a
14/// deterministic winner by sorting candidates on:
15/// 1. lowercase filename
16/// 2. original filename bytes
17pub 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
62    use tempfile::TempDir;
63
64    #[test]
65    fn resolves_case_insensitive_match() {
66        let temp = TempDir::new().expect("create tempdir");
67        let root = temp.path();
68        let path = root.join("ChItIn.KeY");
69        fs::write(&path, b"key").expect("write fixture");
70
71        let found = find_case_insensitive_file(root, "chitin.key")
72            .expect("lookup should succeed")
73            .expect("file should be found");
74        assert_eq!(found, path);
75    }
76
77    #[test]
78    fn returns_none_when_no_match() {
79        let temp = TempDir::new().expect("create tempdir");
80
81        let found =
82            find_case_insensitive_file(temp.path(), "missing.txt").expect("lookup should run");
83        assert!(found.is_none());
84    }
85}