#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct PartialUtf8 {
pub value: u32,
pub n_remain: i32,
}
impl PartialUtf8 {
pub const fn new(value: u32, n_remain: i32) -> Self {
Self { value, n_remain }
}
}
const CHAR_LOOKUP: [usize; 16] = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 3, 4];
const PIECE_LOOKUP: [i32; 16] = [1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 2, 2, 3, 4];
#[inline]
pub(crate) fn byte_at(src: &[u8], i: usize) -> u8 {
if i < src.len() {
src[i]
} else {
0
}
}
pub(crate) fn decode_char(src: &[u8], pos: usize) -> (u32, usize) {
let first_byte = byte_at(src, pos);
let highbits = (first_byte >> 4) as usize;
let len = CHAR_LOOKUP[highbits];
let mask = (1u32 << (8 - len)) - 1;
let mut value = first_byte as u32 & mask;
let end = pos + len; let mut p = pos + 1;
while p < end && byte_at(src, p) != 0 {
value = (value << 6) + (byte_at(src, p) & 0x3F) as u32;
p += 1;
}
(value, p)
}
pub(crate) fn decode_piece(src: &[u8], partial_start: PartialUtf8) -> (Vec<u32>, PartialUtf8) {
let mut pos = 0usize;
let mut code_points: Vec<u32> = Vec::with_capacity(src.len() + 1);
let mut value = partial_start.value;
let mut n_remain = partial_start.n_remain;
while byte_at(src, pos) != 0 && n_remain > 0 {
let next_byte = byte_at(src, pos);
if (next_byte >> 6) != 2 {
code_points.push(0);
return (code_points, PartialUtf8::new(0, -1));
}
value = (value << 6) + (next_byte & 0x3F) as u32;
pos += 1;
n_remain -= 1;
}
if partial_start.n_remain > 0 && n_remain == 0 {
code_points.push(value);
}
while byte_at(src, pos) != 0 {
let first_byte = byte_at(src, pos);
let highbits = (first_byte >> 4) as usize;
n_remain = PIECE_LOOKUP[highbits] - 1;
if n_remain < 0 {
code_points.clear();
code_points.push(0);
return (code_points, PartialUtf8::new(0, n_remain));
}
let mask = (1u32 << (7 - n_remain)) - 1;
value = first_byte as u32 & mask;
pos += 1;
while byte_at(src, pos) != 0 && n_remain > 0 {
value = (value << 6) + (byte_at(src, pos) & 0x3F) as u32;
pos += 1;
n_remain -= 1;
}
if n_remain == 0 {
code_points.push(value);
}
}
code_points.push(0);
(code_points, PartialUtf8::new(value, n_remain))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ascii_piece_decodes_to_its_bytes_plus_a_terminator() {
let (v, p) = decode_piece(b"abc", PartialUtf8::default());
assert_eq!(v, vec![0x61, 0x62, 0x63, 0]);
assert_eq!(p.n_remain, 0);
}
#[test]
fn empty_piece_is_just_the_terminator() {
let (v, p) = decode_piece(b"", PartialUtf8::default());
assert_eq!(v, vec![0]);
assert_eq!(p, PartialUtf8::default());
}
#[test]
fn multibyte_piece_decodes_whole() {
let (v, p) = decode_piece("é€𝄞".as_bytes(), PartialUtf8::default());
assert_eq!(v, vec![0xE9, 0x20AC, 0x1D11E, 0]);
assert_eq!(p.n_remain, 0);
}
#[test]
fn a_codepoint_split_across_two_pieces_is_carried_and_completed() {
let euro = "€".as_bytes();
let (v1, p1) = decode_piece(&euro[..1], PartialUtf8::default());
assert_eq!(v1, vec![0], "no complete code point yet");
assert_eq!(p1.n_remain, 2, "two continuation bytes still expected");
let (v2, p2) = decode_piece(&euro[1..2], p1);
assert_eq!(v2, vec![0], "still incomplete after one continuation byte");
assert_eq!(p2.n_remain, 1);
let (v3, p3) = decode_piece(&euro[2..], p2);
assert_eq!(v3, vec![0x20AC, 0], "completes on the last byte");
assert_eq!(p3.n_remain, 0);
}
#[test]
fn a_split_codepoint_followed_by_more_text_decodes_both() {
let bytes = "€!".as_bytes();
let (_, p1) = decode_piece(&bytes[..1], PartialUtf8::default());
let (v, p) = decode_piece(&bytes[1..], p1);
assert_eq!(v, vec![0x20AC, b'!' as u32, 0]);
assert_eq!(p.n_remain, 0);
}
#[test]
fn a_lead_byte_where_a_continuation_was_due_is_an_invalid_sequence() {
let p1 = PartialUtf8::new(0x02, 1); let (v, p) = decode_piece(b"A", p1);
assert_eq!(v, vec![0]);
assert_eq!(p, PartialUtf8::new(0, -1), "n_remain = -1 marks invalid");
}
#[test]
fn a_stray_continuation_byte_clears_everything_already_decoded() {
let (v, p) = decode_piece(&[b'a', b'b', 0x80], PartialUtf8::default());
assert_eq!(v, vec![0]);
assert_eq!(p.n_remain, -1);
}
#[test]
fn a_truncated_lead_byte_leaves_a_partial_not_an_error() {
let (v, p) = decode_piece(&[0xE2], PartialUtf8::default());
assert_eq!(v, vec![0]);
assert_eq!(p, PartialUtf8::new(0x02, 2));
}
#[test]
fn an_embedded_nul_ends_the_piece_as_it_does_in_c() {
let (v, _) = decode_piece(b"ab\0cd", PartialUtf8::default());
assert_eq!(v, vec![b'a' as u32, b'b' as u32, 0]);
}
#[test]
fn decode_char_reads_one_codepoint_and_reports_its_width() {
assert_eq!(decode_char(b"a", 0), (0x61, 1));
assert_eq!(decode_char("é".as_bytes(), 0), (0xE9, 2));
assert_eq!(decode_char("€".as_bytes(), 0), (0x20AC, 3));
assert_eq!(decode_char("𝄞".as_bytes(), 0), (0x1D11E, 4));
assert_eq!(decode_char("aé".as_bytes(), 1), (0xE9, 3));
}
#[test]
fn decode_char_does_not_run_past_a_truncated_sequence() {
let (value, end) = decode_char(&[0xE2], 0);
assert_eq!(end, 1, "stopped at the buffer end, not at pos+3");
assert_eq!(value, 0x02);
}
#[test]
fn the_two_lookup_tables_disagree_on_continuation_bytes_and_that_is_deliberate() {
assert_eq!(CHAR_LOOKUP[8], 1);
assert_eq!(decode_char(&[0x80], 0), (0x00, 1));
assert_eq!(PIECE_LOOKUP[8], 0);
}
}