1use 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#[derive(Clone, Debug, PartialEq)]
12pub enum PyJson {
13 Null,
14 Bool(bool),
15 Int(String),
17 Float(f64),
19 Str(String),
20 Array(Vec<PyJson>),
21 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 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#[derive(Debug)]
86pub(crate) enum Shape {
87 Record {
93 optional: &'static [&'static str],
94 nested: &'static [(&'static str, Shape)],
95 },
96 Map(&'static Shape),
99 List(&'static Shape),
102}
103
104pub(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
185pub 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
194pub 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 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 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
566pub fn dumps(value: &PyJson) -> String {
568 let mut out = String::new();
569 write_value(value, 0, &mut out);
570 out
571}
572
573pub 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}