bstr 1.9.1

A string type that is not required to be valid UTF-8.
Documentation
use regex_automata::{dfa::Automaton, Anchored, Input};

use crate::{
    ext_slice::ByteSlice,
    unicode::fsm::{
        grapheme_break_fwd::GRAPHEME_BREAK_FWD,
        grapheme_break_rev::GRAPHEME_BREAK_REV,
        regional_indicator_rev::REGIONAL_INDICATOR_REV,
    },
    utf8,
};

/// An iterator over grapheme clusters in a byte string.
///
/// This iterator is typically constructed by
/// [`ByteSlice::graphemes`](trait.ByteSlice.html#method.graphemes).
///
/// Unicode defines a grapheme cluster as an *approximation* to a single user
/// visible character. A grapheme cluster, or just "grapheme," is made up of
/// one or more codepoints. For end user oriented tasks, one should generally
/// prefer using graphemes instead of [`Chars`](struct.Chars.html), which
/// always yields one codepoint at a time.
///
/// Since graphemes are made up of one or more codepoints, this iterator yields
/// `&str` elements. When invalid UTF-8 is encountered, replacement codepoints
/// are [substituted](index.html#handling-of-invalid-utf-8).
///
/// This iterator can be used in reverse. When reversed, exactly the same
/// set of grapheme clusters are yielded, but in reverse order.
///
/// This iterator only yields *extended* grapheme clusters, in accordance with
/// [UAX #29](https://www.unicode.org/reports/tr29/tr29-33.html#Grapheme_Cluster_Boundaries).
#[derive(Clone, Debug)]
pub struct Graphemes<'a> {
    bs: &'a [u8],
}

impl<'a> Graphemes<'a> {
    pub(crate) fn new(bs: &'a [u8]) -> Graphemes<'a> {
        Graphemes { bs }
    }

    /// View the underlying data as a subslice of the original data.
    ///
    /// The slice returned has the same lifetime as the original slice, and so
    /// the iterator can continue to be used while this exists.
    ///
    /// # Examples
    ///
    /// ```
    /// use bstr::ByteSlice;
    ///
    /// let mut it = b"abc".graphemes();
    ///
    /// assert_eq!(b"abc", it.as_bytes());
    /// it.next();
    /// assert_eq!(b"bc", it.as_bytes());
    /// it.next();
    /// it.next();
    /// assert_eq!(b"", it.as_bytes());
    /// ```
    #[inline]
    pub fn as_bytes(&self) -> &'a [u8] {
        self.bs
    }
}

impl<'a> Iterator for Graphemes<'a> {
    type Item = &'a str;

    #[inline]
    fn next(&mut self) -> Option<&'a str> {
        let (grapheme, size) = decode_grapheme(self.bs);
        if size == 0 {
            return None;
        }
        self.bs = &self.bs[size..];
        Some(grapheme)
    }
}

impl<'a> DoubleEndedIterator for Graphemes<'a> {
    #[inline]
    fn next_back(&mut self) -> Option<&'a str> {
        let (grapheme, size) = decode_last_grapheme(self.bs);
        if size == 0 {
            return None;
        }
        self.bs = &self.bs[..self.bs.len() - size];
        Some(grapheme)
    }
}

/// An iterator over grapheme clusters in a byte string and their byte index
/// positions.
///
/// This iterator is typically constructed by
/// [`ByteSlice::grapheme_indices`](trait.ByteSlice.html#method.grapheme_indices).
///
/// Unicode defines a grapheme cluster as an *approximation* to a single user
/// visible character. A grapheme cluster, or just "grapheme," is made up of
/// one or more codepoints. For end user oriented tasks, one should generally
/// prefer using graphemes instead of [`Chars`](struct.Chars.html), which
/// always yields one codepoint at a time.
///
/// Since graphemes are made up of one or more codepoints, this iterator
/// yields `&str` elements (along with their start and end byte offsets).
/// When invalid UTF-8 is encountered, replacement codepoints are
/// [substituted](index.html#handling-of-invalid-utf-8). Because of this, the
/// indices yielded by this iterator may not correspond to the length of the
/// grapheme cluster yielded with those indices. For example, when this
/// iterator encounters `\xFF` in the byte string, then it will yield a pair
/// of indices ranging over a single byte, but will provide an `&str`
/// equivalent to `"\u{FFFD}"`, which is three bytes in length. However, when
/// given only valid UTF-8, then all indices are in exact correspondence with
/// their paired grapheme cluster.
///
/// This iterator can be used in reverse. When reversed, exactly the same
/// set of grapheme clusters are yielded, but in reverse order.
///
/// This iterator only yields *extended* grapheme clusters, in accordance with
/// [UAX #29](https://www.unicode.org/reports/tr29/tr29-33.html#Grapheme_Cluster_Boundaries).
#[derive(Clone, Debug)]
pub struct GraphemeIndices<'a> {
    bs: &'a [u8],
    forward_index: usize,
    reverse_index: usize,
}

impl<'a> GraphemeIndices<'a> {
    pub(crate) fn new(bs: &'a [u8]) -> GraphemeIndices<'a> {
        GraphemeIndices { bs, forward_index: 0, reverse_index: bs.len() }
    }

    /// View the underlying data as a subslice of the original data.
    ///
    /// The slice returned has the same lifetime as the original slice, and so
    /// the iterator can continue to be used while this exists.
    ///
    /// # Examples
    ///
    /// ```
    /// use bstr::ByteSlice;
    ///
    /// let mut it = b"abc".grapheme_indices();
    ///
    /// assert_eq!(b"abc", it.as_bytes());
    /// it.next();
    /// assert_eq!(b"bc", it.as_bytes());
    /// it.next();
    /// it.next();
    /// assert_eq!(b"", it.as_bytes());
    /// ```
    #[inline]
    pub fn as_bytes(&self) -> &'a [u8] {
        self.bs
    }
}

impl<'a> Iterator for GraphemeIndices<'a> {
    type Item = (usize, usize, &'a str);

    #[inline]
    fn next(&mut self) -> Option<(usize, usize, &'a str)> {
        let index = self.forward_index;
        let (grapheme, size) = decode_grapheme(self.bs);
        if size == 0 {
            return None;
        }
        self.bs = &self.bs[size..];
        self.forward_index += size;
        Some((index, index + size, grapheme))
    }
}

impl<'a> DoubleEndedIterator for GraphemeIndices<'a> {
    #[inline]
    fn next_back(&mut self) -> Option<(usize, usize, &'a str)> {
        let (grapheme, size) = decode_last_grapheme(self.bs);
        if size == 0 {
            return None;
        }
        self.bs = &self.bs[..self.bs.len() - size];
        self.reverse_index -= size;
        Some((self.reverse_index, self.reverse_index + size, grapheme))
    }
}

/// Decode a grapheme from the given byte string.
///
/// This returns the resulting grapheme (which may be a Unicode replacement
/// codepoint if invalid UTF-8 was found), along with the number of bytes
/// decoded in the byte string. The number of bytes decoded may not be the
/// same as the length of grapheme in the case where invalid UTF-8 is found.
pub fn decode_grapheme(bs: &[u8]) -> (&str, usize) {
    if bs.is_empty() {
        ("", 0)
    } else if bs.len() >= 2
        && bs[0].is_ascii()
        && bs[1].is_ascii()
        && !bs[0].is_ascii_whitespace()
    {
        // FIXME: It is somewhat sad that we have to special case this, but it
        // leads to a significant speed up in predominantly ASCII text. The
        // issue here is that the DFA has a bit of overhead, and running it for
        // every byte in mostly ASCII text results in a bit slowdown. We should
        // re-litigate this once regex-automata 0.3 is out, but it might be
        // hard to avoid the special case. A DFA is always going to at least
        // require some memory access.

        // Safe because all ASCII bytes are valid UTF-8.
        let grapheme = unsafe { bs[..1].to_str_unchecked() };
        (grapheme, 1)
    } else if let Some(hm) = {
        let input = Input::new(bs).anchored(Anchored::Yes);
        GRAPHEME_BREAK_FWD.try_search_fwd(&input).unwrap()
    } {
        // Safe because a match can only occur for valid UTF-8.
        let grapheme = unsafe { bs[..hm.offset()].to_str_unchecked() };
        (grapheme, grapheme.len())
    } else {
        const INVALID: &'static str = "\u{FFFD}";
        // No match on non-empty bytes implies we found invalid UTF-8.
        let (_, size) = utf8::decode_lossy(bs);
        (INVALID, size)
    }
}

fn decode_last_grapheme(bs: &[u8]) -> (&str, usize) {
    if bs.is_empty() {
        ("", 0)
    } else if let Some(hm) = {
        let input = Input::new(bs).anchored(Anchored::Yes);
        GRAPHEME_BREAK_REV.try_search_rev(&input).unwrap()
    } {
        let start = adjust_rev_for_regional_indicator(bs, hm.offset());
        // Safe because a match can only occur for valid UTF-8.
        let grapheme = unsafe { bs[start..].to_str_unchecked() };
        (grapheme, grapheme.len())
    } else {
        const INVALID: &'static str = "\u{FFFD}";
        // No match on non-empty bytes implies we found invalid UTF-8.
        let (_, size) = utf8::decode_last_lossy(bs);
        (INVALID, size)
    }
}

/// Return the correct offset for the next grapheme decoded at the end of the
/// given byte string, where `i` is the initial guess. In particular,
/// `&bs[i..]` represents the candidate grapheme.
///
/// `i` is returned by this function in all cases except when `&bs[i..]` is
/// a pair of regional indicator codepoints. In that case, if an odd number of
/// additional regional indicator codepoints precedes `i`, then `i` is
/// adjusted such that it points to only a single regional indicator.
///
/// This "fixing" is necessary to handle the requirement that a break cannot
/// occur between regional indicators where it would cause an odd number of
/// regional indicators to exist before the break from the *start* of the
/// string. A reverse regex cannot detect this case easily without look-around.
fn adjust_rev_for_regional_indicator(mut bs: &[u8], i: usize) -> usize {
    // All regional indicators use a 4 byte encoding, and we only care about
    // the case where we found a pair of regional indicators.
    if bs.len() - i != 8 {
        return i;
    }
    // Count all contiguous occurrences of regional indicators. If there's an
    // even number of them, then we can accept the pair we found. Otherwise,
    // we can only take one of them.
    //
    // FIXME: This is quadratic in the worst case, e.g., a string of just
    // regional indicator codepoints. A fix probably requires refactoring this
    // code a bit such that we don't rescan regional indicators.
    let mut count = 0;
    while let Some(hm) = {
        let input = Input::new(bs).anchored(Anchored::Yes);
        REGIONAL_INDICATOR_REV.try_search_rev(&input).unwrap()
    } {
        bs = &bs[..hm.offset()];
        count += 1;
    }
    if count % 2 == 0 {
        i
    } else {
        i + 4
    }
}

#[cfg(all(test, feature = "std"))]
mod tests {
    use alloc::{
        string::{String, ToString},
        vec,
        vec::Vec,
    };

    #[cfg(not(miri))]
    use ucd_parse::GraphemeClusterBreakTest;

    use crate::tests::LOSSY_TESTS;

    use super::*;

    #[test]
    #[cfg(not(miri))]
    fn forward_ucd() {
        for (i, test) in ucdtests().into_iter().enumerate() {
            let given = test.grapheme_clusters.concat();
            let got: Vec<String> = Graphemes::new(given.as_bytes())
                .map(|cluster| cluster.to_string())
                .collect();
            assert_eq!(
                test.grapheme_clusters,
                got,
                "\ngrapheme forward break test {} failed:\n\
                 given:    {:?}\n\
                 expected: {:?}\n\
                 got:      {:?}\n",
                i,
                uniescape(&given),
                uniescape_vec(&test.grapheme_clusters),
                uniescape_vec(&got),
            );
        }
    }

    #[test]
    #[cfg(not(miri))]
    fn reverse_ucd() {
        for (i, test) in ucdtests().into_iter().enumerate() {
            let given = test.grapheme_clusters.concat();
            let mut got: Vec<String> = Graphemes::new(given.as_bytes())
                .rev()
                .map(|cluster| cluster.to_string())
                .collect();
            got.reverse();
            assert_eq!(
                test.grapheme_clusters,
                got,
                "\n\ngrapheme reverse break test {} failed:\n\
                 given:    {:?}\n\
                 expected: {:?}\n\
                 got:      {:?}\n",
                i,
                uniescape(&given),
                uniescape_vec(&test.grapheme_clusters),
                uniescape_vec(&got),
            );
        }
    }

    #[test]
    fn forward_lossy() {
        for &(expected, input) in LOSSY_TESTS {
            let got = Graphemes::new(input.as_bytes()).collect::<String>();
            assert_eq!(expected, got);
        }
    }

    #[test]
    fn reverse_lossy() {
        for &(expected, input) in LOSSY_TESTS {
            let expected: String = expected.chars().rev().collect();
            let got =
                Graphemes::new(input.as_bytes()).rev().collect::<String>();
            assert_eq!(expected, got);
        }
    }

    #[cfg(not(miri))]
    fn uniescape(s: &str) -> String {
        s.chars().flat_map(|c| c.escape_unicode()).collect::<String>()
    }

    #[cfg(not(miri))]
    fn uniescape_vec(strs: &[String]) -> Vec<String> {
        strs.iter().map(|s| uniescape(s)).collect()
    }

    /// Return all of the UCD for grapheme breaks.
    #[cfg(not(miri))]
    fn ucdtests() -> Vec<GraphemeClusterBreakTest> {
        const TESTDATA: &'static str =
            include_str!("data/GraphemeBreakTest.txt");

        let mut tests = vec![];
        for mut line in TESTDATA.lines() {
            line = line.trim();
            if line.starts_with("#") || line.contains("surrogate") {
                continue;
            }
            tests.push(line.parse().unwrap());
        }
        tests
    }
}