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