1use std::{collections::HashSet, rc::Rc};
4
5use rquickjs::{
6 atom::PredefinedAtom, function::This, qjs, Ctx, Exception, Function, Object, Result, Type,
7 Value,
8};
9
10use crate::json::escape::escape_json_string;
11
12const CIRCULAR_REF_DETECTION_DEPTH: usize = 20;
13
14struct StringifyContext<'a, 'js> {
15 ctx: &'a Ctx<'js>,
16 result: &'a mut String,
17 value: &'a Value<'js>,
18 depth: usize,
19 indentation: Option<&'a str>,
20 key: Option<&'a str>,
21 index: Option<usize>,
22 parent: Option<&'a Object<'js>>,
23 ancestors: &'a mut Vec<(usize, Rc<str>)>,
24 replacer_fn: Option<&'a Function<'js>>,
25 include_keys_replacer: Option<&'a HashSet<String>>,
26 itoa_buffer: &'a mut itoa::Buffer,
27 ryu_buffer: &'a mut ryu::Buffer,
28}
29
30#[allow(dead_code)]
31pub fn json_stringify<'js>(ctx: &Ctx<'js>, value: Value<'js>) -> Result<Option<String>> {
32 json_stringify_replacer_space(ctx, value, None, None)
33}
34
35#[allow(dead_code)]
36pub fn json_stringify_replacer<'js>(
37 ctx: &Ctx<'js>,
38 value: Value<'js>,
39 replacer: Option<Value<'js>>,
40) -> Result<Option<String>> {
41 json_stringify_replacer_space(ctx, value, replacer, None)
42}
43
44pub fn json_stringify_replacer_space<'js>(
45 ctx: &Ctx<'js>,
46 value: Value<'js>,
47 replacer: Option<Value<'js>>,
48 indentation: Option<String>,
49) -> Result<Option<String>> {
50 let mut result = String::with_capacity(128);
51 let mut replacer_fn = None;
52 let mut include_keys_replacer = None;
53
54 let tmp_function;
55
56 let mut itoa_buffer = itoa::Buffer::new();
57 let mut ryu_buffer = ryu::Buffer::new();
58
59 if let Some(replacer) = replacer {
60 if let Some(function) = replacer.as_function() {
61 tmp_function = function.clone();
62 replacer_fn = Some(&tmp_function);
63 } else if let Some(array) = replacer.as_array() {
64 let mut filter = HashSet::with_capacity(array.len());
65 for value in array.clone().into_iter() {
66 let value = value?;
67 if let Some(string) = value.as_string() {
68 filter.insert(string.to_string()?);
69 } else if let Some(number) = value.as_int() {
70 filter.insert(itoa_buffer.format(number).to_string());
71 } else if let Some(number) = value.as_float() {
72 filter.insert(ryu_buffer.format(number).to_string());
73 }
74 }
75 include_keys_replacer = Some(filter);
76 }
77 }
78
79 let indentation = indentation.as_deref();
80 let include_keys_replacer = include_keys_replacer.as_ref();
81
82 let mut ancestors = Vec::with_capacity(10);
83
84 let mut context = StringifyContext {
85 ctx,
86 result: &mut result,
87 value: &value,
88 depth: 0,
89 indentation: None,
90 key: None,
91 index: None,
92 parent: None,
93 ancestors: &mut ancestors,
94 replacer_fn,
95 include_keys_replacer,
96 itoa_buffer: &mut itoa_buffer,
97 ryu_buffer: &mut ryu_buffer,
98 };
99
100 match write_primitive(&mut context, false)? {
101 PrimitiveStatus::Written => {
102 return Ok(Some(result));
103 },
104 PrimitiveStatus::Ignored => {
105 return Ok(None);
106 },
107 _ => {},
108 }
109
110 context.depth += 1;
111 context.indentation = indentation;
112 iterate(&mut context, None)?;
113 Ok(Some(result))
114}
115
116#[inline(always)]
117#[cold]
118fn write_indentation(result: &mut String, indentation: Option<&str>, depth: usize) {
119 if let Some(indentation) = indentation {
120 result.push('\n');
121 result.push_str(&indentation.repeat(depth - 1));
122 }
123}
124
125#[inline(always)]
126#[cold]
127fn run_to_json<'js>(
128 context: &mut StringifyContext<'_, 'js>,
129 js_object: &Object<'js>,
130) -> Result<()> {
131 let to_json = js_object.get::<_, Function>(PredefinedAtom::ToJSON)?;
132 let val: Value = to_json.call((This(js_object.clone()),))?;
133
134 let indentation = context.indentation.and_then(|indentation| {
136 matches!(
137 val.type_of(),
138 Type::Object | Type::Array | Type::Exception | Type::Proxy
139 )
140 .then_some(indentation)
141 });
142
143 append_value(
144 &mut StringifyContext {
145 ctx: context.ctx,
146 result: context.result,
147 value: &val,
148 depth: context.depth,
149 indentation,
150 key: None,
151 index: None,
152 parent: Some(js_object),
153 ancestors: context.ancestors,
154 replacer_fn: context.replacer_fn,
155 include_keys_replacer: context.include_keys_replacer,
156 itoa_buffer: context.itoa_buffer,
157 ryu_buffer: context.ryu_buffer,
158 },
159 false,
160 )?;
161 Ok(())
162}
163
164#[derive(PartialEq)]
165enum PrimitiveStatus<'js> {
166 Written,
167 Ignored,
168 Iterate(Option<Value<'js>>),
169}
170
171#[inline(always)]
172#[cold]
173fn run_replacer<'js>(
174 context: &mut StringifyContext<'_, 'js>,
175 replacer_fn: &Function<'js>,
176 add_comma: bool,
177) -> Result<PrimitiveStatus<'js>> {
178 let key = context.key;
179 let index = context.index;
180 let value = context.value;
181 let parent = if let Some(parent) = context.parent {
182 parent.clone()
183 } else {
184 let parent = Object::new(context.ctx.clone())?;
185 parent.set("", value.clone())?;
186 parent
187 };
188 let new_value: Value = replacer_fn.call((
189 This(parent),
190 get_key_or_index(context.itoa_buffer, key, index),
191 value,
192 ))?;
193
194 write_primitive2(context, add_comma, Some(new_value))
195}
196
197fn write_primitive<'js>(
198 context: &mut StringifyContext<'_, 'js>,
199 add_comma: bool,
200) -> Result<PrimitiveStatus<'js>> {
201 if let Some(replacer_fn) = context.replacer_fn {
202 return run_replacer(context, replacer_fn, add_comma);
203 }
204
205 write_primitive2(context, add_comma, None)
206}
207
208fn write_primitive2<'js>(
209 context: &mut StringifyContext<'_, 'js>,
210 add_comma: bool,
211 new_value: Option<Value<'js>>,
212) -> Result<PrimitiveStatus<'js>> {
213 let key = context.key;
214 let index = context.index;
215 let include_keys_replacer = context.include_keys_replacer;
216 let indentation = context.indentation;
217 let depth = context.depth;
218
219 let value = new_value.as_ref().unwrap_or(context.value);
220
221 let type_of = value.type_of();
222
223 if context.index.is_none()
224 && matches!(
225 type_of,
226 Type::Symbol | Type::Undefined | Type::Function | Type::Constructor
227 )
228 {
229 return Ok(PrimitiveStatus::Ignored);
230 }
231
232 if matches!(type_of, Type::BigInt) {
233 return Err(Exception::throw_type(
234 context.ctx,
235 "Do not know how to serialize a BigInt",
236 ));
237 }
238
239 if let Some(include_keys_replacer) = include_keys_replacer {
240 let key = get_key_or_index(context.itoa_buffer, key, index);
241 if !include_keys_replacer.contains(key) {
242 return Ok(PrimitiveStatus::Ignored);
243 }
244 };
245
246 if let Some(indentation) = indentation {
247 write_indented_separator(context.result, key, add_comma, indentation, depth);
248 } else {
249 write_sep(context.result, add_comma, false);
250 if let Some(key) = key {
251 write_key(context.result, key, false);
252 }
253 }
254
255 match type_of {
256 Type::Null | Type::Undefined => context.result.push_str("null"),
257 Type::Bool => {
258 let bool_str = if unsafe { value.as_bool().unwrap_unchecked() } {
259 "true"
260 } else {
261 "false"
262 };
263 context.result.push_str(bool_str);
264 },
265 Type::Int => context.result.push_str(
266 context
267 .itoa_buffer
268 .format(unsafe { value.as_int().unwrap_unchecked() }),
269 ),
270 Type::Float => {
271 let float_value = unsafe { value.as_float().unwrap_unchecked() };
272 const EXP_MASK: u64 = 0x7ff0000000000000;
273 let bits = float_value.to_bits();
274 if bits & EXP_MASK == EXP_MASK {
275 context.result.push_str("null");
276 } else {
277 let str = context.ryu_buffer.format_finite(float_value);
278
279 let bytes = str.as_bytes();
280 let len = bytes.len();
281
282 context.result.push_str(str);
283
284 if &bytes[len - 2..] == b".0" {
285 let len = context.result.len();
286 unsafe { context.result.as_mut_vec().set_len(len - 2) }
287 }
288 }
289 },
290 Type::String => {
291 let js_string = unsafe { value.as_string().unwrap_unchecked() }.clone();
292 write_string(context.result, js_string.to_cstring()?.as_str());
293 },
294 _ => return Ok(PrimitiveStatus::Iterate(new_value)),
295 }
296 Ok(PrimitiveStatus::Written)
297}
298
299#[inline(always)]
300#[cold]
301fn write_indented_separator(
302 result: &mut String,
303 key: Option<&str>,
304 add_comma: bool,
305 indentation: &str,
306 depth: usize,
307) {
308 write_sep(result, add_comma, true);
309 result.push_str(&indentation.repeat(depth));
310 if let Some(key) = key {
311 write_key(result, key, true);
312 }
313}
314
315#[cold]
316fn detect_circular_reference(
317 ctx: &Ctx<'_>,
318 value: &Object<'_>,
319 key: Option<&str>,
320 index: Option<usize>,
321 parent: Option<&Object<'_>>,
322 ancestors: &mut Vec<(usize, Rc<str>)>,
323 itoa_buffer: &mut itoa::Buffer,
324) -> Result<()> {
325 let parent_ptr = unsafe { qjs::JS_VALUE_GET_PTR(parent.unwrap_unchecked().as_raw()) as usize };
326 let current_ptr = unsafe { qjs::JS_VALUE_GET_PTR(value.as_raw()) as usize };
327
328 while !ancestors.is_empty()
329 && match ancestors.last() {
330 Some((ptr, _)) => ptr != &parent_ptr,
331 _ => false,
332 }
333 {
334 ancestors.pop();
335 }
336
337 if ancestors.iter().any(|(ptr, _)| ptr == ¤t_ptr) {
338 let mut iter = ancestors.iter_mut();
339
340 let first = &unsafe { iter.next().unwrap_unchecked() }.1;
341
342 let mut message = iter.rev().take(4).rev().fold(
343 String::from("Circular reference detected at: \".."),
344 |mut acc, (_, key)| {
345 if !key.starts_with('[') {
346 acc.push('.');
347 }
348 acc.push_str(key);
349 acc
350 },
351 );
352
353 if !first.starts_with('[') {
354 message.push('.');
355 }
356
357 message.push_str(first);
358 message.push('"');
359
360 return Err(Exception::throw_type(ctx, &message));
361 }
362 ancestors.push((
363 current_ptr,
364 key.map(|k| k.into()).unwrap_or_else(|| {
365 ["[", itoa_buffer.format(index.unwrap_or_default()), "]"]
366 .concat()
367 .into()
368 }),
369 ));
370
371 Ok(())
372}
373
374#[inline(always)]
375fn append_value(context: &mut StringifyContext<'_, '_>, add_comma: bool) -> Result<bool> {
376 match write_primitive(context, add_comma)? {
377 PrimitiveStatus::Written => Ok(true),
378 PrimitiveStatus::Ignored => Ok(false),
379 PrimitiveStatus::Iterate(new_value) => {
380 context.depth += 1;
381 iterate(context, new_value)?;
382 Ok(true)
383 },
384 }
385}
386
387#[inline(always)]
388fn write_key(string: &mut String, key: &str, indent: bool) {
389 string.push('"');
390 escape_json_string(string, key.as_bytes());
391 string.push_str("\":");
392 if indent {
393 string.push(' ');
394 }
395}
396
397#[inline(always)]
398fn write_sep(result: &mut String, add_comma: bool, has_indentation: bool) {
399 if add_comma {
400 result.push(',');
401 }
402 if has_indentation {
403 result.push('\n');
404 }
405}
406
407#[inline(always)]
408fn write_string(string: &mut String, value: &str) {
409 string.push('"');
410 escape_json_string(string, value.as_bytes());
411 string.push('"');
412}
413
414#[inline(always)]
415fn get_key_or_index<'a>(
416 itoa_buffer: &'a mut itoa::Buffer,
417 key: Option<&'a str>,
418 index: Option<usize>,
419) -> &'a str {
420 key.unwrap_or_else(|| itoa_buffer.format(index.unwrap_or_default()))
421}
422
423fn iterate<'js>(
424 context: &mut StringifyContext<'_, 'js>,
425 new_value: Option<Value<'js>>,
426) -> Result<()> {
427 let mut add_comma;
428 let mut value_written;
429 let elem = new_value.as_ref().unwrap_or(context.value);
430 let depth = context.depth;
431 let ctx = context.ctx;
432 let indentation = context.indentation;
433 match elem.type_of() {
434 Type::Object | Type::Exception | Type::Proxy => {
435 let js_object = unsafe { elem.as_object().unwrap_unchecked() };
436 if js_object.contains_key(PredefinedAtom::ToJSON)? {
437 return run_to_json(context, js_object);
438 }
439
440 if depth > CIRCULAR_REF_DETECTION_DEPTH {
442 detect_circular_reference(
443 ctx,
444 js_object,
445 context.key,
446 context.index,
447 context.parent,
448 context.ancestors,
449 context.itoa_buffer,
450 )?;
451 }
452
453 context.result.push('{');
454
455 value_written = false;
456
457 let keys: Vec<String> = {
460 let collected: Vec<String> = js_object.keys::<String>().flatten().collect();
461 if collected.is_empty() {
462 ctx.catch();
464 ctx.globals()
465 .get::<_, Object>("Object")
466 .ok()
467 .and_then(|o| o.get::<_, Function>("keys").ok())
468 .and_then(|f| f.call::<_, Vec<String>>((js_object.clone(),)).ok())
469 .unwrap_or_default()
470 } else {
471 collected
472 }
473 };
474
475 for key in keys {
476 let val = js_object.get(&key)?;
477
478 add_comma = append_value(
479 &mut StringifyContext {
480 ctx,
481 result: context.result,
482 value: &val,
483 depth,
484 key: Some(&key),
485 indentation,
486 index: None,
487 parent: Some(js_object),
488 ancestors: context.ancestors,
489 replacer_fn: context.replacer_fn,
490 include_keys_replacer: context.include_keys_replacer,
491 itoa_buffer: context.itoa_buffer,
492 ryu_buffer: context.ryu_buffer,
493 },
494 value_written,
495 )?;
496 value_written = value_written || add_comma;
497 }
498
499 if value_written {
500 write_indentation(context.result, indentation, depth);
501 }
502 context.result.push('}');
503 },
504 Type::Array => {
505 context.result.push('[');
506 add_comma = false;
507 value_written = false;
508 let js_array = unsafe { elem.as_array().unwrap_unchecked() };
509 if depth > CIRCULAR_REF_DETECTION_DEPTH {
511 detect_circular_reference(
512 ctx,
513 js_array.as_object(),
514 context.key,
515 context.index,
516 context.parent,
517 context.ancestors,
518 context.itoa_buffer,
519 )?;
520 }
521 for (i, val) in js_array.iter::<Value>().enumerate() {
522 let val = val?;
523 add_comma = append_value(
524 &mut StringifyContext {
525 ctx,
526 result: context.result,
527 value: &val,
528 depth,
529 key: None,
530 indentation,
531 index: Some(i),
532 parent: Some(js_array),
533 ancestors: context.ancestors,
534 replacer_fn: context.replacer_fn,
535 include_keys_replacer: context.include_keys_replacer,
536 itoa_buffer: context.itoa_buffer,
537 ryu_buffer: context.ryu_buffer,
538 },
539 add_comma,
540 )?;
541 value_written = value_written || add_comma;
542 }
543 if value_written {
544 write_indentation(context.result, indentation, depth);
545 }
546 context.result.push(']');
547 },
548 _ => {},
549 }
550 Ok(())
551}