use std::collections::HashMap;
use std::ffi::{OsStr, OsString};
use std::path::{Component, Path, PathBuf};
use unicode_normalization::UnicodeNormalization;
#[derive(Default)]
pub struct Spelling {
listings: HashMap<PathBuf, Vec<OsString>>,
}
impl Spelling {
pub fn on_disk(&mut self, path: &Path) -> PathBuf {
let mut out = PathBuf::new();
for component in path.components() {
let Component::Normal(name) = component else {
out.push(component.as_os_str());
continue;
};
if name.as_encoded_bytes().is_ascii() {
out.push(name);
continue;
}
let dir = if out.as_os_str().is_empty() {
PathBuf::from(".")
} else {
out.clone()
};
match self.entry(&dir, name) {
Some(spelled) => out.push(spelled),
None => out.push(name),
}
}
out
}
fn entry(&mut self, dir: &Path, name: &OsStr) -> Option<OsString> {
let listing = self.listings.entry(dir.to_path_buf()).or_insert_with(|| {
std::fs::read_dir(dir)
.map(|entries| entries.flatten().map(|e| e.file_name()).collect())
.unwrap_or_default()
});
if listing.iter().any(|n| n == name) {
return Some(name.to_os_string());
}
let wanted: String = name.to_string_lossy().nfc().collect();
listing
.iter()
.find(|n| n.to_string_lossy().nfc().eq(wanted.chars()))
.cloned()
}
}
pub fn on_disk(path: &Path) -> PathBuf {
Spelling::default().on_disk(path)
}
#[cfg(test)]
mod tests {
use super::*;
fn spellings() -> (String, String) {
let nfd: String = "Flügel".nfd().collect();
let nfc: String = "Flügel".nfc().collect();
assert_ne!(nfd, nfc);
(nfd, nfc)
}
#[test]
fn a_precomposed_spelling_becomes_the_directorys_own() {
let dir = tempfile::tempdir().unwrap();
let (nfd, nfc) = spellings();
std::fs::create_dir(dir.path().join(&nfd)).unwrap();
std::fs::write(dir.path().join(&nfd).join("song.wav"), b"").unwrap();
let got = on_disk(&dir.path().join(&nfc).join("song.wav"));
let disk = dir.path().join(&nfd).join("song.wav");
assert_eq!(
got.as_os_str().as_encoded_bytes(),
disk.as_os_str().as_encoded_bytes(),
"the bytes the directory holds, not the ones asked with"
);
}
#[test]
fn the_disks_own_spelling_passes_through() {
let dir = tempfile::tempdir().unwrap();
let (nfd, _) = spellings();
std::fs::create_dir(dir.path().join(&nfd)).unwrap();
let disk = dir.path().join(&nfd);
assert_eq!(on_disk(&disk), disk);
}
#[test]
fn an_unreadable_component_is_left_as_given() {
let (_, nfc) = spellings();
let asked = Path::new("/nowhere/at/all").join(&nfc).join("song.wav");
assert_eq!(on_disk(&asked), asked);
}
}