use std::{num::NonZeroU64, slice};
use oxc_data_structures::{code_buffer::CodeBuffer, pointer_ext::PointerExt};
use super::{ESTree, Serializer};
const fn char_to_bytes<const N: usize>(ch: char) -> [u8; N] {
let mut bytes = [0u8; N];
ch.encode_utf8(&mut bytes);
bytes
}
const LOSSY_REPLACEMENT_CHAR_BYTES: [u8; 3] = char_to_bytes('\u{FFFD}');
const LOSSY_REPLACEMENT_CHAR_FIRST_BYTE: u8 = LOSSY_REPLACEMENT_CHAR_BYTES[0];
pub struct JsonSafeString<'s>(pub &'s str);
impl ESTree for JsonSafeString<'_> {
#[inline(always)]
fn serialize<S: Serializer>(&self, mut serializer: S) {
let buffer = serializer.buffer_mut();
buffer.print_ascii_byte(b'"');
buffer.print_str(self.0);
buffer.print_ascii_byte(b'"');
}
}
pub struct LoneSurrogatesString<'s>(pub &'s str);
impl ESTree for LoneSurrogatesString<'_> {
#[inline(always)]
fn serialize<S: Serializer>(&self, mut serializer: S) {
write_str::<LoneSurrogatesEscapeTable>(self.0, serializer.buffer_mut());
}
}
impl ESTree for str {
fn serialize<S: Serializer>(&self, mut serializer: S) {
write_str::<StandardEscapeTable>(self, serializer.buffer_mut());
}
}
impl ESTree for String {
fn serialize<S: Serializer>(&self, serializer: S) {
self.as_str().serialize(serializer);
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
enum Escape {
__ = 0,
BB = b'b', TT = b't', NN = b'n', FF = b'f', RR = b'r', QU = b'"', BS = b'\\', LO = b'X', UU = b'u', }
static ESCAPE: [Escape; 256] = create_table(Escape::__);
static ESCAPE_LONE_SURROGATES: [Escape; 256] = create_table(Escape::LO);
const fn create_table(lo: Escape) -> [Escape; 256] {
#[allow(clippy::enum_glob_use, clippy::allow_attributes)]
use Escape::*;
[
UU, UU, UU, UU, UU, UU, UU, UU, BB, TT, NN, UU, FF, RR, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, __, __, QU, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, BS, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, lo, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, ]
}
trait EscapeTable {
const LONE_SURROGATES: bool;
fn get_escape_for_byte(b: u8) -> Escape;
fn get_escapes_mask(bytes: [u8; 8]) -> u64;
}
struct StandardEscapeTable;
impl EscapeTable for StandardEscapeTable {
const LONE_SURROGATES: bool = false;
#[inline]
fn get_escape_for_byte(b: u8) -> Escape {
ESCAPE[b as usize]
}
#[inline]
fn get_escapes_mask(bytes: [u8; 8]) -> u64 {
const SPACES: u64 = splat_u64(b' ');
const QUOTES: u64 = splat_u64(b'"');
const SLASHES: u64 = splat_u64(b'\\');
const ONES: u64 = splat_u64(1);
const TOP_BITS: u64 = splat_u64(0x80);
let n = u64::from_ne_bytes(bytes);
let less_than_spaces = n.wrapping_sub(SPACES);
let quotes = (n ^ QUOTES).wrapping_sub(ONES);
let slashes = (n ^ SLASHES).wrapping_sub(ONES);
let escapes = less_than_spaces | quotes | slashes;
let asciis = (!n) & TOP_BITS;
escapes & asciis
}
}
struct LoneSurrogatesEscapeTable;
impl EscapeTable for LoneSurrogatesEscapeTable {
const LONE_SURROGATES: bool = true;
#[inline]
fn get_escape_for_byte(b: u8) -> Escape {
ESCAPE_LONE_SURROGATES[b as usize]
}
#[inline]
fn get_escapes_mask(bytes: [u8; 8]) -> u64 {
const LOSSYS: u64 = splat_u64(LOSSY_REPLACEMENT_CHAR_FIRST_BYTE);
const ONES: u64 = splat_u64(1);
const TOP_BITS: u64 = splat_u64(0x80);
let ascii_escapes = StandardEscapeTable::get_escapes_mask(bytes);
let n = u64::from_ne_bytes(bytes);
let lossys = (n ^ LOSSYS).wrapping_sub(ONES);
let non_ascii = n & TOP_BITS;
let lossys = lossys & non_ascii;
ascii_escapes | lossys
}
}
const fn splat_u64(n: u8) -> u64 {
(u64::MAX / 0xFF) * (n as u64)
}
fn write_str<T: EscapeTable>(s: &str, buffer: &mut CodeBuffer) {
buffer.print_ascii_byte(b'"');
let bytes = s.as_bytes();
let mut chunk_start_ptr = bytes.as_ptr();
let mut iter = bytes.iter();
'outer: loop {
let mut byte;
let mut escape;
'inner: loop {
if let Some(chunk) = iter.as_slice().get(..8) {
let chunk: &[u8; 8] = chunk.try_into().unwrap();
let escapes_mask = T::get_escapes_mask(*chunk);
if let Some(escapes_mask) = NonZeroU64::new(escapes_mask) {
let found_bit_index = if cfg!(target_endian = "little") {
escapes_mask.trailing_zeros()
} else {
escapes_mask.leading_zeros()
};
let found_byte_index = (found_bit_index as usize) / 8;
byte = unsafe { *chunk.get_unchecked(found_byte_index) };
escape = T::get_escape_for_byte(byte);
unsafe { advance_unchecked(&mut iter, found_byte_index) };
break 'inner;
}
unsafe { advance_unchecked(&mut iter, 8) };
} else {
for (i, &next_byte) in iter.clone().enumerate() {
byte = next_byte;
escape = T::get_escape_for_byte(byte);
if escape != Escape::__ {
unsafe { advance_unchecked(&mut iter, i) };
break 'inner;
}
}
break 'outer;
}
}
if T::LONE_SURROGATES && escape == Escape::LO {
let next_2_bytes = unsafe { iter.as_slice().get_unchecked(1..3) };
if next_2_bytes == &LOSSY_REPLACEMENT_CHAR_BYTES[1..] {
unsafe {
let current_ptr = iter.as_slice().as_ptr();
let len = current_ptr.offset_from_usize(chunk_start_ptr);
let chunk = slice::from_raw_parts(chunk_start_ptr, len);
buffer.print_bytes_unchecked(chunk);
}
unsafe { advance_unchecked(&mut iter, 3) };
let hex = iter.as_slice().get(..4).unwrap();
if hex == b"fffd" {
buffer.print_str("\u{FFFD}");
unsafe { advance_unchecked(&mut iter, 4) };
chunk_start_ptr = iter.as_slice().as_ptr();
} else {
#[cfg(debug_assertions)]
for &b in hex {
assert!(matches!(b, b'0'..=b'9' | b'a'..=b'f'));
}
buffer.print_str("\\u");
chunk_start_ptr = iter.as_slice().as_ptr();
unsafe { advance_unchecked(&mut iter, 4) }
}
} else {
unsafe { advance_unchecked(&mut iter, 3) };
}
continue;
}
let current_ptr = iter.as_slice().as_ptr();
unsafe {
let len = current_ptr.offset_from_usize(chunk_start_ptr);
let chunk = slice::from_raw_parts(chunk_start_ptr, len);
buffer.print_bytes_unchecked(chunk);
}
write_char_escape(escape, byte, buffer);
unsafe { advance_unchecked(&mut iter, 1) };
chunk_start_ptr = iter.as_slice().as_ptr();
}
let end_ptr = unsafe { iter.as_slice().as_ptr().add(iter.as_slice().len()) };
unsafe {
let len = end_ptr.offset_from_usize(chunk_start_ptr);
let chunk = slice::from_raw_parts(chunk_start_ptr, len);
buffer.print_bytes_unchecked(chunk);
}
buffer.print_ascii_byte(b'"');
}
#[inline]
unsafe fn advance_unchecked(iter: &mut slice::Iter<u8>, count: usize) {
unsafe {
let new_ptr = iter.as_slice().as_ptr().add(count);
let new_len = iter.as_slice().len() - count;
let slice = slice::from_raw_parts(new_ptr, new_len);
*iter = slice.iter();
};
}
fn write_char_escape(escape: Escape, byte: u8, buffer: &mut CodeBuffer) {
#[expect(clippy::if_not_else)]
if escape != Escape::UU {
unsafe { buffer.print_bytes_unchecked(&[b'\\', escape as u8]) };
} else {
static HEX_DIGITS: [u8; 16] = *b"0123456789abcdef";
let bytes = [
b'\\',
b'u',
b'0',
b'0',
HEX_DIGITS[(byte >> 4) as usize],
HEX_DIGITS[(byte & 0xF) as usize],
];
unsafe { buffer.print_bytes_unchecked(&bytes) }
}
}
#[cfg(test)]
mod tests {
use super::super::CompactTSSerializer;
use super::*;
#[test]
fn serialize_string_slice() {
let cases = [
("", r#""""#),
("foobar", r#""foobar""#),
("\n", r#""\n""#),
("\nfoobar", r#""\nfoobar""#),
("foo\nbar", r#""foo\nbar""#),
("foobar\n", r#""foobar\n""#),
(
"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F",
r#""\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\b\t\n\u000b\f\r\u000e\u000f""#,
),
(
"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F",
r#""\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001a\u001b\u001c\u001d\u001e\u001f""#,
),
(r"\r\n\q", r#""\\r\\n\\q""#),
(
r#"They call me "Bob" but I prefer "Dennis", innit?"#,
r#""They call me \"Bob\" but I prefer \"Dennis\", innit?""#,
),
];
for (input, output) in cases {
let mut serializer = CompactTSSerializer::new(false);
input.serialize(&mut serializer);
let s = serializer.into_string();
assert_eq!(&s, output);
}
}
#[test]
fn serialize_string() {
let cases = [(String::new(), r#""""#), ("foobar".to_string(), r#""foobar""#)];
for (input, output) in cases {
let mut serializer = CompactTSSerializer::new(false);
input.to_string().serialize(&mut serializer);
let s = serializer.into_string();
assert_eq!(&s, output);
}
}
#[test]
fn serialize_json_safe_string() {
let cases = [("", r#""""#), ("a", r#""a""#), ("abc", r#""abc""#)];
for (input, output) in cases {
let mut serializer = CompactTSSerializer::new(false);
JsonSafeString(input).serialize(&mut serializer);
let s = serializer.into_string();
assert_eq!(&s, output);
}
}
#[test]
fn serialize_lone_surrogates_string() {
let cases = [
("\u{FFFD}fffd", "\"\u{FFFD}\""),
("_x_\u{FFFD}fffd_y_\u{FFFD}fffd_z_", "\"_x_\u{FFFD}_y_\u{FFFD}_z_\""),
("\u{FFFD}d834\u{FFFD}d835", r#""\ud834\ud835""#),
("_x_\u{FFFD}d834\u{FFFD}d835", r#""_x_\ud834\ud835""#),
("\u{FFFD}d834\u{FFFD}d835_y_", r#""\ud834\ud835_y_""#),
("_x_\u{FFFD}d834_y_\u{FFFD}d835_z_", r#""_x_\ud834_y_\ud835_z_""#),
(
"They call me \"Bob\" but I prefer \"Den\u{FFFD}d834\\qnis\", innit?",
r#""They call me \"Bob\" but I prefer \"Den\ud834\\qnis\", innit?""#,
),
];
for (input, output) in cases {
let mut serializer = CompactTSSerializer::new(false);
LoneSurrogatesString(input).serialize(&mut serializer);
let s = serializer.into_string();
assert_eq!(&s, output);
}
}
}