1use crate::error::{Error, Result};
17use serde_json::Value;
18
19#[derive(Debug, Clone, Copy)]
21pub struct EncodeOptions {
22 pub pretty: bool,
23 pub escape_slashes: bool,
24 pub escape_unicode: bool,
25}
26
27pub const FLAGS_ZERO: EncodeOptions = EncodeOptions {
29 pretty: false,
30 escape_slashes: true,
31 escape_unicode: true,
32};
33
34pub const FLAGS_JSONFILE: EncodeOptions = EncodeOptions {
37 pretty: true,
38 escape_slashes: false,
39 escape_unicode: false,
40};
41
42pub fn php_json_encode(value: &Value) -> Result<String> {
43 php_json_encode_with(value, FLAGS_ZERO)
44}
45
46pub const STDCLASS_MARKER: &str = "\u{0}stdClass";
49
50pub fn empty_stdclass() -> Value {
52 let mut m = serde_json::Map::new();
53 m.insert(STDCLASS_MARKER.to_owned(), Value::Null);
54 Value::Object(m)
55}
56
57pub fn php_json_encode_with(value: &Value, opts: EncodeOptions) -> Result<String> {
58 let mut out = String::new();
59 encode_into(value, &mut out, opts, 0)?;
60 Ok(out)
61}
62
63fn newline_indent(out: &mut String, level: usize) {
64 out.push('\n');
65 for _ in 0..level {
66 out.push_str(" ");
67 }
68}
69
70fn encode_into(value: &Value, out: &mut String, opts: EncodeOptions, level: usize) -> Result<()> {
71 match value {
72 Value::Null => out.push_str("null"),
73 Value::Bool(true) => out.push_str("true"),
74 Value::Bool(false) => out.push_str("false"),
75 Value::Number(n) => encode_number(n, out)?,
76 Value::String(s) => encode_string_with(s, out, opts),
77 Value::Array(items) => encode_list(items.iter(), out, opts, level)?,
78 Value::Object(map) => {
79 if map.len() == 1 && map.contains_key(STDCLASS_MARKER) {
80 out.push_str("{}");
83 } else if is_php_list(map) {
84 encode_list(map.values(), out, opts, level)?;
85 } else {
86 out.push('{');
87 for (i, (key, item)) in map.iter().enumerate() {
88 if i > 0 {
89 out.push(',');
90 }
91 if opts.pretty {
92 newline_indent(out, level + 1);
93 }
94 encode_string_with(key, out, opts);
95 out.push(':');
96 if opts.pretty {
97 out.push(' ');
98 }
99 encode_into(item, out, opts, level + 1)?;
100 }
101 if opts.pretty && !map.is_empty() {
102 newline_indent(out, level);
103 }
104 out.push('}');
105 }
106 }
107 }
108 Ok(())
109}
110
111fn encode_list<'a>(
112 items: impl ExactSizeIterator<Item = &'a Value>,
113 out: &mut String,
114 opts: EncodeOptions,
115 level: usize,
116) -> Result<()> {
117 let len = items.len();
118 out.push('[');
119 for (i, item) in items.enumerate() {
120 if i > 0 {
121 out.push(',');
122 }
123 if opts.pretty {
124 newline_indent(out, level + 1);
125 }
126 encode_into(item, out, opts, level + 1)?;
127 }
128 if opts.pretty && len > 0 {
129 newline_indent(out, level);
130 }
131 out.push(']');
132 Ok(())
133}
134
135fn is_php_list(map: &serde_json::Map<String, Value>) -> bool {
138 map.keys()
139 .enumerate()
140 .all(|(i, k)| k.as_str() == i.to_string())
141}
142
143fn encode_number(n: &serde_json::Number, out: &mut String) -> Result<()> {
144 if let Some(i) = n.as_i64() {
145 out.push_str(&i.to_string());
146 } else if let Some(u) = n.as_u64() {
147 encode_double(u as f64, out)?;
149 } else if let Some(f) = n.as_f64() {
150 encode_double(f, out)?;
151 }
152 Ok(())
153}
154
155fn shortest_digits(a: f64) -> (String, String) {
167 let sci = format!("{:e}", a);
168 let (mantissa, exp) = sci.split_once('e').unwrap_or((sci.as_str(), "0"));
169 let digits: String = mantissa.chars().filter(|c| *c != '.').collect();
170 let exact = format!("{:.*e}", digits.len().saturating_sub(1), a);
171 if exact != sci && exact.parse::<f64>() == Ok(a) {
172 let (m, e) = exact.split_once('e').unwrap_or((exact.as_str(), "0"));
173 let mut d: String = m.chars().filter(|c| *c != '.').collect();
174 while d.len() > 1 && d.ends_with('0') {
175 d.pop();
176 }
177 return (d, e.to_owned());
178 }
179 (digits, exp.to_owned())
180}
181
182pub fn php_double(f: f64) -> Result<String> {
186 let mut out = String::new();
187 encode_double(f, &mut out)?;
188 Ok(out)
189}
190
191fn encode_double(f: f64, out: &mut String) -> Result<()> {
192 if !f.is_finite() {
193 return Err(Error::NonFiniteFloat(f));
194 }
195 if f == 0.0 {
196 out.push_str(if f.is_sign_negative() { "-0" } else { "0" });
197 return Ok(());
198 }
199 if f.is_sign_negative() {
200 out.push('-');
201 }
202 let (digits, exp) = shortest_digits(f.abs());
203 let exp: i32 = exp.parse().map_err(|_| Error::NonFiniteFloat(f))?;
204 let dec_point = exp + 1; if dec_point > -4 && dec_point <= 17 {
207 let n = digits.len() as i32;
208 if dec_point <= 0 {
209 out.push_str("0.");
210 for _ in 0..-dec_point {
211 out.push('0');
212 }
213 out.push_str(&digits);
214 } else if dec_point >= n {
215 out.push_str(&digits);
216 for _ in 0..(dec_point - n) {
217 out.push('0');
218 }
219 } else {
220 out.push_str(&digits[..dec_point as usize]);
221 out.push('.');
222 out.push_str(&digits[dec_point as usize..]);
223 }
224 } else {
225 out.push_str(&digits[..1]);
226 out.push('.');
227 if digits.len() > 1 {
228 out.push_str(&digits[1..]);
229 } else {
230 out.push('0');
231 }
232 let e = dec_point - 1;
233 if e >= 0 {
234 out.push_str("e+");
235 } else {
236 out.push_str("e-");
237 }
238 out.push_str(&e.abs().to_string());
239 }
240 Ok(())
241}
242
243fn encode_string_with(s: &str, out: &mut String, opts: EncodeOptions) {
244 out.push('"');
245 for c in s.chars() {
246 match c {
247 '"' => out.push_str("\\\""),
248 '\\' => out.push_str("\\\\"),
249 '/' if opts.escape_slashes => out.push_str("\\/"),
250 '\u{08}' => out.push_str("\\b"),
251 '\u{0c}' => out.push_str("\\f"),
252 '\n' => out.push_str("\\n"),
253 '\r' => out.push_str("\\r"),
254 '\t' => out.push_str("\\t"),
255 c if (c as u32) < 0x20 => {
256 push_unicode_escape(c as u32, out);
257 }
258 c if c.is_ascii() => out.push(c),
259 '\u{2028}' => out.push_str("\\u2028"),
262 '\u{2029}' => out.push_str("\\u2029"),
263 c if !opts.escape_unicode => out.push(c),
264 c => {
265 let cp = c as u32;
266 if cp > 0xFFFF {
267 let v = cp - 0x10000;
269 push_unicode_escape(0xD800 + (v >> 10), out);
270 push_unicode_escape(0xDC00 + (v & 0x3FF), out);
271 } else {
272 push_unicode_escape(cp, out);
273 }
274 }
275 }
276 }
277 out.push('"');
278}
279
280fn push_unicode_escape(cp: u32, out: &mut String) {
281 use std::fmt::Write as _;
282 let _ = write!(out, "\\u{cp:04x}");
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289 use serde_json::json;
290
291 fn enc(v: Value) -> String {
292 php_json_encode(&v).expect("encodable")
293 }
294
295 #[test]
296 fn compact_object_preserves_order() {
297 let v: Value = serde_json::from_str(r#"{"b":1,"a":{"z":true,"y":null}}"#).unwrap();
298 assert_eq!(enc(v), r#"{"b":1,"a":{"z":true,"y":null}}"#);
299 }
300
301 #[test]
302 fn slash_and_unicode_are_escaped() {
303 assert_eq!(enc(json!("a/b")), r#""a\/b""#);
304 assert_eq!(enc(json!("héhé")), "\"h\\u00e9h\\u00e9\"");
305 assert_eq!(enc(json!("🎼")), "\"\\ud83c\\udfbc\"");
306 assert_eq!(enc(json!("tab\tok")), "\"tab\\tok\"");
307 assert_eq!(enc(json!("\u{1}")), "\"\\u0001\"");
308 }
309
310 #[test]
311 fn empty_object_becomes_array() {
312 let v: Value = serde_json::from_str(r#"{"extra":{}}"#).unwrap();
313 assert_eq!(enc(v), r#"{"extra":[]}"#);
314 }
315
316 #[test]
317 fn sequential_numeric_keys_become_array() {
318 let v: Value = serde_json::from_str(r#"{"0":"a","1":"b"}"#).unwrap();
319 assert_eq!(enc(v), r#"["a","b"]"#);
320 let v: Value = serde_json::from_str(r#"{"1":"a","0":"b"}"#).unwrap();
322 assert_eq!(enc(v), r#"{"1":"a","0":"b"}"#);
323 let v: Value = serde_json::from_str(r#"{"0":"a","2":"b"}"#).unwrap();
325 assert_eq!(enc(v), r#"{"0":"a","2":"b"}"#);
326 }
327
328 #[test]
329 fn numbers_round_trip() {
330 assert_eq!(enc(json!(42)), "42");
331 assert_eq!(enc(json!(-7)), "-7");
332 assert_eq!(enc(json!(1.5)), "1.5");
333 assert_eq!(enc(json!(1.0)), "1");
335 assert_eq!(enc(json!(1.0e-7)), "1.0e-7");
336 assert_eq!(enc(json!(0.0001)), "0.0001");
337 assert_eq!(enc(json!(1.0e-5)), "1.0e-5");
338 assert_eq!(enc(json!(9.9e16)), "99000000000000000");
339 assert_eq!(enc(json!(1.0e17)), "1.0e+17");
340 assert_eq!(enc(json!(1.23e17)), "1.23e+17");
341 assert_eq!(enc(json!(-0.0)), "-0");
342 assert_eq!(enc(json!(5e-324)), "5.0e-324");
343 assert_eq!(
344 enc(json!(1.7976931348623157e308)),
345 "1.7976931348623157e+308"
346 );
347 assert_eq!(
348 enc(json!(12345678901234567890_u64)),
349 "1.2345678901234567e+19"
350 );
351 }
352}