retex-kpath 1.3.0-alpha.2

kpathsea-compatible file lookup for retex
Documentation
//! The fallback case-insensitive search (kpathsea manual, "Casefolding
//! search"), enabled by `texmf_casefold_search`.
//!
//! From black-box tests of kpsewhich: in each path element, a name not
//! found as given matches a file of the same name in another case (ASCII
//! letters only), in the directory of its last component; directories are
//! not casefolded (`SUB/Deep.tex` does not find `sub/deep.tex`), and the
//! database-only elements (`!!`) are not searched so. An exact match in a
//! later element loses to a casefolded one in an earlier element.

use std::path::{Path, PathBuf};

/// Whether a value of `texmf_casefold_search` enables the search: any
/// value but one starting with `0` or `f` (observed: `n` and `no` enable
/// it).
pub fn enabled(value: Option<&str>) -> bool {
    value.is_some_and(|v| !matches!(v.bytes().next(), None | Some(b'0' | b'f')))
}

/// A file in `dir` whose path relative to it is `name` but for the case of
/// its last component; the first in name order if there are several.
pub fn find(dir: &Path, name: &str) -> Option<PathBuf> {
    let (sub, base) = match name.rfind('/') {
        Some(i) => (&name[..i], &name[i + 1..]),
        None => ("", name),
    };
    let dir = if sub.is_empty() {
        dir.to_path_buf()
    } else {
        PathBuf::from(crate::join(&dir.to_string_lossy(), sub))
    };
    let mut hits: Vec<PathBuf> = std::fs::read_dir(&dir)
        .ok()?
        .filter_map(Result::ok)
        .filter(|e| {
            e.file_name()
                .to_str()
                .is_some_and(|n| n.eq_ignore_ascii_case(base))
        })
        .map(|e| {
            PathBuf::from(crate::join(
                &dir.to_string_lossy(),
                &e.file_name().to_string_lossy(),
            ))
        })
        .filter(|p| p.is_file())
        .collect();
    hits.sort();
    hits.into_iter().next()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn values() {
        for v in ["1", "t", "y", "n", "no", "x"] {
            assert!(enabled(Some(v)), "{v}");
        }
        for v in ["0", "f", "false", ""] {
            assert!(!enabled(Some(v)), "{v}");
        }
        assert!(!enabled(None));
    }
}