use std::{num::NonZeroU64, slice};
use oxc_data_structures::{code_buffer::CodeBuffer, slice_iter::SliceIter};
use super::{ESTree, Serializer};
pub struct JsonSafeString<'s>(pub &'s str);
impl ESTree for JsonSafeString<'_> {
#[inline(always)]
fn serialize<S: Serializer>(&self, mut serializer: S) {
serializer.buffer_mut().print_strs_array(["\"", self.0, "\""]);
}
}
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);
}
}
pub struct LoneSurrogatesString<I>(pub I);
impl<I: Iterator<Item = u32> + Clone> ESTree for LoneSurrogatesString<I> {
#[cold]
#[inline(never)]
fn serialize<S: Serializer>(&self, mut serializer: S) {
let buffer = serializer.buffer_mut();
buffer.print_ascii_byte(b'"');
for value in self.0.clone() {
match value {
0x08 => buffer.print_str("\\b"),
0x09 => buffer.print_str("\\t"),
0x0A => buffer.print_str("\\n"),
0x0C => buffer.print_str("\\f"),
0x0D => buffer.print_str("\\r"),
0x22 => buffer.print_str("\\\""),
0x5C => buffer.print_str("\\\\"),
0x00..=0x1F => write_code_point_escape(value, buffer),
_ => match char::from_u32(value) {
Some(ch) => buffer.print_char(ch),
None => write_code_point_escape(value, buffer),
},
}
}
buffer.print_ascii_byte(b'"');
}
}
fn write_code_point_escape(value: u32, buffer: &mut CodeBuffer) {
const HEX: &[u8; 16] = b"0123456789abcdef";
buffer.print_ascii_bytes([
b'\\',
b'u',
HEX[((value >> 12) & 15) as usize],
HEX[((value >> 8) & 15) as usize],
HEX[((value >> 4) & 15) as usize],
HEX[(value & 15) as usize],
]);
}
#[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'\\', UU = b'u', }
static ESCAPE: [Escape; 256] = create_table();
const fn create_table() -> [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, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, ]
}
trait EscapeTable {
fn get_escape_for_byte(b: u8) -> Escape;
fn get_escapes_mask(bytes: [u8; 8]) -> u64;
}
struct StandardEscapeTable;
impl EscapeTable for StandardEscapeTable {
#[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
}
}
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 { iter.advance_unchecked(found_byte_index) };
break 'inner;
}
unsafe { iter.advance_unchecked(8) };
} else {
for (i, &next_byte) in iter.clone().enumerate() {
byte = next_byte;
escape = T::get_escape_for_byte(byte);
if escape != Escape::__ {
unsafe { iter.advance_unchecked(i) };
break 'inner;
}
}
break 'outer;
}
}
let current_ptr = iter.ptr();
unsafe {
let len = current_ptr.offset_from_unsigned(chunk_start_ptr);
let chunk = slice::from_raw_parts(chunk_start_ptr, len);
buffer.print_bytes_short_unchecked(chunk);
}
write_char_escape(escape, byte, buffer);
unsafe { iter.advance_unchecked(1) };
chunk_start_ptr = iter.ptr();
}
unsafe {
let len = iter.end_ptr().offset_from_unsigned(chunk_start_ptr);
let chunk = slice::from_raw_parts(chunk_start_ptr, len);
buffer.print_bytes_short_unchecked(chunk);
}
buffer.print_ascii_byte(b'"');
}
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::CompactSerializer;
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 = CompactSerializer::default();
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 = CompactSerializer::default();
input.clone().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 = CompactSerializer::default();
JsonSafeString(input).serialize(&mut serializer);
let s = serializer.into_string();
assert_eq!(&s, output);
}
}
#[test]
fn serialize_lone_surrogates_string() {
let cases: [(&[u32], &str); 4] = [
(&[0xD800], r#""\ud800""#),
(&[0x61, 0xDC00, 0x62], r#""a\udc00b""#),
(&[0xD800, 0x1F600, 0xDFFF], r#""\ud800😀\udfff""#),
(&[0xD800, 0x08, 0x0A, 0x22, 0x5C, 0x1F], r#""\ud800\b\n\"\\\u001f""#),
];
for (input, output) in cases {
let mut serializer = CompactSerializer::default();
LoneSurrogatesString(input.iter().copied()).serialize(&mut serializer);
let s = serializer.into_string();
assert_eq!(&s, output);
}
}
}