use icu_normalizer::ComposingNormalizerBorrowed;
use std::borrow::Cow;
use std::ffi::OsString;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::{Path, PathBuf};
#[must_use]
pub fn has_non_ascii_utf8(s: &str) -> bool {
s.as_bytes().iter().any(|b| *b & 0x80 != 0)
}
#[must_use]
pub fn precompose_utf8_segment(s: &str) -> Cow<'_, str> {
if !has_non_ascii_utf8(s) {
return Cow::Borrowed(s);
}
let normalized = ComposingNormalizerBorrowed::new_nfc().normalize(s);
if normalized == s {
Cow::Borrowed(s)
} else {
Cow::Owned(normalized.into_owned())
}
}
#[must_use]
pub fn precompose_utf8_path(path: &str) -> Cow<'_, str> {
if !path.as_bytes().iter().any(|b| *b & 0x80 != 0) {
return Cow::Borrowed(path);
}
let mut buf = String::with_capacity(path.len());
for (i, seg) in path.split('/').enumerate() {
if i > 0 {
buf.push('/');
}
let c = precompose_utf8_segment(seg);
buf.push_str(c.as_ref());
}
if buf == path {
Cow::Borrowed(path)
} else {
Cow::Owned(buf)
}
}
pub fn precompose_os_string_utf8_path(s: &mut OsString, enabled: bool) {
if !enabled {
return;
}
let Some(utf8) = s.to_str() else {
return;
};
let normalized = precompose_utf8_path(utf8).into_owned();
if normalized != utf8 {
*s = OsString::from(normalized);
}
}
pub fn probe_filesystem_normalizes_nfd_to_nfc(git_dir: &Path) -> std::io::Result<bool> {
const NFC: &str = "\u{00e4}";
const NFD: &str = "\u{0061}\u{0308}";
let nfc_path: PathBuf = git_dir.join(NFC);
let _ = std::fs::remove_file(&nfc_path);
{
let mut f = OpenOptions::new()
.create_new(true)
.write(true)
.open(&nfc_path)?;
f.write_all(b"x")?;
}
let nfd_path = git_dir.join(NFD);
let aliases = nfd_path.exists();
let _ = std::fs::remove_file(&nfc_path);
Ok(aliases)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn precompose_nfd_filename_to_nfc() {
let nfd = format!("f.{}\u{0308}", 'A');
let nfc = format!("f.\u{00c4}");
assert_eq!(precompose_utf8_path(&nfd).as_ref(), nfc.as_str());
}
}