use std::borrow::Cow;
use std::path::{Path, PathBuf};
use unicode_normalization::UnicodeNormalization;
#[must_use]
pub fn normalize_path_str(s: &str) -> String {
s.nfc().collect()
}
#[must_use]
pub fn normalize_path_str_cow(s: &str) -> Cow<'_, str> {
let normalized: String = s.nfc().collect();
if normalized == s {
Cow::Borrowed(s)
} else {
Cow::Owned(normalized)
}
}
#[must_use]
pub fn normalize_pathbuf(path: &Path) -> PathBuf {
match path.to_str() {
Some(s) => PathBuf::from(normalize_path_str(s)),
None => path.to_path_buf(),
}
}
#[must_use]
pub fn paths_equal(a: &str, b: &str) -> bool {
normalize_path_str(a) == normalize_path_str(b)
}
#[must_use]
pub fn paths_equal_normalized(a: &Path, b: &Path) -> bool {
match (a.to_str(), b.to_str()) {
(Some(a_str), Some(b_str)) => paths_equal(a_str, b_str),
_ => false,
}
}
#[must_use]
pub fn is_nfc(s: &str) -> bool {
unicode_normalization::is_nfc(s)
}
#[must_use]
pub fn path_key(path: &Path) -> String {
normalize_path_str(&path.to_string_lossy())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_normalize_path_str_nfc_unchanged() {
let nfc = "café.txt";
assert_eq!(normalize_path_str(nfc), nfc);
}
#[test]
fn test_normalize_path_str_nfd_to_nfc() {
let nfd = "cafe\u{0301}.txt"; let expected = "café.txt"; assert_eq!(normalize_path_str(nfd), expected);
}
#[test]
fn test_normalize_path_str_ascii_unchanged() {
let ascii = "hello.txt";
assert_eq!(normalize_path_str(ascii), ascii);
}
#[test]
fn test_normalize_path_str_empty() {
assert_eq!(normalize_path_str(""), "");
}
#[test]
fn test_normalize_path_str_mixed_content() {
let mixed = "path/to/file_cafe\u{0301}_2024.txt";
let expected = "path/to/file_café_2024.txt";
assert_eq!(normalize_path_str(mixed), expected);
}
#[test]
fn test_normalize_path_str_multiple_accents() {
let nfd = "re\u{0301}sume\u{0301}.txt"; let nfc = "résumé.txt";
assert_eq!(normalize_path_str(nfd), nfc);
}
#[test]
fn test_normalize_pathbuf() {
let path = PathBuf::from("docs/cafe\u{0301}.txt");
let normalized = normalize_pathbuf(&path);
assert_eq!(normalized, PathBuf::from("docs/café.txt"));
}
#[test]
fn test_normalize_pathbuf_already_nfc() {
let path = PathBuf::from("docs/café.txt");
let normalized = normalize_pathbuf(&path);
assert_eq!(normalized, PathBuf::from("docs/café.txt"));
}
#[test]
fn test_paths_equal_nfc_vs_nfd() {
let nfc = "café.txt";
let nfd = "cafe\u{0301}.txt";
assert!(paths_equal(nfc, nfd));
}
#[test]
fn test_paths_equal_different() {
assert!(!paths_equal("café.txt", "coffee.txt"));
}
#[test]
fn test_paths_equal_normalized() {
let a = Path::new("café.txt");
let b = Path::new("cafe\u{0301}.txt");
assert!(paths_equal_normalized(a, b));
let c = Path::new("other.txt");
assert!(!paths_equal_normalized(a, c));
}
#[test]
fn test_is_nfc() {
assert!(is_nfc("café.txt")); assert!(!is_nfc("cafe\u{0301}.txt")); assert!(is_nfc("hello.txt")); assert!(is_nfc("")); }
#[test]
fn test_path_key() {
let nfc_path = Path::new("café.txt");
let nfd_path = Path::new("cafe\u{0301}.txt");
assert_eq!(path_key(nfc_path), path_key(nfd_path));
}
#[test]
fn test_normalize_path_str_cow_already_nfc() {
let nfc = "café.txt";
let result = normalize_path_str_cow(nfc);
assert!(matches!(result, Cow::Borrowed(_)));
}
#[test]
fn test_normalize_path_str_cow_needs_conversion() {
let nfd = "cafe\u{0301}.txt";
let result = normalize_path_str_cow(nfd);
assert!(matches!(result, Cow::Owned(_)));
assert_eq!(result, "café.txt");
}
#[test]
fn test_combining_characters() {
let nfc = "español.txt";
let nfd = "espan\u{0303}ol.txt";
assert!(paths_equal(nfc, nfd));
}
#[test]
fn test_hangul_normalization() {
let nfc = "가.txt";
let nfd = "\u{1100}\u{1161}.txt";
assert!(paths_equal(nfc, nfd));
}
}