Skip to main content

secs_project/
pyjson.rs

1//! Python-compatible JSON values, parser, and pretty-printer.
2
3use std::collections::HashMap;
4use std::collections::hash_map::Entry;
5use std::fmt;
6use std::mem::size_of;
7
8const DEFAULT_BUDGET_EXTRA: usize = 16 * 1024 * 1024;
9
10/// A JSON value exactly as Python's `json` module reads and writes it.
11#[derive(Clone, Debug, PartialEq)]
12pub enum PyJson {
13    Null,
14    Bool(bool),
15    /// Any integer, as canonical decimal text.
16    Int(String),
17    /// Any `f64`, including NaN, infinities, and negative zero.
18    Float(f64),
19    Str(String),
20    Array(Vec<PyJson>),
21    /// Object members in insertion order.
22    Object(Vec<(String, PyJson)>),
23}
24
25impl PyJson {
26    pub(crate) fn object(&self) -> Option<&[(String, PyJson)]> {
27        match self {
28            Self::Object(value) => Some(value),
29            _ => None,
30        }
31    }
32
33    pub(crate) fn array(&self) -> Option<&[PyJson]> {
34        match self {
35            Self::Array(value) => Some(value),
36            _ => None,
37        }
38    }
39
40    pub(crate) fn str(&self) -> Option<&str> {
41        match self {
42            Self::Str(value) => Some(value),
43            _ => None,
44        }
45    }
46
47    pub(crate) fn bool(&self) -> Option<bool> {
48        match self {
49            Self::Bool(value) => Some(*value),
50            _ => None,
51        }
52    }
53
54    pub(crate) fn get(&self, key: &str) -> Option<&PyJson> {
55        self.object()?
56            .iter()
57            .find(|(name, _)| name == key)
58            .map(|(_, value)| value)
59    }
60
61    pub(crate) fn object_mut(&mut self) -> Option<&mut Vec<(String, PyJson)>> {
62        match self {
63            Self::Object(value) => Some(value),
64            _ => None,
65        }
66    }
67
68    /// Python's `bool(value)`: `None`, `False`, zero and empty containers are false.
69    pub(crate) fn is_truthy(&self) -> bool {
70        match self {
71            Self::Null => false,
72            Self::Bool(value) => *value,
73            Self::Int(value) => value != "0",
74            Self::Float(value) => *value != 0.0,
75            Self::Str(value) => !value.is_empty(),
76            Self::Array(value) => !value.is_empty(),
77            Self::Object(value) => !value.is_empty(),
78        }
79    }
80}
81
82/// How a value the writer rebuilds from a typed field meets the value the
83/// file held, so saving keeps keys this version does not know (ADR 0004,
84/// docs/DEVIATIONS.md F16).
85#[derive(Debug)]
86pub(crate) enum Shape {
87    /// An object with fixed field names: the writer's fields in its order,
88    /// then the file's other fields in theirs. `optional` names fields the
89    /// writer leaves out on purpose, which therefore do not come back from the
90    /// file. `nested` gives the shape of fields merged in turn; the writer's
91    /// value replaces every other field.
92    Record {
93        optional: &'static [&'static str],
94        nested: &'static [(&'static str, Shape)],
95    },
96    /// An object whose keys are data (roles, bundles): only the writer's
97    /// entries, each merged with the file's entry of the same key.
98    Map(&'static Shape),
99    /// An array merged element by element when both sides have the same
100    /// length; otherwise the writer's array.
101    List(&'static Shape),
102}
103
104/// Merges `new`, rebuilt by the writer, with `old`, read from the file.
105pub(crate) fn merge_with_file(new: PyJson, old: &PyJson, shape: &Shape) -> PyJson {
106    match (shape, new, old) {
107        (
108            Shape::Record { optional, nested },
109            PyJson::Object(fields),
110            PyJson::Object(old_fields),
111        ) => {
112            let mut out = fields
113                .into_iter()
114                .map(|(key, value)| {
115                    let value = match (
116                        nested.iter().find(|(name, _)| *name == key),
117                        old_fields.iter().find(|(name, _)| *name == key),
118                    ) {
119                        (Some((_, shape)), Some((_, old))) => merge_with_file(value, old, shape),
120                        _ => value,
121                    };
122                    (key, value)
123                })
124                .collect::<Vec<_>>();
125            let unknown = old_fields
126                .iter()
127                .filter(|(key, _)| {
128                    !optional.contains(&key.as_str()) && !out.iter().any(|(name, _)| name == key)
129                })
130                .cloned()
131                .collect::<Vec<_>>();
132            out.extend(unknown);
133            PyJson::Object(out)
134        }
135        (Shape::Map(entry), PyJson::Object(fields), PyJson::Object(old_fields)) => PyJson::Object(
136            fields
137                .into_iter()
138                .map(|(key, value)| {
139                    let value = match old_fields.iter().find(|(name, _)| *name == key) {
140                        Some((_, old)) => merge_with_file(value, old, entry),
141                        None => value,
142                    };
143                    (key, value)
144                })
145                .collect(),
146        ),
147        (Shape::List(item), PyJson::Array(items), PyJson::Array(old_items))
148            if items.len() == old_items.len() =>
149        {
150            PyJson::Array(
151                items
152                    .into_iter()
153                    .zip(old_items)
154                    .map(|(value, old)| merge_with_file(value, old, item))
155                    .collect(),
156            )
157        }
158        (_, new, _) => new,
159    }
160}
161
162#[derive(Clone, Debug, PartialEq, Eq)]
163pub struct PyJsonError {
164    offset: usize,
165    message: String,
166}
167
168impl PyJsonError {
169    fn new(offset: usize, message: impl Into<String>) -> Self {
170        Self {
171            offset,
172            message: message.into(),
173        }
174    }
175}
176
177impl fmt::Display for PyJsonError {
178    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179        write!(f, "{} at byte {}", self.message, self.offset)
180    }
181}
182
183impl std::error::Error for PyJsonError {}
184
185/// Parses the same value language as CPython's `json.loads`.
186pub fn loads(text: &str) -> Result<PyJson, PyJsonError> {
187    let budget = text
188        .len()
189        .saturating_mul(4)
190        .saturating_add(DEFAULT_BUDGET_EXTRA);
191    loads_with_budget(text, budget)
192}
193
194/// Parses with an explicit allocation budget in bytes.
195pub fn loads_with_budget(text: &str, budget: usize) -> Result<PyJson, PyJsonError> {
196    let mut parser = Parser {
197        text,
198        bytes: text.as_bytes(),
199        pos: 0,
200        remaining: budget,
201    };
202    parser.whitespace();
203    let value = parser.value(0)?;
204    parser.whitespace();
205    if parser.pos != parser.bytes.len() {
206        return Err(parser.error("extra data"));
207    }
208    Ok(value)
209}
210
211struct Parser<'a> {
212    text: &'a str,
213    bytes: &'a [u8],
214    pos: usize,
215    remaining: usize,
216}
217
218impl Parser<'_> {
219    fn error(&self, message: impl Into<String>) -> PyJsonError {
220        PyJsonError::new(self.pos, message)
221    }
222
223    fn charge(&mut self, bytes: usize) -> Result<(), PyJsonError> {
224        self.remaining = self
225            .remaining
226            .checked_sub(bytes)
227            .ok_or_else(|| self.error("JSON allocation budget exceeded"))?;
228        Ok(())
229    }
230
231    fn node(&mut self, value: PyJson, string_bytes: usize) -> Result<PyJson, PyJsonError> {
232        self.charge(size_of::<PyJson>().saturating_add(string_bytes))?;
233        Ok(value)
234    }
235
236    fn reserve_for_push<T>(&mut self, values: &mut Vec<T>) -> Result<(), PyJsonError> {
237        if values.len() < values.capacity() {
238            return Ok(());
239        }
240        let old_capacity = values.capacity();
241        let new_capacity = old_capacity.saturating_mul(2).max(4);
242        let additional = new_capacity.saturating_sub(old_capacity);
243        self.charge(additional.saturating_mul(size_of::<T>()))?;
244        values
245            .try_reserve_exact(additional)
246            .map_err(|_| self.error("JSON allocation failed"))?;
247        let extra = values.capacity().saturating_sub(new_capacity);
248        self.charge(extra.saturating_mul(size_of::<T>()))
249    }
250
251    fn whitespace(&mut self) {
252        while matches!(self.bytes.get(self.pos), Some(b' ' | b'\n' | b'\r' | b'\t')) {
253            self.pos += 1;
254        }
255    }
256
257    fn value(&mut self, depth: usize) -> Result<PyJson, PyJsonError> {
258        match self.bytes.get(self.pos).copied() {
259            Some(b'n') => self.keyword(b"null", PyJson::Null),
260            Some(b't') => self.keyword(b"true", PyJson::Bool(true)),
261            Some(b'f') => self.keyword(b"false", PyJson::Bool(false)),
262            Some(b'N') => self.keyword(b"NaN", PyJson::Float(f64::NAN)),
263            Some(b'I') => self.keyword(b"Infinity", PyJson::Float(f64::INFINITY)),
264            Some(b'-')
265                if self
266                    .bytes
267                    .get(self.pos..)
268                    .is_some_and(|s| s.starts_with(b"-Infinity")) =>
269            {
270                self.keyword(b"-Infinity", PyJson::Float(f64::NEG_INFINITY))
271            }
272            Some(b'"') => {
273                let string = self.string()?;
274                self.node(PyJson::Str(string), 0)
275            }
276            Some(b'[') => self.array_value(depth),
277            Some(b'{') => self.object_value(depth),
278            Some(b'-' | b'0'..=b'9') => self.number(),
279            _ => Err(self.error("expecting value")),
280        }
281    }
282
283    fn keyword(&mut self, word: &[u8], value: PyJson) -> Result<PyJson, PyJsonError> {
284        if self
285            .bytes
286            .get(self.pos..)
287            .is_some_and(|rest| rest.starts_with(word))
288        {
289            self.pos += word.len();
290            self.node(value, 0)
291        } else {
292            Err(self.error("expecting value"))
293        }
294    }
295
296    fn nested(&self, depth: usize) -> Result<usize, PyJsonError> {
297        let next = depth + 1;
298        if next > 1000 {
299            Err(self.error("maximum JSON nesting depth exceeded"))
300        } else {
301            Ok(next)
302        }
303    }
304
305    fn array_value(&mut self, depth: usize) -> Result<PyJson, PyJsonError> {
306        let depth = self.nested(depth)?;
307        self.pos += 1;
308        self.whitespace();
309        let mut values = Vec::new();
310        if self.bytes.get(self.pos) == Some(&b']') {
311            self.pos += 1;
312            return self.node(PyJson::Array(values), 0);
313        }
314        loop {
315            let value = self.value(depth)?;
316            self.reserve_for_push(&mut values)?;
317            values.push(value);
318            self.whitespace();
319            match self.bytes.get(self.pos) {
320                Some(b',') => {
321                    self.pos += 1;
322                    self.whitespace();
323                }
324                Some(b']') => {
325                    self.pos += 1;
326                    return self.node(PyJson::Array(values), 0);
327                }
328                _ => return Err(self.error("expecting ',' or ']'")),
329            }
330        }
331    }
332
333    fn object_value(&mut self, depth: usize) -> Result<PyJson, PyJsonError> {
334        let depth = self.nested(depth)?;
335        self.pos += 1;
336        self.whitespace();
337        let mut values: Vec<(String, PyJson)> = Vec::new();
338        if self.bytes.get(self.pos) == Some(&b'}') {
339            self.pos += 1;
340            return self.node(PyJson::Object(values), 0);
341        }
342        // Key -> position in `values`, only while this object is parsed.
343        let mut index: HashMap<String, usize> = HashMap::new();
344        loop {
345            if self.bytes.get(self.pos) != Some(&b'"') {
346                return Err(self.error("expecting property name"));
347            }
348            let key = self.string()?;
349            self.whitespace();
350            if self.bytes.get(self.pos) != Some(&b':') {
351                return Err(self.error("expecting ':'"));
352            }
353            self.pos += 1;
354            self.whitespace();
355            let value = self.value(depth)?;
356            // Like a Python dict: a repeated key keeps its first position and
357            // takes the last value.
358            match index.entry(key) {
359                Entry::Occupied(slot) => values[*slot.get()].1 = value,
360                Entry::Vacant(slot) => {
361                    self.charge(
362                        slot.key()
363                            .len()
364                            .saturating_add(size_of::<(String, usize)>()),
365                    )?;
366                    let cloned_key = slot.key().clone();
367                    self.charge(cloned_key.capacity().saturating_sub(slot.key().len()))?;
368                    self.reserve_for_push(&mut values)?;
369                    values.push((cloned_key, value));
370                    slot.insert(values.len() - 1);
371                }
372            }
373            self.whitespace();
374            match self.bytes.get(self.pos) {
375                Some(b',') => {
376                    self.pos += 1;
377                    self.whitespace();
378                }
379                Some(b'}') => {
380                    self.pos += 1;
381                    return self.node(PyJson::Object(values), 0);
382                }
383                _ => return Err(self.error("expecting ',' or '}'")),
384            }
385        }
386    }
387
388    fn string_capacity(&self) -> Result<usize, PyJsonError> {
389        let start = self.pos.saturating_add(1);
390        let mut cursor = start;
391        while let Some(&byte) = self.bytes.get(cursor) {
392            match byte {
393                b'"' => return Ok(cursor - start),
394                b'\\' => {
395                    cursor += 1;
396                    if self.bytes.get(cursor).is_none() {
397                        return Err(PyJsonError::new(cursor, "unterminated string"));
398                    }
399                    cursor += 1;
400                }
401                0..=0x1f => {
402                    return Err(PyJsonError::new(cursor, "invalid control character"));
403                }
404                _ => cursor += 1,
405            }
406        }
407        Err(PyJsonError::new(cursor, "unterminated string"))
408    }
409
410    fn string(&mut self) -> Result<String, PyJsonError> {
411        let requested = self.string_capacity()?;
412        self.charge(requested)?;
413        let mut out = String::new();
414        out.try_reserve_exact(requested)
415            .map_err(|_| self.error("JSON allocation failed"))?;
416        self.charge(out.capacity().saturating_sub(requested))?;
417        self.pos += 1;
418        let mut raw_start = self.pos;
419        while let Some(&byte) = self.bytes.get(self.pos) {
420            match byte {
421                b'"' => {
422                    out.push_str(&self.text[raw_start..self.pos]);
423                    self.pos += 1;
424                    return Ok(out);
425                }
426                b'\\' => {
427                    out.push_str(&self.text[raw_start..self.pos]);
428                    self.pos += 1;
429                    let escaped = *self
430                        .bytes
431                        .get(self.pos)
432                        .ok_or_else(|| self.error("unterminated string"))?;
433                    self.pos += 1;
434                    match escaped {
435                        b'"' => out.push('"'),
436                        b'\\' => out.push('\\'),
437                        b'/' => out.push('/'),
438                        b'b' => out.push('\u{8}'),
439                        b'f' => out.push('\u{c}'),
440                        b'n' => out.push('\n'),
441                        b'r' => out.push('\r'),
442                        b't' => out.push('\t'),
443                        b'u' => {
444                            let first = self.hex_unit()?;
445                            let scalar = if (0xd800..=0xdbff).contains(&first) {
446                                if self.bytes.get(self.pos..self.pos.saturating_add(2))
447                                    != Some(b"\\u")
448                                {
449                                    return Err(self.error("unpaired UTF-16 surrogate"));
450                                }
451                                self.pos += 2;
452                                let second = self.hex_unit()?;
453                                if !(0xdc00..=0xdfff).contains(&second) {
454                                    return Err(self.error("unpaired UTF-16 surrogate"));
455                                }
456                                0x10000
457                                    + ((u32::from(first) - 0xd800) << 10)
458                                    + (u32::from(second) - 0xdc00)
459                            } else if (0xdc00..=0xdfff).contains(&first) {
460                                return Err(self.error("unpaired UTF-16 surrogate"));
461                            } else {
462                                u32::from(first)
463                            };
464                            out.push(
465                                char::from_u32(scalar)
466                                    .ok_or_else(|| self.error("invalid Unicode escape"))?,
467                            );
468                        }
469                        _ => return Err(self.error("invalid escape")),
470                    }
471                    raw_start = self.pos;
472                }
473                0..=0x1f => return Err(self.error("invalid control character")),
474                _ => {
475                    let ch = self.text[self.pos..]
476                        .chars()
477                        .next()
478                        .ok_or_else(|| self.error("unterminated string"))?;
479                    self.pos += ch.len_utf8();
480                }
481            }
482        }
483        Err(self.error("unterminated string"))
484    }
485
486    fn hex_unit(&mut self) -> Result<u16, PyJsonError> {
487        let end = self
488            .pos
489            .checked_add(4)
490            .ok_or_else(|| self.error("invalid Unicode escape"))?;
491        let digits = self
492            .text
493            .get(self.pos..end)
494            .ok_or_else(|| self.error("invalid Unicode escape"))?;
495        if !digits.bytes().all(|b| b.is_ascii_hexdigit()) {
496            return Err(self.error("invalid Unicode escape"));
497        }
498        self.pos = end;
499        u16::from_str_radix(digits, 16).map_err(|_| self.error("invalid Unicode escape"))
500    }
501
502    fn number(&mut self) -> Result<PyJson, PyJsonError> {
503        let start = self.pos;
504        if self.bytes.get(self.pos) == Some(&b'-') {
505            self.pos += 1;
506        }
507        match self.bytes.get(self.pos) {
508            Some(b'0') => self.pos += 1,
509            Some(b'1'..=b'9') => {
510                self.pos += 1;
511                while self.bytes.get(self.pos).is_some_and(u8::is_ascii_digit) {
512                    self.pos += 1;
513                }
514            }
515            _ => return Err(self.error("invalid number")),
516        }
517        let mut float = false;
518        if self.bytes.get(self.pos) == Some(&b'.') {
519            float = true;
520            self.pos += 1;
521            let before = self.pos;
522            while self.bytes.get(self.pos).is_some_and(u8::is_ascii_digit) {
523                self.pos += 1;
524            }
525            if self.pos == before {
526                return Err(self.error("invalid number"));
527            }
528        }
529        if matches!(self.bytes.get(self.pos), Some(b'e' | b'E')) {
530            float = true;
531            self.pos += 1;
532            if matches!(self.bytes.get(self.pos), Some(b'+' | b'-')) {
533                self.pos += 1;
534            }
535            let before = self.pos;
536            while self.bytes.get(self.pos).is_some_and(u8::is_ascii_digit) {
537                self.pos += 1;
538            }
539            if self.pos == before {
540                return Err(self.error("invalid number"));
541            }
542        }
543        let raw = &self.text[start..self.pos];
544        if float {
545            let value = raw
546                .parse::<f64>()
547                .map_err(|_| self.error("invalid number"))?;
548            self.node(PyJson::Float(value), 0)
549        } else {
550            let (text_start, bytes) = if raw == "-0" {
551                (None, 1)
552            } else {
553                (Some(start), raw.len())
554            };
555            self.charge(bytes)?;
556            let value = text_start.map_or_else(
557                || "0".to_owned(),
558                |start| self.text[start..self.pos].to_owned(),
559            );
560            self.charge(value.capacity().saturating_sub(bytes))?;
561            self.node(PyJson::Int(value), 0)
562        }
563    }
564}
565
566/// Formats like `json.dumps(value, ensure_ascii=False, indent=2)`.
567pub fn dumps(value: &PyJson) -> String {
568    let mut out = String::new();
569    write_value(value, 0, &mut out);
570    out
571}
572
573/// Formats like `json.dumps(value, ensure_ascii=False)`.
574pub fn dumps_compact(value: &PyJson) -> String {
575    let mut out = String::new();
576    write_compact(value, &mut out);
577    out
578}
579
580fn write_compact(value: &PyJson, out: &mut String) {
581    match value {
582        PyJson::Null => out.push_str("null"),
583        PyJson::Bool(value) => out.push_str(if *value { "true" } else { "false" }),
584        PyJson::Int(value) => out.push_str(value),
585        PyJson::Float(value) => out.push_str(&python_float(*value)),
586        PyJson::Str(value) => write_string(value, out),
587        PyJson::Array(values) => {
588            out.push('[');
589            for (index, value) in values.iter().enumerate() {
590                if index != 0 {
591                    out.push_str(", ");
592                }
593                write_compact(value, out);
594            }
595            out.push(']');
596        }
597        PyJson::Object(values) => {
598            out.push('{');
599            for (index, (key, value)) in values.iter().enumerate() {
600                if index != 0 {
601                    out.push_str(", ");
602                }
603                write_string(key, out);
604                out.push_str(": ");
605                write_compact(value, out);
606            }
607            out.push('}');
608        }
609    }
610}
611
612fn write_value(value: &PyJson, depth: usize, out: &mut String) {
613    match value {
614        PyJson::Null => out.push_str("null"),
615        PyJson::Bool(value) => out.push_str(if *value { "true" } else { "false" }),
616        PyJson::Int(value) => out.push_str(value),
617        PyJson::Float(value) => out.push_str(&python_float(*value)),
618        PyJson::Str(value) => write_string(value, out),
619        PyJson::Array(values) => {
620            if values.is_empty() {
621                out.push_str("[]");
622                return;
623            }
624            out.push_str("[\n");
625            for (index, value) in values.iter().enumerate() {
626                out.push_str(&" ".repeat((depth + 1) * 2));
627                write_value(value, depth + 1, out);
628                if index + 1 != values.len() {
629                    out.push(',');
630                }
631                out.push('\n');
632            }
633            out.push_str(&" ".repeat(depth * 2));
634            out.push(']');
635        }
636        PyJson::Object(values) => {
637            if values.is_empty() {
638                out.push_str("{}");
639                return;
640            }
641            out.push_str("{\n");
642            for (index, (key, value)) in values.iter().enumerate() {
643                out.push_str(&" ".repeat((depth + 1) * 2));
644                write_string(key, out);
645                out.push_str(": ");
646                write_value(value, depth + 1, out);
647                if index + 1 != values.len() {
648                    out.push(',');
649                }
650                out.push('\n');
651            }
652            out.push_str(&" ".repeat(depth * 2));
653            out.push('}');
654        }
655    }
656}
657
658fn write_string(value: &str, out: &mut String) {
659    out.push('"');
660    for ch in value.chars() {
661        match ch {
662            '"' => out.push_str("\\\""),
663            '\\' => out.push_str("\\\\"),
664            '\u{8}' => out.push_str("\\b"),
665            '\u{c}' => out.push_str("\\f"),
666            '\n' => out.push_str("\\n"),
667            '\r' => out.push_str("\\r"),
668            '\t' => out.push_str("\\t"),
669            '\0'..='\u{1f}' => {
670                use std::fmt::Write as _;
671                write!(out, "\\u{:04x}", ch as u32).expect("writing to String cannot fail");
672            }
673            _ => out.push(ch),
674        }
675    }
676    out.push('"');
677}
678
679fn python_float(value: f64) -> String {
680    if value.is_nan() {
681        return "NaN".into();
682    }
683    if value == f64::INFINITY {
684        return "Infinity".into();
685    }
686    if value == f64::NEG_INFINITY {
687        return "-Infinity".into();
688    }
689    if value == 0.0 && value.is_sign_negative() {
690        return "-0.0".into();
691    }
692    let mut text = ryu::Buffer::new().format_finite(value).to_owned();
693    let absolute = value.abs();
694    if !text.contains(['e', 'E']) && absolute != 0.0 && (absolute < 1e-4 || absolute >= 1e16) {
695        let negative = text.starts_with('-');
696        let unsigned = text.trim_start_matches('-');
697        let (digits, exponent) = if absolute < 1.0 {
698            let fractional = unsigned.trim_start_matches("0.");
699            let zeroes = fractional.chars().take_while(|ch| *ch == '0').count();
700            (
701                fractional.trim_start_matches('0').to_owned(),
702                -(zeroes as i32) - 1,
703            )
704        } else {
705            let whole = unsigned.find('.').unwrap_or(unsigned.len());
706            (unsigned.replace('.', ""), whole as i32 - 1)
707        };
708        let (first, rest) = digits.split_at(1);
709        let mantissa = if rest.is_empty() {
710            first.into()
711        } else {
712            format!("{first}.{rest}")
713        };
714        text = format!("{}{mantissa}e{exponent:+}", if negative { "-" } else { "" });
715    }
716    if !text.contains(['.', 'e', 'E']) {
717        text.push_str(".0");
718    }
719    if let Some(index) = text.find('e') {
720        let exponent = text[index + 1..]
721            .parse::<i32>()
722            .expect("ryu exponent is an integer");
723        text = format!(
724            "{}e{}{:02}",
725            &text[..index],
726            if exponent >= 0 { "+" } else { "-" },
727            exponent.abs()
728        );
729    }
730    text
731}
732
733#[cfg(test)]
734mod tests {
735    use super::*;
736
737    #[test]
738    fn scanner_and_encoder_edge_cases() {
739        let input = r#" {"a":-0,"b":-0.0,"c":1e400,"d":340282366920938463463374607431768211456,"a":2,"s":"ν•œκΈ€ πŸ˜€\nπŸ˜€"} "#;
740        let value = loads(input).unwrap();
741        assert_eq!(value.get("a"), Some(&PyJson::Int("2".into())));
742        assert_eq!(value.get("b"), Some(&PyJson::Float(-0.0)));
743        assert_eq!(value.get("c"), Some(&PyJson::Float(f64::INFINITY)));
744        assert!(dumps(&value).contains("340282366920938463463374607431768211456"));
745        assert!(dumps(&value).contains("ν•œκΈ€ πŸ˜€\\nπŸ˜€"));
746    }
747
748    #[test]
749    fn rejects_controls_surrogates_and_excess_depth() {
750        assert!(loads("\"line\n\"").is_err());
751        assert!(loads(r#""\ud800""#).is_err());
752        assert!(loads(&format!("{}0{}", "[".repeat(1000), "]".repeat(1000))).is_ok());
753        assert!(loads(&format!("{}0{}", "[".repeat(1001), "]".repeat(1001))).is_err());
754    }
755
756    #[test]
757    fn allocation_budget_rejects_a_wide_array_td19() {
758        let document = format!("[{}]", vec!["null"; 64].join(","));
759        let budget = std::mem::size_of::<PyJson>() * 8;
760        let error = loads_with_budget(&document, budget).unwrap_err();
761        assert!(error.to_string().contains("allocation budget"));
762        assert!(loads(&document).is_ok());
763    }
764
765    #[test]
766    fn allocation_budget_rejects_a_large_string_before_allocating_td19() {
767        let document = format!("\"{}\"", "x".repeat(2 * 1024 * 1024));
768        let error = loads_with_budget(&document, 64).unwrap_err();
769        assert_eq!(error.offset, 0);
770        assert!(error.to_string().contains("allocation budget"));
771    }
772}