#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct Utf8 {
needed: u8,
low: u8,
high: u8,
}
impl Utf8 {
pub(crate) const fn new() -> Self {
Self {
needed: 0,
low: 0x80,
high: 0xbf,
}
}
#[inline]
pub(crate) fn push(&mut self, b: u8) -> bool {
if self.needed == 0 {
match b {
0x00..=0x7f => true,
0xc2..=0xdf => self.expect(1, 0x80, 0xbf),
0xe0 => self.expect(2, 0xa0, 0xbf),
0xe1..=0xec | 0xee..=0xef => self.expect(2, 0x80, 0xbf),
0xed => self.expect(2, 0x80, 0x9f),
0xf0 => self.expect(3, 0x90, 0xbf),
0xf1..=0xf3 => self.expect(3, 0x80, 0xbf),
0xf4 => self.expect(3, 0x80, 0x8f),
_ => false,
}
} else {
if b < self.low || b > self.high {
return false;
}
self.needed -= 1;
self.low = 0x80;
self.high = 0xbf;
true
}
}
fn expect(&mut self, needed: u8, low: u8, high: u8) -> bool {
self.needed = needed;
self.low = low;
self.high = high;
true
}
pub(crate) const fn is_complete(&self) -> bool {
self.needed == 0
}
}
#[cfg(test)]
mod tests {
use super::*;
fn valid(bytes: &[u8]) -> bool {
let mut v = Utf8::new();
bytes.iter().all(|&b| v.push(b)) && v.is_complete()
}
#[test]
fn test_utf8_matches_core_for_every_short_sequence() {
for a in 0u8..=255 {
assert_eq!(valid(&[a]), core::str::from_utf8(&[a]).is_ok(), "{a:#x}");
for b in 0u8..=255 {
let s = [a, b];
assert_eq!(valid(&s), core::str::from_utf8(&s).is_ok(), "{s:x?}");
}
}
for lead in [
0xe0u8, 0xe1, 0xec, 0xed, 0xee, 0xef, 0xf0, 0xf1, 0xf3, 0xf4, 0xf5,
] {
for second in [0x7fu8, 0x80, 0x8f, 0x90, 0x9f, 0xa0, 0xbf, 0xc0] {
for third in [0x7fu8, 0x80, 0xbf, 0xc0] {
let s3 = [lead, second, third];
assert_eq!(valid(&s3), core::str::from_utf8(&s3).is_ok(), "{s3:x?}");
let s4 = [lead, second, third, 0x80];
assert_eq!(valid(&s4), core::str::from_utf8(&s4).is_ok(), "{s4:x?}");
}
}
}
}
#[test]
fn test_utf8_refuses_surrogates_and_overlong() {
assert!(!valid(&[0xed, 0xa0, 0x80]));
assert!(!valid(&[0xc0, 0xaf]));
assert!(!valid(&[0xe0, 0x80, 0xaf]));
assert!(!valid(&[0xf4, 0x90, 0x80, 0x80]));
assert!(!valid(&[0xc3]));
assert!(valid("caf\u{e9} \u{20ac} \u{10ffff}".as_bytes()));
}
}