Skip to main content

taba_client/json/
mod.rs

1//! Minimal JSON reader (doc 19, section 18.5).
2//!
3//! Walks the text without building a tree: it can skip any value, hands out an object's fields
4//! as name → text-span pairs and decodes a JSON string. A message's `body` is
5//! any JSON, so the connector returns the text span without interpreting it.
6
7use std::fmt;
8
9/// Maximum nesting depth accepted; above it the document is refused instead of overflowing
10/// the stack.
11pub const MAX_DEPTH: usize = 512;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct JsonError {
15    pub position: usize,
16    pub message: &'static str,
17}
18
19impl fmt::Display for JsonError {
20    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21        write!(
22            f,
23            "invalid JSON at position {}: {}",
24            self.position, self.message
25        )
26    }
27}
28
29impl std::error::Error for JsonError {}
30
31type Result<T> = std::result::Result<T, JsonError>;
32
33fn err<T>(position: usize, message: &'static str) -> Result<T> {
34    Err(JsonError { position, message })
35}
36
37fn ws(b: &[u8], mut i: usize) -> usize {
38    while matches!(b.get(i), Some(b' ' | b'\t' | b'\n' | b'\r')) {
39        i += 1;
40    }
41    i
42}
43
44/// Checks that the whole of `s` is a single valid JSON value (surrounding spaces are accepted).
45pub fn validate(s: &str) -> Result<()> {
46    let end = skip(s.as_bytes(), 0, 0)?;
47    finish(s.as_bytes(), end, Vec::new()).map(|_| ())
48}
49
50/// Skips the value that starts at `start` (after spaces) and returns the index right after it.
51pub fn skip_value(s: &str, start: usize) -> Result<usize> {
52    skip(s.as_bytes(), start, 0)
53}
54
55fn skip(b: &[u8], i: usize, depth: usize) -> Result<usize> {
56    if depth > MAX_DEPTH {
57        return err(i, "nesting too deep");
58    }
59    let i = ws(b, i);
60    match b.get(i).copied() {
61        None => err(i, "unexpected end"),
62        Some(b'{') => {
63            let mut i = ws(b, i + 1);
64            if b.get(i) == Some(&b'}') {
65                return Ok(i + 1);
66            }
67            loop {
68                i = skip_string(b, i)?;
69                i = ws(b, i);
70                if b.get(i) != Some(&b':') {
71                    return err(i, "esperava ':'");
72                }
73                i = skip(b, i + 1, depth + 1)?;
74                i = ws(b, i);
75                match b.get(i) {
76                    Some(b',') => i = ws(b, i + 1),
77                    Some(b'}') => return Ok(i + 1),
78                    _ => return err(i, "expected ',' or '}'"),
79                }
80            }
81        }
82        Some(b'[') => {
83            let mut i = ws(b, i + 1);
84            if b.get(i) == Some(&b']') {
85                return Ok(i + 1);
86            }
87            loop {
88                i = skip(b, i, depth + 1)?;
89                i = ws(b, i);
90                match b.get(i) {
91                    Some(b',') => i += 1,
92                    Some(b']') => return Ok(i + 1),
93                    _ => return err(i, "expected ',' or ']'"),
94                }
95            }
96        }
97        Some(b'"') => skip_string(b, i),
98        Some(b't') => literal(b, i, b"true"),
99        Some(b'f') => literal(b, i, b"false"),
100        Some(b'n') => literal(b, i, b"null"),
101        Some(b'-' | b'0'..=b'9') => skip_number(b, i),
102        Some(_) => err(i, "unexpected value"),
103    }
104}
105
106fn literal(b: &[u8], i: usize, word: &[u8]) -> Result<usize> {
107    if b[i..].starts_with(word) {
108        Ok(i + word.len())
109    } else {
110        err(i, "invalid literal")
111    }
112}
113
114fn skip_string(b: &[u8], i: usize) -> Result<usize> {
115    if b.get(i) != Some(&b'"') {
116        return err(i, "expected a string");
117    }
118    let mut i = i + 1;
119    loop {
120        match b.get(i).copied() {
121            None => return err(i, "unterminated string"),
122            Some(b'"') => return Ok(i + 1),
123            Some(b'\\') => match b.get(i + 1).copied() {
124                Some(b'"' | b'\\' | b'/' | b'b' | b'f' | b'n' | b'r' | b't') => i += 2,
125                Some(b'u') => {
126                    let hex = b.get(i + 2..i + 6);
127                    if hex.is_some_and(|h| h.iter().all(u8::is_ascii_hexdigit)) {
128                        i += 6;
129                    } else {
130                        return err(i, "invalid \\u escape");
131                    }
132                }
133                _ => return err(i, "invalid escape"),
134            },
135            Some(c) if c < 0x20 => return err(i, "control character in string"),
136            Some(_) => i += 1,
137        }
138    }
139}
140
141fn skip_number(b: &[u8], mut i: usize) -> Result<usize> {
142    let digits = |b: &[u8], mut i: usize| -> (usize, usize) {
143        let start = i;
144        while matches!(b.get(i), Some(b'0'..=b'9')) {
145            i += 1;
146        }
147        (i, i - start)
148    };
149    if b.get(i) == Some(&b'-') {
150        i += 1;
151    }
152    match b.get(i) {
153        Some(b'0') => i += 1,
154        Some(b'1'..=b'9') => i = digits(b, i).0,
155        _ => return err(i, "invalid number"),
156    }
157    if b.get(i) == Some(&b'.') {
158        let (next, count) = digits(b, i + 1);
159        if count == 0 {
160            return err(i, "invalid number");
161        }
162        i = next;
163    }
164    if matches!(b.get(i), Some(b'e' | b'E')) {
165        i += 1;
166        if matches!(b.get(i), Some(b'+' | b'-')) {
167            i += 1;
168        }
169        let (next, count) = digits(b, i);
170        if count == 0 {
171            return err(i, "invalid number");
172        }
173        i = next;
174    }
175    Ok(i)
176}
177
178/// The fields of an object, with the name still quoted and the value as a text span.
179fn raw_fields(s: &str) -> Result<Vec<(&str, &str)>> {
180    let b = s.as_bytes();
181    let mut i = ws(b, 0);
182    if b.get(i) != Some(&b'{') {
183        return err(i, "expected an object");
184    }
185    i = ws(b, i + 1);
186    let mut out = Vec::new();
187    if b.get(i) == Some(&b'}') {
188        return finish(b, i + 1, out);
189    }
190    loop {
191        let key_end = skip_string(b, i)?;
192        let key = &s[i..key_end];
193        i = ws(b, key_end);
194        if b.get(i) != Some(&b':') {
195            return err(i, "esperava ':'");
196        }
197        let value_start = ws(b, i + 1);
198        let value_end = skip(b, value_start, 1)?;
199        out.push((key, &s[value_start..value_end]));
200        i = ws(b, value_end);
201        match b.get(i) {
202            Some(b',') => i = ws(b, i + 1),
203            Some(b'}') => return finish(b, i + 1, out),
204            _ => return err(i, "expected ',' or '}'"),
205        }
206    }
207}
208
209fn finish<'a>(b: &[u8], i: usize, out: Vec<(&'a str, &'a str)>) -> Result<Vec<(&'a str, &'a str)>> {
210    let i = ws(b, i);
211    if i != b.len() {
212        return err(i, "text after the document");
213    }
214    Ok(out)
215}
216
217/// The fields of an object: decoded name and the text span of each value.
218pub fn object_fields(s: &str) -> Result<Vec<(String, &str)>> {
219    raw_fields(s)?
220        .into_iter()
221        .map(|(k, v)| Ok((decode_string(k)?, v)))
222        .collect()
223}
224
225/// The items of a list, each one as a text span.
226pub fn array_items(s: &str) -> Result<Vec<&str>> {
227    let b = s.as_bytes();
228    let mut i = ws(b, 0);
229    if b.get(i) != Some(&b'[') {
230        return err(i, "expected a list");
231    }
232    i = ws(b, i + 1);
233    let mut out = Vec::new();
234    if b.get(i) == Some(&b']') {
235        finish(b, i + 1, Vec::new())?;
236        return Ok(out);
237    }
238    loop {
239        let end = skip(b, i, 1)?;
240        out.push(s[i..end].trim());
241        i = ws(b, end);
242        match b.get(i) {
243            Some(b',') => i = ws(b, i + 1),
244            Some(b']') => {
245                finish(b, i + 1, Vec::new())?;
246                return Ok(out);
247            }
248            _ => return err(i, "expected ',' or ']'"),
249        }
250    }
251}
252
253/// Decodes a JSON string (`raw` includes the quotes): escapes, `\uXXXX` and surrogate pairs.
254pub fn decode_string(raw: &str) -> Result<String> {
255    let raw = raw.trim();
256    let b = raw.as_bytes();
257    if b.len() < 2 || b[0] != b'"' || b[b.len() - 1] != b'"' {
258        return err(0, "expected a string");
259    }
260    let inner = &raw[1..raw.len() - 1];
261    let mut out = String::with_capacity(inner.len());
262    let mut chars = inner.char_indices();
263    while let Some((at, c)) = chars.next() {
264        if c != '\\' {
265            out.push(c);
266            continue;
267        }
268        let Some((_, e)) = chars.next() else {
269            return err(at, "escape incompleto");
270        };
271        match e {
272            '"' => out.push('"'),
273            '\\' => out.push('\\'),
274            '/' => out.push('/'),
275            'b' => out.push('\u{8}'),
276            'f' => out.push('\u{c}'),
277            'n' => out.push('\n'),
278            'r' => out.push('\r'),
279            't' => out.push('\t'),
280            'u' => {
281                let first = hex4(&mut chars, at)?;
282                if (0xD800..0xDC00).contains(&first) {
283                    // Surrogate pair: the second escape must follow right away.
284                    let mut ahead = chars.clone();
285                    let paired = match (ahead.next(), ahead.next()) {
286                        (Some((_, '\\')), Some((_, 'u'))) => {
287                            let second = hex4(&mut ahead, at)?;
288                            if (0xDC00..0xE000).contains(&second) {
289                                chars = ahead;
290                                Some(0x10000 + ((first - 0xD800) << 10) + (second - 0xDC00))
291                            } else {
292                                None
293                            }
294                        }
295                        _ => None,
296                    };
297                    out.push(paired.and_then(char::from_u32).unwrap_or('\u{FFFD}'));
298                } else {
299                    out.push(char::from_u32(first).unwrap_or('\u{FFFD}'));
300                }
301            }
302            _ => return err(at, "invalid escape"),
303        }
304    }
305    Ok(out)
306}
307
308fn hex4(chars: &mut std::str::CharIndices<'_>, at: usize) -> Result<u32> {
309    let mut value = 0;
310    for _ in 0..4 {
311        let digit = chars.next().and_then(|(_, c)| c.to_digit(16));
312        match digit {
313            Some(d) => value = value * 16 + d,
314            None => return err(at, "invalid \\u escape"),
315        }
316    }
317    Ok(value)
318}
319
320/// Writes `s` as a JSON string, in quotes.
321pub fn quote(s: &str) -> String {
322    let mut out = String::with_capacity(s.len() + 2);
323    out.push('"');
324    for c in s.chars() {
325        match c {
326            '"' => out.push_str("\\\""),
327            '\\' => out.push_str("\\\\"),
328            '\n' => out.push_str("\\n"),
329            '\r' => out.push_str("\\r"),
330            '\t' => out.push_str("\\t"),
331            c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
332            c => out.push(c),
333        }
334    }
335    out.push('"');
336    out
337}
338
339/// The fields of an object, searchable by name.
340pub struct Fields<'a>(Vec<(String, &'a str)>);
341
342impl<'a> Fields<'a> {
343    pub fn parse(s: &'a str) -> Result<Self> {
344        object_fields(s).map(Fields)
345    }
346
347    /// The field's text span, if it exists.
348    pub fn raw(&self, name: &str) -> Option<&'a str> {
349        self.0.iter().find(|(k, _)| k == name).map(|(_, v)| *v)
350    }
351
352    /// The field as text, if it exists and is a string.
353    pub fn string(&self, name: &str) -> Option<String> {
354        let raw = self.raw(name)?;
355        if raw.starts_with('"') {
356            decode_string(raw).ok()
357        } else {
358            None
359        }
360    }
361
362    /// The field as an unsigned integer, if it exists and is such a number.
363    pub fn unsigned(&self, name: &str) -> Option<u64> {
364        self.raw(name)?.parse().ok()
365    }
366
367    /// The field as a boolean, if it exists and is `true` or `false`.
368    pub fn boolean(&self, name: &str) -> Option<bool> {
369        match self.raw(name)? {
370            "true" => Some(true),
371            "false" => Some(false),
372            _ => None,
373        }
374    }
375
376    pub fn names(&self) -> impl Iterator<Item = &str> {
377        self.0.iter().map(|(k, _)| k.as_str())
378    }
379}
380
381/// Re-indents a JSON document with two spaces, without touching the text of strings and numbers.
382pub fn pretty(s: &str) -> Result<String> {
383    let mut out = String::with_capacity(s.len() * 2);
384    pretty_into(s.trim(), &mut out, 0, 0)?;
385    Ok(out)
386}
387
388fn pretty_into(s: &str, out: &mut String, indent: usize, depth: usize) -> Result<()> {
389    if depth > MAX_DEPTH {
390        return err(0, "nesting too deep");
391    }
392    match s.as_bytes().first() {
393        Some(b'{') => {
394            let fields = raw_fields(s)?;
395            if fields.is_empty() {
396                out.push_str("{}");
397                return Ok(());
398            }
399            out.push_str("{\n");
400            for (n, (key, value)) in fields.iter().enumerate() {
401                out.push_str(&" ".repeat(indent + 2));
402                out.push_str(key);
403                out.push_str(": ");
404                pretty_into(value, out, indent + 2, depth + 1)?;
405                out.push_str(if n + 1 < fields.len() { ",\n" } else { "\n" });
406            }
407            out.push_str(&" ".repeat(indent));
408            out.push('}');
409        }
410        Some(b'[') => {
411            let items = array_items(s)?;
412            if items.is_empty() {
413                out.push_str("[]");
414                return Ok(());
415            }
416            out.push_str("[\n");
417            for (n, item) in items.iter().enumerate() {
418                out.push_str(&" ".repeat(indent + 2));
419                pretty_into(item, out, indent + 2, depth + 1)?;
420                out.push_str(if n + 1 < items.len() { ",\n" } else { "\n" });
421            }
422            out.push_str(&" ".repeat(indent));
423            out.push(']');
424        }
425        _ => {
426            let end = skip_value(s, 0)?;
427            if ws(s.as_bytes(), end) != s.len() {
428                return err(end, "text after the document");
429            }
430            out.push_str(s.trim());
431        }
432    }
433    Ok(())
434}
435
436#[cfg(test)]
437mod tests;