1use std::borrow::Cow;
16use std::ffi::OsStr;
17use std::path::{Path, PathBuf};
18use std::sync::atomic::{AtomicBool, Ordering};
19
20use unicode_normalization::UnicodeNormalization;
21
22static PRECOMPOSE_UNICODE: AtomicBool = AtomicBool::new(false);
26
27pub fn set_precompose_unicode(enabled: bool) {
29 PRECOMPOSE_UNICODE.store(enabled, Ordering::Relaxed);
30}
31
32pub fn precompose_unicode_enabled() -> bool {
34 PRECOMPOSE_UNICODE.load(Ordering::Relaxed)
35}
36
37pub fn activate_precompose_unicode(value: Option<bool>) {
39 set_precompose_unicode(value.unwrap_or(false));
40}
41
42pub fn has_non_ascii(s: &str) -> bool {
44 s.bytes().any(|b| b >= 0x80)
45}
46
47pub fn has_non_ascii_bytes(bytes: &[u8]) -> bool {
49 bytes.iter().any(|&b| b >= 0x80)
50}
51
52pub fn precompose_string_if_needed(input: &str) -> Cow<'_, str> {
57 if !precompose_unicode_enabled() || !has_non_ascii(input) {
58 return Cow::Borrowed(input);
59 }
60 let nfc: String = input.nfc().collect();
61 if nfc == input {
62 Cow::Borrowed(input)
63 } else {
64 Cow::Owned(nfc)
65 }
66}
67
68pub fn precompose_bytes_if_needed(input: &[u8]) -> Cow<'_, [u8]> {
73 if !precompose_unicode_enabled() || !has_non_ascii_bytes(input) {
74 return Cow::Borrowed(input);
75 }
76 let Ok(text) = std::str::from_utf8(input) else {
77 return Cow::Borrowed(input);
78 };
79 match precompose_string_if_needed(text) {
80 Cow::Borrowed(_) => Cow::Borrowed(input),
81 Cow::Owned(nfc) => Cow::Owned(nfc.into_bytes()),
82 }
83}
84
85pub fn precompose_owned_string_if_needed(input: String) -> String {
87 match precompose_string_if_needed(&input) {
88 Cow::Borrowed(_) => input,
89 Cow::Owned(nfc) => nfc,
90 }
91}
92
93pub fn precompose_path_if_needed(path: &Path) -> Cow<'_, Path> {
95 if !precompose_unicode_enabled() {
96 return Cow::Borrowed(path);
97 }
98 let lossy = path.to_string_lossy();
99 if !has_non_ascii(&lossy) {
100 return Cow::Borrowed(path);
101 }
102 let mut changed = false;
105 let mut out = PathBuf::new();
106 for component in path.components() {
107 match component {
108 std::path::Component::Normal(name) => {
109 let name_str = name.to_string_lossy();
110 match precompose_string_if_needed(&name_str) {
111 Cow::Borrowed(_) => out.push(name),
112 Cow::Owned(nfc) => {
113 changed = true;
114 out.push(nfc);
115 }
116 }
117 }
118 other => out.push(other.as_os_str()),
119 }
120 }
121 if changed {
122 Cow::Owned(out)
123 } else {
124 Cow::Borrowed(path)
125 }
126}
127
128pub fn precompose_os_str_bytes_if_needed(name: &OsStr) -> Cow<'_, [u8]> {
130 #[cfg(unix)]
131 {
132 use std::os::unix::ffi::OsStrExt;
133 precompose_bytes_if_needed(name.as_bytes())
134 }
135 #[cfg(not(unix))]
136 {
137 let owned = name.to_string_lossy().into_owned();
138 match precompose_string_if_needed(&owned) {
139 Cow::Borrowed(_) => Cow::Owned(owned.into_bytes()),
140 Cow::Owned(nfc) => Cow::Owned(nfc.into_bytes()),
141 }
142 }
143}
144
145pub fn precompose_argv_if_needed(args: &mut [String]) {
147 if !precompose_unicode_enabled() {
148 return;
149 }
150 for arg in args.iter_mut() {
151 if has_non_ascii(arg) {
152 *arg = precompose_owned_string_if_needed(std::mem::take(arg));
153 }
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160 use std::sync::{Mutex, OnceLock};
161
162 fn lock_precompose_for_test() -> std::sync::MutexGuard<'static, ()> {
165 static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
166 LOCK.get_or_init(|| Mutex::new(()))
167 .lock()
168 .unwrap_or_else(|poisoned| poisoned.into_inner())
169 }
170
171 #[test]
172 fn nfc_conversion_only_when_enabled() {
173 let _guard = lock_precompose_for_test();
174 let nfd = "A\u{0308}"; let nfc = "\u{00C4}"; set_precompose_unicode(false);
178 assert_eq!(precompose_string_if_needed(nfd).as_ref(), nfd);
179
180 set_precompose_unicode(true);
181 assert_eq!(precompose_string_if_needed(nfd).as_ref(), nfc);
182 assert_eq!(precompose_string_if_needed(nfc).as_ref(), nfc);
183 assert_eq!(precompose_string_if_needed("ascii").as_ref(), "ascii");
184
185 set_precompose_unicode(false);
186 }
187
188 #[test]
189 fn path_components_are_precomposed() {
190 let _guard = lock_precompose_for_test();
191 set_precompose_unicode(true);
192 let nfd = PathBuf::from("d.A\u{0308}/f.A\u{0308}".to_string());
193 let precomposed = precompose_path_if_needed(&nfd);
194 assert_eq!(
195 precomposed.to_string_lossy().as_ref(),
196 "d.\u{00C4}/f.\u{00C4}"
197 );
198 set_precompose_unicode(false);
199 }
200
201 #[test]
204 fn worker_thread_sees_parent_set_value() {
205 let _guard = lock_precompose_for_test();
206 set_precompose_unicode(true);
207 let nfd = "A\u{0308}";
208 let nfc = "\u{00C4}";
209
210 let enabled = std::thread::scope(|s| {
211 s.spawn(|| {
212 assert!(precompose_unicode_enabled());
213 assert_eq!(precompose_string_if_needed(nfd).as_ref(), nfc);
214 precompose_unicode_enabled()
215 })
216 .join()
217 .expect("worker join")
218 });
219 assert!(enabled);
220
221 set_precompose_unicode(false);
222 let disabled = std::thread::scope(|s| {
223 s.spawn(|| {
224 assert!(!precompose_unicode_enabled());
225 assert_eq!(precompose_string_if_needed(nfd).as_ref(), nfd);
226 precompose_unicode_enabled()
227 })
228 .join()
229 .expect("worker join")
230 });
231 assert!(!disabled);
232 }
233}