gix_utils/str.rs
1use std::{borrow::Cow, ffi::OsStr, path::Path};
2
3/// Assure that `s` is precomposed, i.e. `ä` is a single code-point, and not two i.e. `a` and `<umlaut>`.
4///
5/// At the expense of extra-compute, it does nothing if there is no work to be done, returning the original input without allocating.
6pub fn precompose(s: Cow<'_, str>) -> Cow<'_, str> {
7 use unicode_normalization::{char, is_nfc};
8 if is_nfc(s.as_ref()) {
9 return s;
10 }
11
12 /// Compose filesystem-decomposed characters without the canonical reordering that full NFC performs.
13 /// Non-composable combining marks must retain their byte order to keep matching index entries.
14 ///
15 /// * `out` holds the characters emitted so far; composition replaces its starter, otherwise `ch` is appended.
16 /// * `starter` is the index of the latest class-zero character eligible for composition, if one exists.
17 /// * `max_class` is the highest combining class appended since the starter, used to block invalid composition.
18 /// * `ch` is the next canonically decomposed character to process.
19 ///
20 /// Returns `true` if `ch` was composed into the starter, or `false` if it was appended unchanged.
21 fn push(out: &mut Vec<char>, starter: &mut Option<usize>, max_class: &mut u8, ch: char) -> bool {
22 let class = char::canonical_combining_class(ch);
23 if let Some(starter) = *starter {
24 if *max_class == 0 || *max_class < class {
25 if let Some(composed) = char::compose(out[starter], ch) {
26 out[starter] = composed;
27 return true;
28 }
29 }
30 }
31 if class == 0 {
32 *starter = Some(out.len());
33 *max_class = 0;
34 } else {
35 *max_class = (*max_class).max(class);
36 }
37 out.push(ch);
38 false
39 }
40
41 let mut out = Vec::with_capacity(s.chars().count());
42 let mut starter = None;
43 let mut max_class = 0;
44 let mut changed = false;
45 for ch in s.chars() {
46 let mut first = true;
47 char::decompose_canonical(ch, |decomposed| {
48 changed |= !first || decomposed != ch;
49 first = false;
50 changed |= push(&mut out, &mut starter, &mut max_class, decomposed);
51 });
52 }
53 if changed {
54 Cow::Owned(out.into_iter().collect())
55 } else {
56 s
57 }
58}
59
60/// Assure that `s` is decomposed, i.e. `ä` turns into `a` and `<umlaut>`.
61///
62/// At the expense of extra-compute, it does nothing if there is no work to be done, returning the original input without allocating.
63pub fn decompose(s: Cow<'_, str>) -> Cow<'_, str> {
64 use unicode_normalization::{UnicodeNormalization, is_nfd};
65 if is_nfd(s.as_ref()) {
66 s
67 } else {
68 Cow::Owned(s.as_ref().nfd().collect())
69 }
70}
71
72/// Return the precomposed version of `path`, or `path` itself if it contained illformed unicode,
73/// or if the unicode version didn't contains decomposed unicode.
74/// Otherwise, similar to [`precompose()`]
75pub fn precompose_path(path: Cow<'_, Path>) -> Cow<'_, Path> {
76 match path.to_str() {
77 None => path,
78 Some(maybe_decomposed) => match precompose(maybe_decomposed.into()) {
79 Cow::Borrowed(_) => path,
80 Cow::Owned(precomposed) => Cow::Owned(precomposed.into()),
81 },
82 }
83}
84
85/// Return the precomposed version of `name`, or `name` itself if it contained illformed unicode,
86/// or if the unicode version didn't contains decomposed unicode.
87/// Otherwise, similar to [`precompose()`]
88pub fn precompose_os_string(name: Cow<'_, OsStr>) -> Cow<'_, OsStr> {
89 match name.to_str() {
90 None => name,
91 Some(maybe_decomposed) => match precompose(maybe_decomposed.into()) {
92 Cow::Borrowed(_) => name,
93 Cow::Owned(precomposed) => Cow::Owned(precomposed.into()),
94 },
95 }
96}
97
98/// Return the precomposed version of `s`, or `s` itself if it contained illformed unicode,
99/// or if the unicode version didn't contains decomposed unicode.
100/// Otherwise, similar to [`precompose()`]
101#[cfg(feature = "bstr")]
102pub fn precompose_bstr(s: Cow<'_, bstr::BStr>) -> Cow<'_, bstr::BStr> {
103 use bstr::ByteSlice;
104 match s.to_str().ok() {
105 None => s,
106 Some(maybe_decomposed) => match precompose(maybe_decomposed.into()) {
107 Cow::Borrowed(_) => s,
108 Cow::Owned(precomposed) => Cow::Owned(precomposed.into()),
109 },
110 }
111}