Skip to main content

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}