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 to_json: &Function<'js>,
131) -> Result<()> {
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 include_keys_replacer = context.include_keys_replacer;
215 let indentation = context.indentation;
216 let depth = context.depth;
217
218 let value = new_value.as_ref().unwrap_or(context.value);
219
220 let type_of = value.type_of();
221
222 if context.index.is_none()
223 && matches!(
224 type_of,
225 Type::Symbol | Type::Undefined | Type::Function | Type::Constructor
226 )
227 {
228 return Ok(PrimitiveStatus::Ignored);
229 }
230
231 if matches!(type_of, Type::BigInt) {
232 return Err(Exception::throw_type(
233 context.ctx,
234 "Do not know how to serialize a BigInt",
235 ));
236 }
237
238 if let (Some(include_keys_replacer), Some(key)) = (include_keys_replacer, key) {
244 if !include_keys_replacer.contains(key) {
245 return Ok(PrimitiveStatus::Ignored);
246 }
247 }
248
249 if let Some(indentation) = indentation {
250 write_indented_separator(context.result, key, add_comma, indentation, depth);
251 } else {
252 write_sep(context.result, add_comma, false);
253 if let Some(key) = key {
254 write_key(context.result, key, false);
255 }
256 }
257
258 match type_of {
259 Type::Null | Type::Undefined => context.result.push_str("null"),
260 Type::Bool => {
261 let bool_str = if unsafe { value.as_bool().unwrap_unchecked() } {
262 "true"
263 } else {
264 "false"
265 };
266 context.result.push_str(bool_str);
267 },
268 Type::Int => context.result.push_str(
269 context
270 .itoa_buffer
271 .format(unsafe { value.as_int().unwrap_unchecked() }),
272 ),
273 Type::Float => {
274 let float_value = unsafe { value.as_float().unwrap_unchecked() };
275 const EXP_MASK: u64 = 0x7ff0000000000000;
276 let bits = float_value.to_bits();
277 if bits & EXP_MASK == EXP_MASK {
278 context.result.push_str("null");
279 } else {
280 let str = context.ryu_buffer.format_finite(float_value);
281
282 let bytes = str.as_bytes();
283 let len = bytes.len();
284
285 context.result.push_str(str);
286
287 if &bytes[len - 2..] == b".0" {
288 let len = context.result.len();
289 unsafe { context.result.as_mut_vec().set_len(len - 2) }
290 }
291 }
292 },
293 Type::String => {
294 let js_string = unsafe { value.as_string().unwrap_unchecked() }.clone();
295 write_string(context.result, js_string.to_cstring()?.as_str());
296 },
297 _ => return Ok(PrimitiveStatus::Iterate(new_value)),
298 }
299 Ok(PrimitiveStatus::Written)
300}
301
302#[inline(always)]
303#[cold]
304fn write_indented_separator(
305 result: &mut String,
306 key: Option<&str>,
307 add_comma: bool,
308 indentation: &str,
309 depth: usize,
310) {
311 write_sep(result, add_comma, true);
312 result.push_str(&indentation.repeat(depth));
313 if let Some(key) = key {
314 write_key(result, key, true);
315 }
316}
317
318#[cold]
319fn detect_circular_reference(
320 ctx: &Ctx<'_>,
321 value: &Object<'_>,
322 key: Option<&str>,
323 index: Option<usize>,
324 parent: Option<&Object<'_>>,
325 ancestors: &mut Vec<(usize, Rc<str>)>,
326 itoa_buffer: &mut itoa::Buffer,
327) -> Result<()> {
328 let parent_ptr = unsafe { qjs::JS_VALUE_GET_PTR(parent.unwrap_unchecked().as_raw()) as usize };
329 let current_ptr = unsafe { qjs::JS_VALUE_GET_PTR(value.as_raw()) as usize };
330
331 while !ancestors.is_empty()
332 && match ancestors.last() {
333 Some((ptr, _)) => ptr != &parent_ptr,
334 _ => false,
335 }
336 {
337 ancestors.pop();
338 }
339
340 if ancestors.iter().any(|(ptr, _)| ptr == ¤t_ptr) {
341 let mut iter = ancestors.iter_mut();
342
343 let first = &unsafe { iter.next().unwrap_unchecked() }.1;
344
345 let mut message = iter.rev().take(4).rev().fold(
346 String::from("Circular reference detected at: \".."),
347 |mut acc, (_, key)| {
348 if !key.starts_with('[') {
349 acc.push('.');
350 }
351 acc.push_str(key);
352 acc
353 },
354 );
355
356 if !first.starts_with('[') {
357 message.push('.');
358 }
359
360 message.push_str(first);
361 message.push('"');
362
363 return Err(Exception::throw_type(ctx, &message));
364 }
365 ancestors.push((
366 current_ptr,
367 key.map(|k| k.into()).unwrap_or_else(|| {
368 ["[", itoa_buffer.format(index.unwrap_or_default()), "]"]
369 .concat()
370 .into()
371 }),
372 ));
373
374 Ok(())
375}
376
377#[inline(always)]
378fn append_value(context: &mut StringifyContext<'_, '_>, add_comma: bool) -> Result<bool> {
379 match write_primitive(context, add_comma)? {
380 PrimitiveStatus::Written => Ok(true),
381 PrimitiveStatus::Ignored => Ok(false),
382 PrimitiveStatus::Iterate(new_value) => {
383 context.depth += 1;
384 iterate(context, new_value)?;
385 Ok(true)
386 },
387 }
388}
389
390#[inline(always)]
391fn write_key(string: &mut String, key: &str, indent: bool) {
392 string.push('"');
393 escape_json_string(string, key.as_bytes());
394 string.push_str("\":");
395 if indent {
396 string.push(' ');
397 }
398}
399
400#[inline(always)]
401fn write_sep(result: &mut String, add_comma: bool, has_indentation: bool) {
402 if add_comma {
403 result.push(',');
404 }
405 if has_indentation {
406 result.push('\n');
407 }
408}
409
410#[inline(always)]
411fn write_string(string: &mut String, value: &str) {
412 string.push('"');
413 escape_json_string(string, value.as_bytes());
414 string.push('"');
415}
416
417#[inline(always)]
418fn get_key_or_index<'a>(
419 itoa_buffer: &'a mut itoa::Buffer,
420 key: Option<&'a str>,
421 index: Option<usize>,
422) -> &'a str {
423 key.unwrap_or_else(|| itoa_buffer.format(index.unwrap_or_default()))
424}
425
426fn iterate<'js>(
427 context: &mut StringifyContext<'_, 'js>,
428 new_value: Option<Value<'js>>,
429) -> Result<()> {
430 let mut add_comma;
431 let mut value_written;
432 let elem = new_value.as_ref().unwrap_or(context.value);
433 let depth = context.depth;
434 let ctx = context.ctx;
435 let indentation = context.indentation;
436 match elem.type_of() {
437 Type::Object | Type::Exception | Type::Proxy => {
438 let js_object = unsafe { elem.as_object().unwrap_unchecked() };
439 let to_json: Value<'js> = js_object.get(PredefinedAtom::ToJSON)?;
447 if let Some(to_json) = to_json.as_function() {
448 return run_to_json(context, js_object, to_json);
449 }
450
451 if depth > CIRCULAR_REF_DETECTION_DEPTH {
453 detect_circular_reference(
454 ctx,
455 js_object,
456 context.key,
457 context.index,
458 context.parent,
459 context.ancestors,
460 context.itoa_buffer,
461 )?;
462 }
463
464 context.result.push('{');
465
466 value_written = false;
467
468 let keys: Vec<String> = {
471 let collected: Vec<String> = js_object.keys::<String>().flatten().collect();
472 if collected.is_empty() {
473 ctx.catch();
475 ctx.globals()
476 .get::<_, Object>("Object")
477 .ok()
478 .and_then(|o| o.get::<_, Function>("keys").ok())
479 .and_then(|f| f.call::<_, Vec<String>>((js_object.clone(),)).ok())
480 .unwrap_or_default()
481 } else {
482 collected
483 }
484 };
485
486 for key in keys {
487 let val = js_object.get(&key)?;
488
489 add_comma = append_value(
490 &mut StringifyContext {
491 ctx,
492 result: context.result,
493 value: &val,
494 depth,
495 key: Some(&key),
496 indentation,
497 index: None,
498 parent: Some(js_object),
499 ancestors: context.ancestors,
500 replacer_fn: context.replacer_fn,
501 include_keys_replacer: context.include_keys_replacer,
502 itoa_buffer: context.itoa_buffer,
503 ryu_buffer: context.ryu_buffer,
504 },
505 value_written,
506 )?;
507 value_written = value_written || add_comma;
508 }
509
510 if value_written {
511 write_indentation(context.result, indentation, depth);
512 }
513 context.result.push('}');
514 },
515 Type::Array => {
516 context.result.push('[');
517 add_comma = false;
518 value_written = false;
519 let js_array = unsafe { elem.as_array().unwrap_unchecked() };
520 if depth > CIRCULAR_REF_DETECTION_DEPTH {
522 detect_circular_reference(
523 ctx,
524 js_array.as_object(),
525 context.key,
526 context.index,
527 context.parent,
528 context.ancestors,
529 context.itoa_buffer,
530 )?;
531 }
532 for (i, val) in js_array.iter::<Value>().enumerate() {
533 let val = val?;
534 add_comma = append_value(
535 &mut StringifyContext {
536 ctx,
537 result: context.result,
538 value: &val,
539 depth,
540 key: None,
541 indentation,
542 index: Some(i),
543 parent: Some(js_array),
544 ancestors: context.ancestors,
545 replacer_fn: context.replacer_fn,
546 include_keys_replacer: context.include_keys_replacer,
547 itoa_buffer: context.itoa_buffer,
548 ryu_buffer: context.ryu_buffer,
549 },
550 add_comma,
551 )?;
552 value_written = value_written || add_comma;
553 }
554 if value_written {
555 write_indentation(context.result, indentation, depth);
556 }
557 context.result.push(']');
558 },
559 _ => {},
560 }
561 Ok(())
562}