use std::borrow::Cow;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use unicode_normalization::UnicodeNormalization;
static PRECOMPOSE_UNICODE: AtomicBool = AtomicBool::new(false);
pub fn set_precompose_unicode(enabled: bool) {
PRECOMPOSE_UNICODE.store(enabled, Ordering::Relaxed);
}
pub fn precompose_unicode_enabled() -> bool {
PRECOMPOSE_UNICODE.load(Ordering::Relaxed)
}
pub fn activate_precompose_unicode(value: Option<bool>) {
set_precompose_unicode(value.unwrap_or(false));
}
pub fn has_non_ascii(s: &str) -> bool {
s.bytes().any(|b| b >= 0x80)
}
pub fn has_non_ascii_bytes(bytes: &[u8]) -> bool {
bytes.iter().any(|&b| b >= 0x80)
}
pub fn precompose_string_if_needed(input: &str) -> Cow<'_, str> {
if !precompose_unicode_enabled() || !has_non_ascii(input) {
return Cow::Borrowed(input);
}
let nfc: String = input.nfc().collect();
if nfc == input {
Cow::Borrowed(input)
} else {
Cow::Owned(nfc)
}
}
pub fn precompose_bytes_if_needed(input: &[u8]) -> Cow<'_, [u8]> {
if !precompose_unicode_enabled() || !has_non_ascii_bytes(input) {
return Cow::Borrowed(input);
}
let Ok(text) = std::str::from_utf8(input) else {
return Cow::Borrowed(input);
};
match precompose_string_if_needed(text) {
Cow::Borrowed(_) => Cow::Borrowed(input),
Cow::Owned(nfc) => Cow::Owned(nfc.into_bytes()),
}
}
pub fn precompose_owned_string_if_needed(input: String) -> String {
match precompose_string_if_needed(&input) {
Cow::Borrowed(_) => input,
Cow::Owned(nfc) => nfc,
}
}
pub fn precompose_path_if_needed(path: &Path) -> Cow<'_, Path> {
if !precompose_unicode_enabled() {
return Cow::Borrowed(path);
}
let lossy = path.to_string_lossy();
if !has_non_ascii(&lossy) {
return Cow::Borrowed(path);
}
let mut changed = false;
let mut out = PathBuf::new();
for component in path.components() {
match component {
std::path::Component::Normal(name) => {
let name_str = name.to_string_lossy();
match precompose_string_if_needed(&name_str) {
Cow::Borrowed(_) => out.push(name),
Cow::Owned(nfc) => {
changed = true;
out.push(nfc);
}
}
}
other => out.push(other.as_os_str()),
}
}
if changed {
Cow::Owned(out)
} else {
Cow::Borrowed(path)
}
}
pub fn precompose_os_str_bytes_if_needed(name: &OsStr) -> Cow<'_, [u8]> {
#[cfg(unix)]
{
use std::os::unix::ffi::OsStrExt;
precompose_bytes_if_needed(name.as_bytes())
}
#[cfg(not(unix))]
{
let owned = name.to_string_lossy().into_owned();
match precompose_string_if_needed(&owned) {
Cow::Borrowed(_) => Cow::Owned(owned.into_bytes()),
Cow::Owned(nfc) => Cow::Owned(nfc.into_bytes()),
}
}
}
pub fn precompose_argv_if_needed(args: &mut [String]) {
if !precompose_unicode_enabled() {
return;
}
for arg in args.iter_mut() {
if has_non_ascii(arg) {
*arg = precompose_owned_string_if_needed(std::mem::take(arg));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Mutex, OnceLock};
fn lock_precompose_for_test() -> std::sync::MutexGuard<'static, ()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
#[test]
fn nfc_conversion_only_when_enabled() {
let _guard = lock_precompose_for_test();
let nfd = "A\u{0308}"; let nfc = "\u{00C4}";
set_precompose_unicode(false);
assert_eq!(precompose_string_if_needed(nfd).as_ref(), nfd);
set_precompose_unicode(true);
assert_eq!(precompose_string_if_needed(nfd).as_ref(), nfc);
assert_eq!(precompose_string_if_needed(nfc).as_ref(), nfc);
assert_eq!(precompose_string_if_needed("ascii").as_ref(), "ascii");
set_precompose_unicode(false);
}
#[test]
fn path_components_are_precomposed() {
let _guard = lock_precompose_for_test();
set_precompose_unicode(true);
let nfd = PathBuf::from("d.A\u{0308}/f.A\u{0308}".to_string());
let precomposed = precompose_path_if_needed(&nfd);
assert_eq!(
precomposed.to_string_lossy().as_ref(),
"d.\u{00C4}/f.\u{00C4}"
);
set_precompose_unicode(false);
}
#[test]
fn worker_thread_sees_parent_set_value() {
let _guard = lock_precompose_for_test();
set_precompose_unicode(true);
let nfd = "A\u{0308}";
let nfc = "\u{00C4}";
let enabled = std::thread::scope(|s| {
s.spawn(|| {
assert!(precompose_unicode_enabled());
assert_eq!(precompose_string_if_needed(nfd).as_ref(), nfc);
precompose_unicode_enabled()
})
.join()
.expect("worker join")
});
assert!(enabled);
set_precompose_unicode(false);
let disabled = std::thread::scope(|s| {
s.spawn(|| {
assert!(!precompose_unicode_enabled());
assert_eq!(precompose_string_if_needed(nfd).as_ref(), nfd);
precompose_unicode_enabled()
})
.join()
.expect("worker join")
});
assert!(!disabled);
}
}