Skip to main content

uqa_core/json/
string.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! String tokens use the shared grammar and admit their decoded UTF-8 output before production.
8
9use super::{Budgeted, CancellationToken, JsonReadError, JsonReader, JsonToken, MemoryBudget};
10use crate::memory::{Produced, ProductionControl, ProductionString};
11
12/// Decode one quoted JSON string under a shared allowance, retaining its actual destination capacity.
13pub fn decode_json_string(
14    encoded: impl AsRef<[u8]>,
15    memory: &MemoryBudget,
16    cancellation: &CancellationToken,
17) -> Result<Budgeted<String>, JsonReadError> {
18    decode_json_string_with_control(
19        encoded,
20        &ProductionControl::new(memory, cancellation, cancellation),
21    )?
22    .into_budgeted()
23    .map_err(|_| unreachable!("controlled JSON string"))
24}
25
26/// Decode with the existing token grammar and one admitted string constructor. Both cancellation owners are checked during validation and output; no parser-owned escape scratch or second output string is allocated.
27pub fn decode_json_string_with_control(
28    encoded: impl AsRef<[u8]>,
29    control: &ProductionControl<'_>,
30) -> Result<Produced<String>, JsonReadError> {
31    control.check()?;
32    let input = std::str::from_utf8(encoded.as_ref()).map_err(|_| JsonReadError::InvalidJson)?;
33    let mut reader = JsonReader::with_control(input, control);
34    let event = reader.next_event()?.ok_or(JsonReadError::InvalidJson)?;
35    let JsonToken::String(token) = event.token else {
36        return Err(JsonReadError::InvalidJson);
37    };
38    if reader.next_event()?.is_some() {
39        return Err(JsonReadError::InvalidJson);
40    }
41    let text = std::str::from_utf8(token).map_err(|_| JsonReadError::InvalidJson)?;
42    let mut output = ProductionString::new(*control);
43    let mut position = 1;
44    while position < text.len() - 1 {
45        control.check()?;
46        let start = position;
47        while position < text.len() - 1 && text.as_bytes()[position] != b'\\' {
48            position += 1;
49            if position.is_multiple_of(4096) {
50                control.check()?;
51            }
52        }
53        output.push_str(&text[start..position])?;
54        if position == text.len() - 1 {
55            break;
56        }
57        position += 1;
58        let escape = text.as_bytes()[position];
59        position += 1;
60        let character = match escape {
61            b'"' => '"',
62            b'\\' => '\\',
63            b'/' => '/',
64            b'b' => '\u{0008}',
65            b'f' => '\u{000c}',
66            b'n' => '\n',
67            b'r' => '\r',
68            b't' => '\t',
69            b'u' => {
70                let mut code = hex_unit(text, &mut position);
71                if (0xd800..=0xdbff).contains(&code) {
72                    position += 2;
73                    let low = hex_unit(text, &mut position);
74                    code = 0x10000 + ((code - 0xd800) << 10) + (low - 0xdc00);
75                }
76                char::from_u32(code).expect("validated JSON Unicode escape")
77            }
78            _ => unreachable!("validated JSON escape"),
79        };
80        output.push(character)?;
81    }
82    output.finish().map_err(Into::into)
83}
84
85fn hex_unit(text: &str, position: &mut usize) -> u32 {
86    let value = u32::from_str_radix(&text[*position..*position + 4], 16)
87        .expect("validated JSON hexadecimal escape");
88    *position += 4;
89    value
90}