Skip to main content

ferrijs_std/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, 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    //only preserve indentation if we're returning nested data
135    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 == &current_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            //only start detect circular reference at this level
441            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            // Collect keys: js_object.keys() uses JS_GetOwnPropertyNames which can fail for
458            // Proxy objects. Fall back to Object.keys() in that case.
459            let keys: Vec<String> = {
460                let collected: Vec<String> = js_object.keys::<String>().flatten().collect();
461                if collected.is_empty() {
462                    // Clear any pending exception and try Object.keys() for Proxy support
463                    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            //only start detect circular reference at this level
510            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}