Skip to main content

ferrijs_std/json/
mod.rs

1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3use std::cmp::min;
4
5use rquickjs::{
6    atom::PredefinedAtom, function::Opt, prelude::Func, Ctx, IntoJs, Object, Result, Value,
7};
8
9pub mod escape;
10pub mod parse;
11pub mod stringify;
12
13use crate::json::parse::json_parse_string;
14use crate::json::stringify::json_stringify_replacer_space;
15
16/// Replace `JSON.parse` / `JSON.stringify` with this module's.
17///
18/// NOT called by [`crate::init`], and not ready to be. It is about a
19/// third faster than the engine's both ways, but measured against Node
20/// it still differs in ways that matter:
21///
22/// - `parse` assigns keys through `[[Set]]`, so `{"__proto__": {...}}`
23///   retargets the result's prototype rather than becoming an own
24///   property. For a runtime that parses untrusted JSON that is a
25///   prototype-pollution primitive, and it is the reason this is off.
26/// - `parse` ignores its `reviver` argument entirely.
27/// - `stringify` writes a hole or an `undefined` array element as
28///   nothing rather than `null`, which emits invalid JSON (`[1,,2]`).
29/// - `stringify` does not unwrap boxed primitives: `new Number(5)`
30///   serialises as `{}` where the spec says `5`.
31/// - `stringify` writes `-0` for negative zero (spec: `0`) and `1e21`
32///   where the spec's ToString gives `1e+21`.
33///
34/// `crates/ferrijs/tests/json_conformance.rs` is the set these came
35/// from; make it pass before turning this on.
36pub fn redefine_static_methods(ctx: &Ctx<'_>) -> Result<()> {
37    let globals = ctx.globals();
38    let json_module: Object = globals.get(PredefinedAtom::JSON)?;
39    json_module.set("parse", Func::from(json_parse_string))?;
40    json_module.set(
41        "stringify",
42        Func::from(|ctx, value, replacer, space| {
43            struct StringifyArgs<'js>(Ctx<'js>, Value<'js>, Opt<Value<'js>>, Opt<Value<'js>>);
44            let StringifyArgs(ctx, value, replacer, space) =
45                StringifyArgs(ctx, value, replacer, space);
46
47            let mut space_value = None;
48            let mut replacer_value = None;
49
50            if let Some(replacer) = replacer.0 {
51                if let Some(space) = space.0 {
52                    if let Some(space) = space.as_string() {
53                        let mut space = space.clone().to_string()?;
54                        space.truncate(20);
55                        space_value = Some(space);
56                    }
57                    if let Some(number) = space.as_int() {
58                        if number > 0 {
59                            space_value = Some(" ".repeat(min(10, number as usize)));
60                        }
61                    }
62                }
63                replacer_value = Some(replacer);
64            }
65
66            json_stringify_replacer_space(&ctx, value, replacer_value, space_value)
67                .map(|v| v.into_js(&ctx))?
68        }),
69    )?;
70    Ok(())
71}
72
73#[cfg(test)]
74mod tests {
75    use crate::test::test_sync_with;
76    use rquickjs::{prelude::Func, Array, CatchResultExt, IntoJs, Null, Object, Undefined, Value};
77
78    use crate::json::{
79        parse::{json_parse, json_parse_string},
80        stringify::{json_stringify, json_stringify_replacer_space},
81    };
82
83    static JSON: &str = r#"{"organization":{"name":"TechCorp","founding_year":2000,"departments":[{"name":"Engineering","head":{"name":"Alice Smith","title":"VP of Engineering","contact":{"email":"alice.smith@techcorp.com","phone":"+1 (555) 123-4567"}},"employees":[{"id":101,"name":"Bob Johnson","position":"Software Engineer","contact":{"email":"bob.johnson@techcorp.com","phone":"+1 (555) 234-5678"},"projects":[{"project_id":"P001","name":"Project A","status":"In Progress","description":"Developing a revolutionary software solution for clients.","start_date":"2023-01-15","end_date":null,"team":[{"id":201,"name":"Sara Davis","role":"UI/UX Designer"},{"id":202,"name":"Charlie Brown","role":"Quality Assurance Engineer"}]},{"project_id":"P002","name":"Project B","status":"Completed","description":"Upgrading existing systems to enhance performance.","start_date":"2022-05-01","end_date":"2022-11-30","team":[{"id":203,"name":"Emily White","role":"Systems Architect"},{"id":204,"name":"James Green","role":"Database Administrator"}]}]},{"id":102,"name":"Carol Williams","position":"Senior Software Engineer","contact":{"email":"carol.williams@techcorp.com","phone":"+1 (555) 345-6789"},"projects":[{"project_id":"P001","name":"Project A","status":"In Progress","description":"Working on the backend development of Project A.","start_date":"2023-01-15","end_date":null,"team":[{"id":205,"name":"Alex Turner","role":"DevOps Engineer"},{"id":206,"name":"Mia Garcia","role":"Software Developer"}]},{"project_id":"P003","name":"Project C","status":"Planning","description":"Researching and planning for a future project.","start_date":null,"end_date":null,"team":[]}]}]},{"name":"Marketing","head":{"name":"David Brown","title":"VP of Marketing","contact":{"email":"david.brown@techcorp.com","phone":"+1 (555) 456-7890"}},"employees":[{"id":201,"name":"Eva Miller","position":"Marketing Specialist","contact":{"email":"eva.miller@techcorp.com","phone":"+1 (555) 567-8901"},"campaigns":[{"campaign_id":"C001","name":"Product Launch","status":"Upcoming","description":"Planning for the launch of a new product line.","start_date":"2023-03-01","end_date":null,"team":[{"id":301,"name":"Oliver Martinez","role":"Graphic Designer"},{"id":302,"name":"Sophie Johnson","role":"Content Writer"}]},{"campaign_id":"C002","name":"Brand Awareness","status":"Ongoing","description":"Executing strategies to increase brand visibility.","start_date":"2022-11-15","end_date":"2023-01-31","team":[{"id":303,"name":"Liam Taylor","role":"Social Media Manager"},{"id":304,"name":"Ava Clark","role":"Marketing Analyst"}]}]}]}]}}"#;
84
85    #[tokio::test]
86    async fn json_parser() {
87        test_sync_with(|ctx| {
88            let json_data = [
89                r#"{"aa\"\"aaaaaaaaaaaaaaaa":"a","b":"bbb"}"#,
90                r#"{"a":"aaaaaaaaaaaaaaaaaa","b":"bbb"}"#,
91                r#"{"a":["a","a","aaaa","a"],"b":"b"}"#,
92                r#"{"type":"Buffer","data":[10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10]}"#,
93                r#"{"a":[{"object2":{"key1":"value1","key2":123,"key3":false,"nestedObject":{"nestedKey":"nestedValue"}},"string":"Hello, World!","emptyObj":{},"emptyArr":[],"number":42,"boolean":true,"nullValue":null,"array":[1,2,3,"four",5.5,true,null],"object":{"key1":"value1","key2":123,"key3":false,"nestedObject":{"nestedKey":"nestedValue"}}}]}"#,
94                JSON,
95            ];
96
97            for json_str in json_data {
98                let json = json_str.to_string();
99                let json2 = json.clone();
100
101                let value = json_parse(&ctx, json2)?;
102                let new_json = json_stringify_replacer_space(&ctx, value.clone(),None,Some("  ".into()))?.unwrap();
103                let builtin_json = ctx.json_stringify_replacer_space(value,Null,"  ".to_string())?.unwrap().to_string()?;
104                assert_eq!(new_json, builtin_json);
105            }
106
107            Ok(())
108        })
109        .await;
110    }
111
112    #[tokio::test]
113    async fn json_parse_non_string() {
114        test_sync_with(|ctx| {
115            ctx.globals().set("parse", Func::from(json_parse_string))?;
116
117            let result = ctx.eval::<(), _>("parse({})").catch(&ctx);
118
119            if let Err(err) = result {
120                assert_eq!(
121                   err.to_string(),
122                   "Error: \"[object Object]\" not valid JSON at index 1 ('o')\n    at <eval> (eval_script:1:1)\n"
123               );
124            } else {
125                panic!("expected error")
126            }
127
128            Ok(())
129        })
130        .await;
131    }
132
133    #[tokio::test]
134    async fn json_stringify_undefined() {
135        test_sync_with(|ctx| {
136            let stringified = json_stringify(&ctx, Undefined.into_js(&ctx)?)?;
137            let stringified_2 = ctx
138                .json_stringify(Undefined)?
139                .map(|v| v.to_string().unwrap());
140            assert_eq!(stringified, stringified_2);
141
142            let obj: Value = ctx.eval(
143                r#"let obj = { value: undefined, array: [undefined, null, 1, true, "hello", { [Symbol("sym")]: 1, [undefined]: 2}] };obj;"#,
144            )?;
145
146            let stringified = json_stringify(&ctx, obj.clone())?;
147            let stringified_2 = ctx
148                .json_stringify(obj)?
149                .map(|v| v.to_string().unwrap());
150            assert_eq!(stringified, stringified_2);
151
152            Ok(())
153        })
154        .await;
155    }
156
157    #[tokio::test]
158    async fn json_stringify_objects() {
159        test_sync_with(|ctx| {
160            let date: Value = ctx.eval("let obj = { date: new Date(0) };obj;")?;
161            let stringified = json_stringify(&ctx, date.clone())?.unwrap();
162            let stringified_2 = ctx.json_stringify(date)?.unwrap().to_string()?;
163            assert_eq!(stringified, stringified_2);
164            Ok(())
165        })
166        .await;
167    }
168
169    #[tokio::test]
170    async fn huge_numbers() {
171        test_sync_with(|ctx| {
172
173            let big_int_value = json_parse(&ctx, b"99999999999999999999999999999999999999999999999999999999999999999999999999999999999")?;
174
175            let stringified = json_stringify(&ctx, big_int_value.clone())?.unwrap();
176            let stringified_2 = ctx.json_stringify(big_int_value)?.unwrap().to_string()?.replace("e+", "e");
177            assert_eq!(stringified, stringified_2);
178
179            let big_int_value: Value = ctx.eval("999999999999")?;
180            let stringified = json_stringify(&ctx, big_int_value.clone())?.unwrap();
181            let stringified_2 = ctx.json_stringify(big_int_value)?.unwrap().to_string()?;
182            assert_eq!(stringified, stringified_2);
183
184            Ok(())
185        })
186        .await;
187    }
188
189    #[tokio::test]
190    async fn json_circular_ref() {
191        test_sync_with(|ctx| {
192            let obj1 = Object::new(ctx.clone())?;
193            let obj2 = Object::new(ctx.clone())?;
194            let obj3 = Object::new(ctx.clone())?;
195            let obj4 = Object::new(ctx.clone())?;
196            obj4.set("key", "value")?;
197            obj3.set("sub2", obj4.clone())?;
198            obj2.set("sub1", obj3)?;
199            obj1.set("root1", obj2.clone())?;
200            obj1.set("root2", obj2.clone())?;
201            obj1.set("root3", obj2.clone())?;
202
203            let value = obj1.clone().into_value();
204
205            let stringified = json_stringify(&ctx, value.clone())?.unwrap();
206            let stringified_2 = ctx.json_stringify(value.clone())?.unwrap().to_string()?;
207            assert_eq!(stringified, stringified_2);
208
209            obj4.set("recursive", obj1.clone())?;
210
211            let stringified = json_stringify(&ctx, value.clone());
212
213            if let Err(error_message) = stringified.catch(&ctx) {
214                let error_str = error_message.to_string();
215                assert_eq!(
216                    "Error: Circular reference detected at: \"...root1.sub1.sub2.recursive\"\n",
217                    error_str
218                )
219            } else {
220                panic!("Expected an error, but got Ok");
221            }
222
223            let array1 = Array::new(ctx.clone())?;
224            let array2 = Array::new(ctx.clone())?;
225            let array3 = Array::new(ctx.clone())?;
226
227            let obj5 = Object::new(ctx.clone())?;
228            obj5.set("key", obj1.clone())?;
229            array3.set(2, obj5)?;
230            array2.set(1, array3)?;
231            array1.set(0, array2)?;
232
233            obj4.remove("recursive")?;
234            obj1.set("recursiveArray", array1)?;
235
236            let stringified = json_stringify(&ctx, value.clone());
237
238            if let Err(error_message) = stringified.catch(&ctx) {
239                let error_str = error_message.to_string();
240                assert_eq!(
241                    "Error: Circular reference detected at: \"...recursiveArray[0][1][2].key\"\n",
242                    error_str
243                )
244            } else {
245                panic!("Expected an error, but got Ok");
246            }
247
248            Ok(())
249        })
250        .await;
251    }
252}