Skip to main content

blazingly_json/
canonical.rs

1// These tiny methods are a cross-crate code-generation primitive. Benchmarks
2// show a material protocol-path regression when LLVM does not inline them.
3#![allow(clippy::inline_always)]
4
5/// A zero-allocation recognizer for a known canonical JSON layout.
6///
7/// This is not a general JSON parser. It is an optimization primitive for
8/// generated or protocol-specific code that first attempts one exact layout
9/// and falls back to [`crate::JsonCursor`] when any expectation does not match.
10/// Every successful recognizer must consume and check the complete input.
11#[derive(Clone, Copy, Debug)]
12pub struct CanonicalScanner<'a> {
13    remaining: &'a str,
14}
15
16/// A zero-allocation canonical-layout recognizer over raw bytes.
17///
18/// Unlike converting the complete input with [`std::str::from_utf8`] first,
19/// this scanner validates only captured string fields. Structural fragments
20/// are matched as ASCII bytes. A successful recognizer still proves that the
21/// complete accepted layout is valid UTF-8 JSON because every byte is either
22/// an exact ASCII literal, an ASCII number/boolean, or part of a validated
23/// string.
24#[derive(Clone, Copy, Debug)]
25pub struct CanonicalBytesScanner<'a> {
26    remaining: &'a [u8],
27}
28
29impl<'a> CanonicalScanner<'a> {
30    /// Starts matching an already UTF-8-validated string.
31    #[must_use]
32    #[inline(always)]
33    pub const fn new(input: &'a str) -> Self {
34        Self { remaining: input }
35    }
36
37    /// Consumes an exact string fragment.
38    ///
39    /// Returns `None` without advancing when the sequence does not match.
40    #[inline(always)]
41    pub fn literal(&mut self, expected: &str) -> Option<()> {
42        self.remaining = self.remaining.strip_prefix(expected)?;
43        Some(())
44    }
45
46    /// Consumes a JSON string with no escapes and returns its borrowed value.
47    ///
48    /// Escaped strings deliberately return `None` so the caller can use the
49    /// general parser instead.
50    #[inline(always)]
51    pub fn plain_string(&mut self) -> Option<&'a str> {
52        let input = self.remaining.strip_prefix('"')?;
53        let end = memchr::memchr(b'"', input.as_bytes())?;
54        let value = &input[..end];
55        if value
56            .as_bytes()
57            .iter()
58            .any(|byte| *byte == b'\\' || *byte < 0x20)
59        {
60            return None;
61        }
62        self.remaining = &input[end + 1..];
63        Some(value)
64    }
65
66    /// Consumes one unsigned JSON integer.
67    #[inline(always)]
68    pub fn unsigned(&mut self) -> Option<u64> {
69        let bytes = self.remaining.as_bytes();
70        let first = *bytes.first()?;
71        if !first.is_ascii_digit() {
72            return None;
73        }
74        if first == b'0' && bytes.get(1).is_some_and(u8::is_ascii_digit) {
75            return None;
76        }
77
78        let mut value = 0_u64;
79        let mut length = 0;
80        for &digit in bytes {
81            if !digit.is_ascii_digit() {
82                break;
83            }
84            value = value
85                .checked_mul(10)?
86                .checked_add(u64::from(digit - b'0'))?;
87            length += 1;
88        }
89        self.remaining = &self.remaining[length..];
90        Some(value)
91    }
92
93    /// Consumes `true` or `false`.
94    #[inline(always)]
95    pub fn boolean(&mut self) -> Option<bool> {
96        if let Some(remaining) = self.remaining.strip_prefix("true") {
97            self.remaining = remaining;
98            Some(true)
99        } else if let Some(remaining) = self.remaining.strip_prefix("false") {
100            self.remaining = remaining;
101            Some(false)
102        } else {
103            None
104        }
105    }
106
107    /// Returns the unmatched suffix.
108    #[must_use]
109    #[inline(always)]
110    pub const fn remaining(&self) -> &'a str {
111        self.remaining
112    }
113
114    /// Returns true only when the recognizer consumed the complete input.
115    #[must_use]
116    #[inline(always)]
117    pub const fn is_finished(&self) -> bool {
118        self.remaining.is_empty()
119    }
120}
121
122impl<'a> CanonicalBytesScanner<'a> {
123    /// Starts matching a byte slice without a full-input UTF-8 pre-pass.
124    #[must_use]
125    #[inline(always)]
126    pub const fn new(input: &'a [u8]) -> Self {
127        Self { remaining: input }
128    }
129
130    /// Consumes an exact ASCII JSON fragment.
131    #[inline(always)]
132    pub fn literal(&mut self, expected: &str) -> Option<()> {
133        self.remaining = self.remaining.strip_prefix(expected.as_bytes())?;
134        Some(())
135    }
136
137    /// Consumes a JSON string with no escapes and validates only its payload.
138    #[inline(always)]
139    pub fn plain_string(&mut self) -> Option<&'a str> {
140        let input = self.remaining.strip_prefix(b"\"")?;
141        let end = memchr::memchr(b'"', input)?;
142        let value = &input[..end];
143        if value.iter().any(|byte| *byte == b'\\' || *byte < 0x20) {
144            return None;
145        }
146        let value = std::str::from_utf8(value).ok()?;
147        self.remaining = &input[end + 1..];
148        Some(value)
149    }
150
151    /// Consumes an unescaped ASCII JSON string and returns its bytes.
152    ///
153    /// This is the fastest byte-input path for protocol identifiers and other
154    /// fields whose canonical form is ASCII. Non-ASCII input returns `None` so
155    /// the caller can fall back to [`Self::plain_string`] or a general parser.
156    #[inline(always)]
157    pub fn plain_ascii_string(&mut self) -> Option<&'a [u8]> {
158        let input = self.remaining.strip_prefix(b"\"")?;
159        let end = memchr::memchr(b'"', input)?;
160        let value = &input[..end];
161        if value
162            .iter()
163            .any(|byte| !byte.is_ascii() || *byte == b'\\' || *byte < 0x20)
164        {
165            return None;
166        }
167        self.remaining = &input[end + 1..];
168        Some(value)
169    }
170
171    /// Consumes one unsigned JSON integer.
172    #[inline(always)]
173    pub fn unsigned(&mut self) -> Option<u64> {
174        let first = *self.remaining.first()?;
175        if !first.is_ascii_digit() {
176            return None;
177        }
178        if first == b'0' && self.remaining.get(1).is_some_and(u8::is_ascii_digit) {
179            return None;
180        }
181
182        let mut value = 0_u64;
183        let mut length = 0;
184        for &digit in self.remaining {
185            if !digit.is_ascii_digit() {
186                break;
187            }
188            value = value
189                .checked_mul(10)?
190                .checked_add(u64::from(digit - b'0'))?;
191            length += 1;
192        }
193        self.remaining = &self.remaining[length..];
194        Some(value)
195    }
196
197    /// Consumes `true` or `false`.
198    #[inline(always)]
199    pub fn boolean(&mut self) -> Option<bool> {
200        if let Some(remaining) = self.remaining.strip_prefix(b"true") {
201            self.remaining = remaining;
202            Some(true)
203        } else if let Some(remaining) = self.remaining.strip_prefix(b"false") {
204            self.remaining = remaining;
205            Some(false)
206        } else {
207            None
208        }
209    }
210
211    /// Returns the unmatched suffix.
212    #[must_use]
213    #[inline(always)]
214    pub const fn remaining(&self) -> &'a [u8] {
215        self.remaining
216    }
217
218    /// Returns true only when the recognizer consumed the complete input.
219    #[must_use]
220    #[inline(always)]
221    pub const fn is_finished(&self) -> bool {
222        self.remaining.is_empty()
223    }
224}