rqjs_ext/json/
stringify.rs

1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3use 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 == &current_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        "",    // add_comma = false, has_indentation = false
367        ",",   // add_comma = false, has_indentation = true
368        "\n",  // add_comma = true, has_indentation = false
369        ",\n", // add_comma = true, has_indentation = true
370    ];
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            //only start detect circular reference at this level
407            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            //only start detect circular reference at this level
456            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}