Skip to main content

ocpi_tariffs/json/
decode.rs

1//! String decoding for JSON values parsed by [`super`].
2//!
3//! # Parsing vs decoding
4//!
5//! The JSON parser ([`super::parser`]) is intentionally lenient: it accepts
6//! any structurally valid JSON string, leaving escape sequences and control
7//! characters verbatim inside a [`super::RawStr`]. Decoding is a separate
8//! step performed by this module.
9//!
10//! Keeping these concerns apart means the linter can treat string-encoding
11//! problems as warnings rather than fatal parse errors. The caller receives
12//! both a best-effort string value and a list of [`Warning`]s describing what
13//! was wrong and where, which is enough information to suggest a corrected
14//! encoding to the user.
15#[cfg(test)]
16mod test_from_str;
17
18use std::{borrow::Cow, fmt, iter::Peekable, ops::RangeInclusive};
19
20use super::Element;
21use crate::{
22    warning::{self, CaveatDeferred, IntoCaveatDeferred as _},
23    Caveat, IntoCaveat as _,
24};
25
26const ESCAPE_CHAR: char = '\\';
27
28/// The kind of `Warning` that can happen when decoding a `&str`.
29#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
30pub enum Warning {
31    /// Control chars were found while parsing a JSON string.
32    ControlCharacter(usize),
33
34    /// A UTF-16 surrogate pair failed to decode.
35    DecodeUtf16(usize, u16),
36
37    /// A string contains invalid escape chars.
38    InvalidEscape(usize),
39
40    /// The String ended before the parser expected.
41    UnexpectedEndOfString(usize),
42}
43
44impl crate::Warning for Warning {
45    /// A human readable identifier for each `Warning`.
46    fn id(&self) -> warning::Id {
47        match self {
48            Self::ControlCharacter(_) => {
49                warning::Id::from_static("control_character_while_parsing_string")
50            }
51            Self::DecodeUtf16(..) => warning::Id::from_static("decode_utf_1_6"),
52            Self::InvalidEscape(_) => warning::Id::from_static("invalid_escape"),
53            Self::UnexpectedEndOfString(_) => warning::Id::from_static("unexpected_end_of_string"),
54        }
55    }
56}
57
58impl fmt::Display for Warning {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        match self {
61            Self::ControlCharacter(index) => {
62                write!(
63                    f,
64                    "Control chars were found at index `{index}` while decoding a JSON string."
65                )
66            }
67            Self::DecodeUtf16(index, code) => {
68                write!(
69                    f,
70                    "A UTF-16 surrogate pair `{code}` failed to decode at index: `{index}`."
71                )
72            }
73            Self::InvalidEscape(index) => {
74                write!(
75                    f,
76                    "String contains an invalid escape char at index: `{index})`."
77                )
78            }
79            Self::UnexpectedEndOfString(index) => {
80                write!(f, "The String ended prematurely at index: `{index}`.")
81            }
82        }
83    }
84}
85
86/// Return a `PendingStr` that marks the inner `&str` is containing escape codes or not.
87pub(super) fn analyze<'buf>(
88    s: &'buf str,
89    elem: &Element<'buf>,
90) -> Caveat<super::PendingStr<'buf>, Warning> {
91    let mut warnings = warning::Set::new();
92
93    // Strings are expected to be small so running over all bytes
94    // with the intent of early exiting is acceptable.
95    if let Some((index, _)) = s.char_indices().find(|(_, ch)| ch.is_control()) {
96        warnings.insert(elem, Warning::ControlCharacter(index));
97    }
98
99    if s.chars().any(|ch| ch == ESCAPE_CHAR) {
100        super::PendingStr::HasEscapes(super::EscapeStr(s)).into_caveat(warnings)
101    } else {
102        super::PendingStr::NoEscapes(s).into_caveat(warnings)
103    }
104}
105
106/// Return the `str` with escaped chars replaced with the decoded equivalent.
107///
108/// Returns `Cow::Owned` when all escapes decoded successfully.
109/// Returns `Cow::Borrowed` is the raw string doesn't contain any escapes
110/// or if there is any issues/warnings with the source `&str`.
111pub(super) fn from_raw<'buf>(s: &'buf str) -> CaveatDeferred<Cow<'buf, str>, Warning> {
112    let mut warnings = warning::SetDeferred::new();
113
114    // Strings are expected to be small so running over all
115    // bytes to early out is acceptable.
116    if !s.chars().any(|ch| ch == ESCAPE_CHAR) {
117        if let Some((index, _)) = s.char_indices().find(|(_, ch)| ch.is_control()) {
118            warnings.insert(Warning::ControlCharacter(index));
119        }
120        return Cow::Borrowed(s).into_caveat_deferred(warnings);
121    }
122
123    let mut buf = Buffer::with_capacity(s.len());
124    for decoded in Decoded::from_str(s) {
125        match decoded {
126            Ok(ch) => buf.push(ch),
127            Err(warn_kind) => {
128                warnings.insert(warn_kind);
129                return Cow::Borrowed(s).into_caveat_deferred(warnings);
130            }
131        }
132    }
133
134    Cow::<'buf, str>::Owned(buf.into_string()).into_caveat_deferred(warnings)
135}
136
137/// Scan a raw JSON string body for the lexical issues
138/// [`super::RawStr::lexical_issues`] reports: whether it contains escape sequences,
139/// and whether its decoded form contains non-printable ASCII. Done in a single pass
140/// without allocating.
141pub(super) fn lexical_issues(s: &str) -> super::LexicalIssues {
142    // `\` is ASCII (`0x5C`) so it cannot appear within a multi-byte UTF-8 sequence; a
143    // raw byte search is therefore a correct escape-presence test.
144    let escapes = s.as_bytes().contains(&b'\\');
145
146    // Without escapes the raw bytes are the content. With escapes we must decode to see
147    // the real chars (` ` decodes to a space, for example). Decoding on the fly
148    // avoids the allocation that `from_raw` would make.
149    let non_printable_ascii = if escapes {
150        decoded_has_non_printable_ascii(s)
151    } else {
152        s.chars().any(is_non_printable_ascii)
153    };
154
155    super::LexicalIssues {
156        escapes,
157        non_printable_ascii,
158    }
159}
160
161/// Whether the decoded form of `s` contains a non-printable ASCII char.
162///
163/// A decode failure mirrors [`from_raw`], which returns the raw string unchanged on
164/// error; the scan then falls back to the raw bytes.
165fn decoded_has_non_printable_ascii(s: &str) -> bool {
166    let mut found = false;
167    for decoded in Decoded::from_str(s) {
168        match decoded {
169            Ok(ch) => {
170                if is_non_printable_ascii(ch) {
171                    found = true;
172                }
173            }
174            Err(_) => return s.chars().any(is_non_printable_ascii),
175        }
176    }
177    found
178}
179
180/// Whether `ch` is an ASCII control character or ASCII whitespace.
181fn is_non_printable_ascii(ch: char) -> bool {
182    ch.is_ascii_whitespace() || ch.is_ascii_control()
183}
184
185/// Compare `other` against the decoded form of the raw JSON string body `raw`,
186/// decoding escape sequences on the fly. Does not allocate.
187///
188/// Returns `Ok(true)` / `Ok(false)` for the comparison, or `Err` if `raw` has a
189/// decoding problem (invalid escape, control character, ...) at or before the
190/// first differing character. Once the two strings are known to differ, decoding
191/// stops, so a decode problem beyond that point is not reported.
192pub(super) fn eq(raw: &str, other: &str) -> Result<bool, Warning> {
193    let mut decoded = Decoded::from_str(raw);
194    let mut expected = other.chars();
195
196    loop {
197        match decoded.next() {
198            Some(Err(warn_kind)) => return Err(warn_kind),
199            Some(Ok(actual)) => {
200                if expected.next() != Some(actual) {
201                    return Ok(false);
202                }
203            }
204            // `raw` is exhausted; the strings are equal only if `other` is too.
205            None => return Ok(expected.next().is_none()),
206        }
207    }
208}
209
210/// Like [`eq`], but compares ASCII letters case-insensitively.
211pub(super) fn eq_ignore_ascii_case(raw: &str, other: &str) -> Result<bool, Warning> {
212    let mut decoded = Decoded::from_str(raw);
213    let mut expected = other.chars();
214
215    loop {
216        match decoded.next() {
217            Some(Err(warn_kind)) => return Err(warn_kind),
218            Some(Ok(actual)) => match expected.next() {
219                Some(expected) if expected.eq_ignore_ascii_case(&actual) => {}
220                _ => return Ok(false),
221            },
222            // `raw` is exhausted; the strings are equal only if `other` is too.
223            None => return Ok(expected.next().is_none()),
224        }
225    }
226}
227
228/// Parses a JSON escape sequence and appends it into the `Buffer`. Assumes
229/// the previous byte read was a backslash.
230///
231/// * See: <https://datatracker.ietf.org/doc/html/rfc8259#section-7>
232fn parse_escape(chars: &mut Chars<'_>) -> Result<char, Warning> {
233    let (index, ch) = chars.next_or_eof()?;
234
235    let ch = match ch {
236        '"' => '"',
237        '\\' => '\\',
238        '/' => '/',
239        'b' => '\x08',
240        'f' => '\x0c',
241        'n' => '\n',
242        'r' => '\r',
243        't' => '\t',
244        'u' => return parse_unicode_escape(chars),
245        _ => {
246            return Err(Warning::InvalidEscape(index));
247        }
248    };
249
250    if ch.is_control() {
251        return Err(Warning::ControlCharacter(index));
252    }
253
254    Ok(ch)
255}
256
257/// Parses a JSON `\u` escape and appends it into the buffer.
258/// Assumes `\u` has just been read.
259///
260/// The Unicode escape might be a UTF-16 surrogate pair.
261///
262/// * See: <https://datatracker.ietf.org/doc/html/rfc8259#section-8.2>
263fn parse_unicode_escape(chars: &mut Chars<'_>) -> Result<char, Warning> {
264    // High surrogates occupy `U+D800..=U+DBFF`. A high surrogate is the first
265    // code unit of a UTF-16 surrogate pair; it must be followed by a low
266    // surrogate (`U+DC00..=U+DFFF`) to form a valid scalar value. Lone high
267    // surrogates and all low surrogates are not valid Unicode scalar values.
268    const HIGH_SURROGATE: RangeInclusive<u16> = 0xD800..=0xDBFF;
269
270    let n1 = decode_hex_escape(chars)?;
271
272    let ch = if HIGH_SURROGATE.contains(&n1) {
273        // Only look for a surrogate-pair continuation when N1 is a high surrogate.
274        // Calling `is_next_escape` unconditionally would consume the `\` of any
275        // following escape and pass two unrelated BMP codepoints to
276        // decode_surrogate_pair, which only decodes the first and silently drops the second.
277        let Some(n2) = chars.is_next_escape()? else {
278            return Err(Warning::InvalidEscape(chars.index));
279        };
280        decode_surrogate_pair(n1, n2, chars.index)?
281    } else {
282        let Some(ch) = char::from_u32(u32::from(n1)) else {
283            return Err(Warning::InvalidEscape(chars.index));
284        };
285        ch
286    };
287
288    if ch.is_control() {
289        return Err(Warning::ControlCharacter(chars.index));
290    }
291
292    Ok(ch)
293}
294
295/// A char iterator that can fail if the next char is a control char.
296struct Chars<'buf> {
297    /// The `char` iterator.
298    ///
299    /// This needs to be a `CharIndices` as the `Chars` iterator skips over escaped chars.
300    /// And this needs to be `Peekable` as we need to look ahead to detect a potential second
301    /// Unicode literal and treat that as a UTF-16 surrogate pair.
302    char_indices: Peekable<std::str::CharIndices<'buf>>,
303
304    /// A single character pushed back by `is_next_escape` when it consumed a `\`
305    /// speculatively but found it was not the start of a `\uXXXX` sequence.
306    push_back: Option<(usize, char)>,
307
308    /// The last parsed char index.
309    index: usize,
310}
311
312impl<'buf> Chars<'buf> {
313    /// Create a new `Chars` iterator from a `&str`.
314    fn from_str(s: &'buf str) -> Self {
315        Self {
316            char_indices: s.char_indices().peekable(),
317            push_back: None,
318            index: 0,
319        }
320    }
321
322    /// Return the next char as `Ok` or return `Err(UnexpectedEndOfString)` if there is no char
323    /// or return `Err(ControlCharacter)` if the next char is a control char.
324    fn next_or_eof(&mut self) -> Result<(usize, char), Warning> {
325        if let Some((index, ch)) = self.next() {
326            if ch.is_control() {
327                return Err(Warning::ControlCharacter(index));
328            }
329
330            Ok((index, ch))
331        } else {
332            Err(Warning::UnexpectedEndOfString(self.index))
333        }
334    }
335
336    /// Look ahead in the char stream and if there is another `\uXXXX` sequence
337    /// return it as a decoded hex value.
338    ///
339    /// If a `\` is found but not followed by `u`, the `\` is pushed back so the
340    /// outer loop can process it as the start of a different escape sequence.
341    fn is_next_escape(&mut self) -> Result<Option<u16>, Warning> {
342        let Some(backslash) = self.char_indices.next_if(|(_, ch)| *ch == ESCAPE_CHAR) else {
343            return Ok(None);
344        };
345
346        if self.char_indices.next_if(|(_, ch)| *ch == 'u').is_none() {
347            self.push_back = Some(backslash);
348            return Ok(None);
349        }
350
351        let n = decode_hex_escape(self)?;
352        Ok(Some(n))
353    }
354}
355
356impl Iterator for Chars<'_> {
357    type Item = (usize, char);
358
359    fn next(&mut self) -> Option<Self::Item> {
360        if let Some(item) = self.push_back.take() {
361            self.index = item.0;
362            return Some(item);
363        }
364        if let Some((index, char)) = self.char_indices.next() {
365            self.index = index;
366            Some((index, char))
367        } else {
368            None
369        }
370    }
371}
372
373/// Iterator over the decoded `char`s of a JSON string body (quotes removed).
374///
375/// Escape sequences are decoded on the fly; control characters and malformed
376/// escapes are returned as `Err`. Callers typically stop at the first `Err`.
377struct Decoded<'buf> {
378    chars: Chars<'buf>,
379}
380
381impl<'buf> Decoded<'buf> {
382    /// Create a `Decoded` iterator from a JSON string body.
383    fn from_str(s: &'buf str) -> Self {
384        Self {
385            chars: Chars::from_str(s),
386        }
387    }
388}
389
390impl Iterator for Decoded<'_> {
391    type Item = Result<char, Warning>;
392
393    fn next(&mut self) -> Option<Self::Item> {
394        let (index, ch) = self.chars.next()?;
395
396        if ch == ESCAPE_CHAR {
397            Some(parse_escape(&mut self.chars))
398        } else if ch.is_control() {
399            Some(Err(Warning::ControlCharacter(index)))
400        } else {
401            Some(Ok(ch))
402        }
403    }
404}
405
406/// The `String` based buffer where we accumulate the decoded JSON string.
407struct Buffer {
408    /// The `String` to accumulate chars in.
409    buf: String,
410}
411
412impl Buffer {
413    /// Create a new `Buffer`.
414    fn with_capacity(capacity: usize) -> Self {
415        Self {
416            buf: String::with_capacity(capacity),
417        }
418    }
419
420    /// Push an already-decoded char into the `String`.
421    fn push(&mut self, ch: char) {
422        self.buf.push(ch);
423    }
424
425    /// Consume the `Buffer` and return the inner `String`.
426    fn into_string(self) -> String {
427        self.buf
428    }
429}
430
431/// Decode the high and low code units of a UTF-16 surrogate pair into a `char`.
432///
433/// Returns `Err(DecodeUtf16)` if the two code units do not form a valid pair.
434fn decode_surrogate_pair(n1: u16, n2: u16, index: usize) -> Result<char, Warning> {
435    let Some(ch) = char::decode_utf16([n1, n2]).next() else {
436        return Err(Warning::InvalidEscape(index));
437    };
438
439    match ch {
440        Ok(ch) => Ok(ch),
441        Err(err) => Err(Warning::DecodeUtf16(index, err.unpaired_surrogate())),
442    }
443}
444
445/// Munch four chars as bytes and try to convert them into a `char`.
446fn decode_hex_escape(chars: &mut Chars<'_>) -> Result<u16, Warning> {
447    const RADIX: u32 = 16;
448
449    let (_, one) = chars.next_or_eof()?;
450    let (_, two) = chars.next_or_eof()?;
451    let (_, three) = chars.next_or_eof()?;
452    let (index, four) = chars.next_or_eof()?;
453
454    let string = [one, two, three, four].into_iter().collect::<String>();
455    let Ok(n) = u16::from_str_radix(&string, RADIX) else {
456        return Err(Warning::InvalidEscape(index));
457    };
458
459    Ok(n)
460}