use std::{collections::HashSet, rc::Rc};
use rquickjs::{
atom::PredefinedAtom, function::This, qjs, Ctx, Exception, Function, Object, Result, Type,
Value,
};
use crate::json::escape::escape_json_string;
const CIRCULAR_REF_DETECTION_DEPTH: usize = 20;
struct StringifyContext<'a, 'js> {
ctx: &'a Ctx<'js>,
result: &'a mut String,
value: &'a Value<'js>,
depth: usize,
indentation: Option<&'a str>,
key: Option<&'a str>,
index: Option<usize>,
parent: Option<&'a Object<'js>>,
ancestors: &'a mut Vec<(usize, Rc<str>)>,
replacer_fn: Option<&'a Function<'js>>,
include_keys_replacer: Option<&'a HashSet<String>>,
itoa_buffer: &'a mut itoa::Buffer,
ryu_buffer: &'a mut ryu::Buffer,
}
#[allow(dead_code)]
pub fn json_stringify<'js>(ctx: &Ctx<'js>, value: Value<'js>) -> Result<Option<String>> {
json_stringify_replacer_space(ctx, value, None, None)
}
#[allow(dead_code)]
pub fn json_stringify_replacer<'js>(
ctx: &Ctx<'js>,
value: Value<'js>,
replacer: Option<Value<'js>>,
) -> Result<Option<String>> {
json_stringify_replacer_space(ctx, value, replacer, None)
}
pub fn json_stringify_replacer_space<'js>(
ctx: &Ctx<'js>,
value: Value<'js>,
replacer: Option<Value<'js>>,
indentation: Option<String>,
) -> Result<Option<String>> {
let mut result = String::with_capacity(128);
let mut replacer_fn = None;
let mut include_keys_replacer = None;
let tmp_function;
let mut itoa_buffer = itoa::Buffer::new();
let mut ryu_buffer = ryu::Buffer::new();
if let Some(replacer) = replacer {
if let Some(function) = replacer.as_function() {
tmp_function = function.clone();
replacer_fn = Some(&tmp_function);
} else if let Some(array) = replacer.as_array() {
let mut filter = HashSet::with_capacity(array.len());
for value in array.clone().into_iter() {
let value = value?;
if let Some(string) = value.as_string() {
filter.insert(string.to_string()?);
} else if let Some(number) = value.as_int() {
filter.insert(itoa_buffer.format(number).to_string());
} else if let Some(number) = value.as_float() {
filter.insert(ryu_buffer.format(number).to_string());
}
}
include_keys_replacer = Some(filter);
}
}
let indentation = indentation.as_deref();
let include_keys_replacer = include_keys_replacer.as_ref();
let mut ancestors = Vec::with_capacity(10);
let mut context = StringifyContext {
ctx,
result: &mut result,
value: &value,
depth: 0,
indentation: None,
key: None,
index: None,
parent: None,
ancestors: &mut ancestors,
replacer_fn,
include_keys_replacer,
itoa_buffer: &mut itoa_buffer,
ryu_buffer: &mut ryu_buffer,
};
match write_primitive(&mut context, false)? {
PrimitiveStatus::Written => {
return Ok(Some(result));
},
PrimitiveStatus::Ignored => {
return Ok(None);
},
_ => {},
}
context.depth += 1;
context.indentation = indentation;
iterate(&mut context, None)?;
Ok(Some(result))
}
#[inline(always)]
#[cold]
fn write_indentation(result: &mut String, indentation: Option<&str>, depth: usize) {
if let Some(indentation) = indentation {
result.push('\n');
result.push_str(&indentation.repeat(depth - 1));
}
}
#[inline(always)]
#[cold]
fn run_to_json<'js>(
context: &mut StringifyContext<'_, 'js>,
js_object: &Object<'js>,
to_json: &Function<'js>,
) -> Result<()> {
let val: Value = to_json.call((This(js_object.clone()),))?;
let indentation = context.indentation.and_then(|indentation| {
matches!(
val.type_of(),
Type::Object | Type::Array | Type::Exception | Type::Proxy
)
.then_some(indentation)
});
append_value(
&mut StringifyContext {
ctx: context.ctx,
result: context.result,
value: &val,
depth: context.depth,
indentation,
key: None,
index: None,
parent: Some(js_object),
ancestors: context.ancestors,
replacer_fn: context.replacer_fn,
include_keys_replacer: context.include_keys_replacer,
itoa_buffer: context.itoa_buffer,
ryu_buffer: context.ryu_buffer,
},
false,
)?;
Ok(())
}
#[derive(PartialEq)]
enum PrimitiveStatus<'js> {
Written,
Ignored,
Iterate(Option<Value<'js>>),
}
#[inline(always)]
#[cold]
fn run_replacer<'js>(
context: &mut StringifyContext<'_, 'js>,
replacer_fn: &Function<'js>,
add_comma: bool,
) -> Result<PrimitiveStatus<'js>> {
let key = context.key;
let index = context.index;
let value = context.value;
let parent = if let Some(parent) = context.parent {
parent.clone()
} else {
let parent = Object::new(context.ctx.clone())?;
parent.set("", value.clone())?;
parent
};
let new_value: Value = replacer_fn.call((
This(parent),
get_key_or_index(context.itoa_buffer, key, index),
value,
))?;
write_primitive2(context, add_comma, Some(new_value))
}
fn write_primitive<'js>(
context: &mut StringifyContext<'_, 'js>,
add_comma: bool,
) -> Result<PrimitiveStatus<'js>> {
if let Some(replacer_fn) = context.replacer_fn {
return run_replacer(context, replacer_fn, add_comma);
}
write_primitive2(context, add_comma, None)
}
fn write_primitive2<'js>(
context: &mut StringifyContext<'_, 'js>,
add_comma: bool,
new_value: Option<Value<'js>>,
) -> Result<PrimitiveStatus<'js>> {
let key = context.key;
let include_keys_replacer = context.include_keys_replacer;
let indentation = context.indentation;
let depth = context.depth;
let value = new_value.as_ref().unwrap_or(context.value);
let type_of = value.type_of();
if context.index.is_none()
&& matches!(
type_of,
Type::Symbol | Type::Undefined | Type::Function | Type::Constructor
)
{
return Ok(PrimitiveStatus::Ignored);
}
if matches!(type_of, Type::BigInt) {
return Err(Exception::throw_type(
context.ctx,
"Do not know how to serialize a BigInt",
));
}
if let (Some(include_keys_replacer), Some(key)) = (include_keys_replacer, key) {
if !include_keys_replacer.contains(key) {
return Ok(PrimitiveStatus::Ignored);
}
}
if let Some(indentation) = indentation {
write_indented_separator(context.result, key, add_comma, indentation, depth);
} else {
write_sep(context.result, add_comma, false);
if let Some(key) = key {
write_key(context.result, key, false);
}
}
match type_of {
Type::Null | Type::Undefined => context.result.push_str("null"),
Type::Bool => {
let bool_str = if unsafe { value.as_bool().unwrap_unchecked() } {
"true"
} else {
"false"
};
context.result.push_str(bool_str);
},
Type::Int => context.result.push_str(
context
.itoa_buffer
.format(unsafe { value.as_int().unwrap_unchecked() }),
),
Type::Float => {
let float_value = unsafe { value.as_float().unwrap_unchecked() };
const EXP_MASK: u64 = 0x7ff0000000000000;
let bits = float_value.to_bits();
if bits & EXP_MASK == EXP_MASK {
context.result.push_str("null");
} else {
let str = context.ryu_buffer.format_finite(float_value);
let bytes = str.as_bytes();
let len = bytes.len();
context.result.push_str(str);
if &bytes[len - 2..] == b".0" {
let len = context.result.len();
unsafe { context.result.as_mut_vec().set_len(len - 2) }
}
}
},
Type::String => {
let js_string = unsafe { value.as_string().unwrap_unchecked() }.clone();
write_string(context.result, js_string.to_cstring()?.as_str());
},
_ => return Ok(PrimitiveStatus::Iterate(new_value)),
}
Ok(PrimitiveStatus::Written)
}
#[inline(always)]
#[cold]
fn write_indented_separator(
result: &mut String,
key: Option<&str>,
add_comma: bool,
indentation: &str,
depth: usize,
) {
write_sep(result, add_comma, true);
result.push_str(&indentation.repeat(depth));
if let Some(key) = key {
write_key(result, key, true);
}
}
#[cold]
fn detect_circular_reference(
ctx: &Ctx<'_>,
value: &Object<'_>,
key: Option<&str>,
index: Option<usize>,
parent: Option<&Object<'_>>,
ancestors: &mut Vec<(usize, Rc<str>)>,
itoa_buffer: &mut itoa::Buffer,
) -> Result<()> {
let parent_ptr = unsafe { qjs::JS_VALUE_GET_PTR(parent.unwrap_unchecked().as_raw()) as usize };
let current_ptr = unsafe { qjs::JS_VALUE_GET_PTR(value.as_raw()) as usize };
while !ancestors.is_empty()
&& match ancestors.last() {
Some((ptr, _)) => ptr != &parent_ptr,
_ => false,
}
{
ancestors.pop();
}
if ancestors.iter().any(|(ptr, _)| ptr == ¤t_ptr) {
let mut iter = ancestors.iter_mut();
let first = &unsafe { iter.next().unwrap_unchecked() }.1;
let mut message = iter.rev().take(4).rev().fold(
String::from("Circular reference detected at: \".."),
|mut acc, (_, key)| {
if !key.starts_with('[') {
acc.push('.');
}
acc.push_str(key);
acc
},
);
if !first.starts_with('[') {
message.push('.');
}
message.push_str(first);
message.push('"');
return Err(Exception::throw_type(ctx, &message));
}
ancestors.push((
current_ptr,
key.map(|k| k.into()).unwrap_or_else(|| {
["[", itoa_buffer.format(index.unwrap_or_default()), "]"]
.concat()
.into()
}),
));
Ok(())
}
#[inline(always)]
fn append_value(context: &mut StringifyContext<'_, '_>, add_comma: bool) -> Result<bool> {
match write_primitive(context, add_comma)? {
PrimitiveStatus::Written => Ok(true),
PrimitiveStatus::Ignored => Ok(false),
PrimitiveStatus::Iterate(new_value) => {
context.depth += 1;
iterate(context, new_value)?;
Ok(true)
},
}
}
#[inline(always)]
fn write_key(string: &mut String, key: &str, indent: bool) {
string.push('"');
escape_json_string(string, key.as_bytes());
string.push_str("\":");
if indent {
string.push(' ');
}
}
#[inline(always)]
fn write_sep(result: &mut String, add_comma: bool, has_indentation: bool) {
if add_comma {
result.push(',');
}
if has_indentation {
result.push('\n');
}
}
#[inline(always)]
fn write_string(string: &mut String, value: &str) {
string.push('"');
escape_json_string(string, value.as_bytes());
string.push('"');
}
#[inline(always)]
fn get_key_or_index<'a>(
itoa_buffer: &'a mut itoa::Buffer,
key: Option<&'a str>,
index: Option<usize>,
) -> &'a str {
key.unwrap_or_else(|| itoa_buffer.format(index.unwrap_or_default()))
}
fn iterate<'js>(
context: &mut StringifyContext<'_, 'js>,
new_value: Option<Value<'js>>,
) -> Result<()> {
let mut add_comma;
let mut value_written;
let elem = new_value.as_ref().unwrap_or(context.value);
let depth = context.depth;
let ctx = context.ctx;
let indentation = context.indentation;
match elem.type_of() {
Type::Object | Type::Exception | Type::Proxy => {
let js_object = unsafe { elem.as_object().unwrap_unchecked() };
let to_json: Value<'js> = js_object.get(PredefinedAtom::ToJSON)?;
if let Some(to_json) = to_json.as_function() {
return run_to_json(context, js_object, to_json);
}
if depth > CIRCULAR_REF_DETECTION_DEPTH {
detect_circular_reference(
ctx,
js_object,
context.key,
context.index,
context.parent,
context.ancestors,
context.itoa_buffer,
)?;
}
context.result.push('{');
value_written = false;
let keys: Vec<String> = {
let collected: Vec<String> = js_object.keys::<String>().flatten().collect();
if collected.is_empty() {
ctx.catch();
ctx.globals()
.get::<_, Object>("Object")
.ok()
.and_then(|o| o.get::<_, Function>("keys").ok())
.and_then(|f| f.call::<_, Vec<String>>((js_object.clone(),)).ok())
.unwrap_or_default()
} else {
collected
}
};
for key in keys {
let val = js_object.get(&key)?;
add_comma = append_value(
&mut StringifyContext {
ctx,
result: context.result,
value: &val,
depth,
key: Some(&key),
indentation,
index: None,
parent: Some(js_object),
ancestors: context.ancestors,
replacer_fn: context.replacer_fn,
include_keys_replacer: context.include_keys_replacer,
itoa_buffer: context.itoa_buffer,
ryu_buffer: context.ryu_buffer,
},
value_written,
)?;
value_written = value_written || add_comma;
}
if value_written {
write_indentation(context.result, indentation, depth);
}
context.result.push('}');
},
Type::Array => {
context.result.push('[');
add_comma = false;
value_written = false;
let js_array = unsafe { elem.as_array().unwrap_unchecked() };
if depth > CIRCULAR_REF_DETECTION_DEPTH {
detect_circular_reference(
ctx,
js_array.as_object(),
context.key,
context.index,
context.parent,
context.ancestors,
context.itoa_buffer,
)?;
}
for (i, val) in js_array.iter::<Value>().enumerate() {
let val = val?;
add_comma = append_value(
&mut StringifyContext {
ctx,
result: context.result,
value: &val,
depth,
key: None,
indentation,
index: Some(i),
parent: Some(js_array),
ancestors: context.ancestors,
replacer_fn: context.replacer_fn,
include_keys_replacer: context.include_keys_replacer,
itoa_buffer: context.itoa_buffer,
ryu_buffer: context.ryu_buffer,
},
add_comma,
)?;
value_written = value_written || add_comma;
}
if value_written {
write_indentation(context.result, indentation, depth);
}
context.result.push(']');
},
_ => {},
}
Ok(())
}