|
|
|
// Protocol Buffers - Google's data interchange format
|
|
|
|
// Copyright 2023 Google LLC. All rights reserved.
|
|
|
|
//
|
|
|
|
// Use of this source code is governed by a BSD-style
|
|
|
|
// license that can be found in the LICENSE file or at
|
|
|
|
// https://developers.google.com/open-source/licenses/bsd
|
|
|
|
|
|
|
|
//! Lossy UTF-8 processing utilities.
|
|
|
|
#![deny(unsafe_op_in_unsafe_fn)]
|
|
|
|
|
|
|
|
// TODO: Replace this with the `std` versions once stable.
|
|
|
|
// This is adapted from https://github.com/rust-lang/rust/blob/e8ee0b7/library/core/src/str/lossy.rs
|
|
|
|
// The adaptations:
|
|
|
|
// - remove `#[unstable]` attributes.
|
|
|
|
// - replace `crate`/`super` paths with their `std` equivalents in code and
|
|
|
|
// examples.
|
|
|
|
// - include `UTF8_CHAR_WIDTH`/`utf8_char_width` from `core::str::validations`.
|
|
|
|
// - use a custom `split_at_unchecked` instead of the nightly one
|
|
|
|
|
|
|
|
use std::fmt;
|
|
|
|
use std::fmt::Formatter;
|
|
|
|
use std::fmt::Write;
|
|
|
|
use std::iter::FusedIterator;
|
|
|
|
use std::str::from_utf8_unchecked;
|
|
|
|
|
|
|
|
/// An item returned by the [`Utf8Chunks`] iterator.
|
|
|
|
///
|
|
|
|
/// A `Utf8Chunk` stores a sequence of [`u8`] up to the first broken character
|
|
|
|
/// when decoding a UTF-8 string.
|
|
|
|
///
|
|
|
|
/// # Examples
|
|
|
|
///
|
|
|
|
/// ```
|
|
|
|
/// use googletest::prelude::*;
|
|
|
|
/// use utf8::Utf8Chunks;
|
|
|
|
///
|
|
|
|
/// // An invalid UTF-8 string
|
|
|
|
/// let bytes = b"foo\xF1\x80bar";
|
|
|
|
///
|
|
|
|
/// // Decode the first `Utf8Chunk`
|
|
|
|
/// let chunk = Utf8Chunks::new(bytes).next().unwrap();
|
|
|
|
///
|
|
|
|
/// // The first three characters are valid UTF-8
|
|
|
|
/// assert_that!("foo", eq(chunk.valid()));
|
|
|
|
///
|
|
|
|
/// // The fourth character is broken
|
|
|
|
/// assert_that!(b"\xF1\x80", eq(chunk.invalid()));
|
|
|
|
/// ```
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
|
|
pub struct Utf8Chunk<'a> {
|
|
|
|
valid: &'a str,
|
|
|
|
invalid: &'a [u8],
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> Utf8Chunk<'a> {
|
|
|
|
/// Returns the next validated UTF-8 substring.
|
|
|
|
///
|
|
|
|
/// This substring can be empty at the start of the string or between
|
|
|
|
/// broken UTF-8 characters.
|
|
|
|
#[must_use]
|
|
|
|
pub fn valid(&self) -> &'a str {
|
|
|
|
self.valid
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns the invalid sequence that caused a failure.
|
|
|
|
///
|
|
|
|
/// The returned slice will have a maximum length of 3 and starts after the
|
|
|
|
/// substring given by [`valid`]. Decoding will resume after this sequence.
|
|
|
|
///
|
|
|
|
/// If empty, this is the last chunk in the string. If non-empty, an
|
|
|
|
/// unexpected byte was encountered or the end of the input was reached
|
|
|
|
/// unexpectedly.
|
|
|
|
///
|
|
|
|
/// Lossy decoding would replace this sequence with [`U+FFFD REPLACEMENT
|
|
|
|
/// CHARACTER`].
|
|
|
|
///
|
|
|
|
/// [`valid`]: Self::valid
|
|
|
|
/// [`U+FFFD REPLACEMENT CHARACTER`]: std::char::REPLACEMENT_CHARACTER
|
|
|
|
#[must_use]
|
|
|
|
pub fn invalid(&self) -> &'a [u8] {
|
|
|
|
self.invalid
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[must_use]
|
|
|
|
pub struct Debug<'a>(&'a [u8]);
|
|
|
|
|
|
|
|
impl fmt::Debug for Debug<'_> {
|
|
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
|
|
|
f.write_char('"')?;
|
|
|
|
|
|
|
|
for chunk in Utf8Chunks::new(self.0) {
|
|
|
|
// Valid part.
|
|
|
|
// Here we partially parse UTF-8 again which is suboptimal.
|
|
|
|
{
|
|
|
|
let valid = chunk.valid();
|
|
|
|
let mut from = 0;
|
|
|
|
for (i, c) in valid.char_indices() {
|
|
|
|
let esc = c.escape_debug();
|
|
|
|
// If char needs escaping, flush backlog so far and write, else skip
|
|
|
|
if esc.len() != 1 {
|
|
|
|
f.write_str(&valid[from..i])?;
|
|
|
|
for c in esc {
|
|
|
|
f.write_char(c)?;
|
|
|
|
}
|
|
|
|
from = i + c.len_utf8();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
f.write_str(&valid[from..])?;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Broken parts of string as hex escape.
|
|
|
|
for &b in chunk.invalid() {
|
|
|
|
write!(f, "\\x{:02X}", b)?;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
f.write_char('"')
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// An iterator used to decode a slice of mostly UTF-8 bytes to string slices
|
|
|
|
/// ([`&str`]) and byte slices ([`&[u8]`][byteslice]).
|
|
|
|
///
|
|
|
|
/// If you want a simple conversion from UTF-8 byte slices to string slices,
|
|
|
|
/// [`from_utf8`] is easier to use.
|
|
|
|
///
|
|
|
|
/// [byteslice]: slice
|
|
|
|
/// [`from_utf8`]: std::str::from_utf8
|
|
|
|
///
|
|
|
|
/// # Examples
|
|
|
|
///
|
|
|
|
/// This can be used to create functionality similar to
|
|
|
|
/// [`String::from_utf8_lossy`] without allocating heap memory:
|
|
|
|
///
|
|
|
|
/// ```
|
|
|
|
/// use utf8::Utf8Chunks;
|
|
|
|
///
|
|
|
|
/// fn from_utf8_lossy<F>(input: &[u8], mut push: F) where F: FnMut(&str) {
|
|
|
|
/// for chunk in Utf8Chunks::new(input) {
|
|
|
|
/// push(chunk.valid());
|
|
|
|
///
|
|
|
|
/// if !chunk.invalid().is_empty() {
|
|
|
|
/// push("\u{FFFD}");
|
|
|
|
/// }
|
|
|
|
/// }
|
|
|
|
/// }
|
|
|
|
/// ```
|
|
|
|
#[must_use = "iterators are lazy and do nothing unless consumed"]
|
|
|
|
#[derive(Clone)]
|
|
|
|
pub struct Utf8Chunks<'a> {
|
|
|
|
source: &'a [u8],
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> Utf8Chunks<'a> {
|
|
|
|
/// Creates a new iterator to decode the bytes.
|
|
|
|
pub fn new(bytes: &'a [u8]) -> Self {
|
|
|
|
Self { source: bytes }
|
|
|
|
}
|
|
|
|
|
|
|
|
#[doc(hidden)]
|
|
|
|
pub fn debug(&self) -> Debug<'_> {
|
|
|
|
Debug(self.source)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> Iterator for Utf8Chunks<'a> {
|
|
|
|
type Item = Utf8Chunk<'a>;
|
|
|
|
|
|
|
|
fn next(&mut self) -> Option<Utf8Chunk<'a>> {
|
|
|
|
if self.source.is_empty() {
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
|
|
|
|
const TAG_CONT_U8: u8 = 128;
|
|
|
|
fn safe_get(xs: &[u8], i: usize) -> u8 {
|
|
|
|
*xs.get(i).unwrap_or(&0)
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut i = 0;
|
|
|
|
let mut valid_up_to = 0;
|
|
|
|
while i < self.source.len() {
|
|
|
|
// SAFETY: `i < self.source.len()` per previous line.
|
|
|
|
// For some reason the following are both significantly slower:
|
|
|
|
// while let Some(&byte) = self.source.get(i) {
|
|
|
|
// while let Some(byte) = self.source.get(i).copied() {
|
|
|
|
let byte = unsafe { *self.source.get_unchecked(i) };
|
|
|
|
i += 1;
|
|
|
|
|
|
|
|
if byte < 128 {
|
|
|
|
// This could be a `1 => ...` case in the match below, but for
|
|
|
|
// the common case of all-ASCII inputs, we bypass loading the
|
|
|
|
// sizeable UTF8_CHAR_WIDTH table into cache.
|
|
|
|
} else {
|
|
|
|
let w = utf8_char_width(byte);
|
|
|
|
|
|
|
|
match w {
|
|
|
|
2 => {
|
|
|
|
if safe_get(self.source, i) & 192 != TAG_CONT_U8 {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
i += 1;
|
|
|
|
}
|
|
|
|
3 => {
|
|
|
|
match (byte, safe_get(self.source, i)) {
|
|
|
|
(0xE0, 0xA0..=0xBF) => (),
|
|
|
|
(0xE1..=0xEC, 0x80..=0xBF) => (),
|
|
|
|
(0xED, 0x80..=0x9F) => (),
|
|
|
|
(0xEE..=0xEF, 0x80..=0xBF) => (),
|
|
|
|
_ => break,
|
|
|
|
}
|
|
|
|
i += 1;
|
|
|
|
if safe_get(self.source, i) & 192 != TAG_CONT_U8 {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
i += 1;
|
|
|
|
}
|
|
|
|
4 => {
|
|
|
|
match (byte, safe_get(self.source, i)) {
|
|
|
|
(0xF0, 0x90..=0xBF) => (),
|
|
|
|
(0xF1..=0xF3, 0x80..=0xBF) => (),
|
|
|
|
(0xF4, 0x80..=0x8F) => (),
|
|
|
|
_ => break,
|
|
|
|
}
|
|
|
|
i += 1;
|
|
|
|
if safe_get(self.source, i) & 192 != TAG_CONT_U8 {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
i += 1;
|
|
|
|
if safe_get(self.source, i) & 192 != TAG_CONT_U8 {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
i += 1;
|
|
|
|
}
|
|
|
|
_ => break,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
valid_up_to = i;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// # Safety
|
|
|
|
/// `index` must be in-bounds for `x`
|
|
|
|
unsafe fn split_at_unchecked(x: &[u8], index: usize) -> (&[u8], &[u8]) {
|
|
|
|
// SAFETY: in-bounds as promised by the caller
|
|
|
|
unsafe { (x.get_unchecked(..index), x.get_unchecked(index..)) }
|
|
|
|
}
|
|
|
|
|
|
|
|
// SAFETY: `i <= self.source.len()` because it is only ever incremented
|
|
|
|
// via `i += 1` and in between every single one of those increments, `i`
|
|
|
|
// is compared against `self.source.len()`. That happens either
|
|
|
|
// literally by `i < self.source.len()` in the while-loop's condition,
|
|
|
|
// or indirectly by `safe_get(self.source, i) & 192 != TAG_CONT_U8`. The
|
|
|
|
// loop is terminated as soon as the latest `i += 1` has made `i` no
|
|
|
|
// longer less than `self.source.len()`, which means it'll be at most
|
|
|
|
// equal to `self.source.len()`.
|
|
|
|
let (inspected, remaining) = unsafe { split_at_unchecked(self.source, i) };
|
|
|
|
self.source = remaining;
|
|
|
|
|
|
|
|
// SAFETY: `valid_up_to <= i` because it is only ever assigned via
|
|
|
|
// `valid_up_to = i` and `i` only increases.
|
|
|
|
let (valid, invalid) = unsafe { split_at_unchecked(inspected, valid_up_to) };
|
|
|
|
|
|
|
|
Some(Utf8Chunk {
|
|
|
|
// SAFETY: All bytes up to `valid_up_to` are valid UTF-8.
|
|
|
|
valid: unsafe { from_utf8_unchecked(valid) },
|
|
|
|
invalid,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl FusedIterator for Utf8Chunks<'_> {}
|
|
|
|
|
|
|
|
impl fmt::Debug for Utf8Chunks<'_> {
|
|
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
|
|
|
f.debug_struct("Utf8Chunks").field("source", &self.debug()).finish()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// https://tools.ietf.org/html/rfc3629
|
|
|
|
const UTF8_CHAR_WIDTH: &[u8; 256] = &[
|
|
|
|
// 1 2 3 4 5 6 7 8 9 A B C D E F
|
|
|
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0
|
|
|
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1
|
|
|
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 2
|
|
|
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 3
|
|
|
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4
|
|
|
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5
|
|
|
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6
|
|
|
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7
|
|
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 8
|
|
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 9
|
|
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // A
|
|
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // B
|
|
|
|
0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // C
|
|
|
|
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // D
|
|
|
|
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, // E
|
|
|
|
4, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // F
|
|
|
|
];
|
|
|
|
|
|
|
|
/// Given a first byte, determines how many bytes are in this UTF-8 character.
|
|
|
|
#[must_use]
|
|
|
|
#[inline]
|
|
|
|
const fn utf8_char_width(b: u8) -> usize {
|
|
|
|
UTF8_CHAR_WIDTH[b as usize] as usize
|
|
|
|
}
|