Skip to main content

ytsaurus_yson/
scan.rs

1//! Finding record boundaries without decoding.
2//!
3//! A YTsaurus job reads a *list fragment* — `value; value; value;` — from a pipe,
4//! and that stream can be far larger than memory. To consume it incrementally you
5//! need to answer one question about a partially-filled buffer: **where does the
6//! next complete value end, and is there one at all?**
7//!
8//! [`scan_value`] answers exactly that. It walks the token stream without
9//! allocating or building any value, and reports either the length of the first
10//! complete value or that more bytes are needed. Truncation is reported as
11//! [`Scan::Incomplete`] rather than as an error, which is what makes it safe to
12//! call on a buffer that stops in the middle of a record.
13
14use crate::{error::YsonError, lexer::YsonIterator, node::Token, ser::YsonFormat};
15
16/// Nesting depth beyond which scanning gives up.
17///
18/// Matches the deserializer's limit, so anything this accepts can also be
19/// decoded, and a hostile stream cannot drive the scanner into deep recursion.
20const MAX_DEPTH: usize = 128;
21
22/// Outcome of scanning for one complete value.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum Scan {
25    /// A complete value occupies the first `len` bytes of the input.
26    Complete {
27        /// Length of the value in bytes, from the start of the input.
28        len: usize,
29    },
30    /// The input ends in the middle of a value; supply more bytes and retry.
31    ///
32    /// An empty input is also `Incomplete`: whether that means "end of stream"
33    /// or "not read enough yet" is for the caller to decide.
34    Incomplete,
35}
36
37/// Returns the length of the first complete YSON value in `input`.
38///
39/// Leading whitespace and comments (text format only) are counted as part of the
40/// value's extent, so `&input[..len]` can be handed straight to
41/// [`crate::from_slice`]. A leading item separator is *not* consumed — strip it
42/// before calling.
43///
44/// # Errors
45///
46/// Returns [`YsonError`] if the input is malformed in a way that more data
47/// cannot fix, such as an invalid marker or a mismatched bracket. Running out of
48/// bytes is never an error; it is [`Scan::Incomplete`].
49///
50/// # Examples
51///
52/// ```
53/// use ytsaurus_yson::{YsonFormat, scan::{Scan, scan_value}};
54///
55/// // Two records; only the first is measured.
56/// let input = b"{a=1};{b=2}";
57/// assert_eq!(scan_value(input, YsonFormat::Text)?, Scan::Complete { len: 5 });
58///
59/// // A record cut short asks for more bytes instead of failing.
60/// assert_eq!(scan_value(b"{a=", YsonFormat::Text)?, Scan::Incomplete);
61/// # Ok::<(), ytsaurus_yson::YsonError>(())
62/// ```
63pub fn scan_value(input: &[u8], format: YsonFormat) -> Result<Scan, YsonError> {
64    let mut lexer = YsonIterator::new(input, matches!(format, YsonFormat::Binary));
65
66    match scan_tree(&mut lexer, 0) {
67        Ok(()) => Ok(Scan::Complete { len: lexer.pos() }),
68        // Running past the end of the buffer means "not enough bytes yet", not
69        // "broken". The caller distinguishes the two by knowing whether the
70        // underlying stream is exhausted.
71        Err(YsonError::Eof | YsonError::UnexpectedEof(_)) => Ok(Scan::Incomplete),
72        Err(e) => Err(e),
73    }
74}
75
76/// `<tree> = [ <attributes> ], <object>`
77fn scan_tree(lexer: &mut YsonIterator<'_>, depth: usize) -> Result<(), YsonError> {
78    if depth > MAX_DEPTH {
79        return Err(YsonError::Custom("Recursion limit exceeded".into()));
80    }
81
82    let mut token = lexer.next_token()?;
83
84    if matches!(token, Token::BeginAttributes) {
85        scan_fragment(lexer, depth + 1, Token::EndAttributes)?;
86        token = lexer.next_token()?;
87    }
88
89    scan_object(lexer, depth, token)
90}
91
92fn scan_object(
93    lexer: &mut YsonIterator<'_>,
94    depth: usize,
95    token: Token<'_>,
96) -> Result<(), YsonError> {
97    match token {
98        Token::String(_)
99        | Token::Int64(_)
100        | Token::Uint64(_)
101        | Token::Double(_)
102        | Token::Boolean(_)
103        | Token::Entity => Ok(()),
104
105        Token::BeginList => scan_list(lexer, depth + 1),
106        Token::BeginMap => scan_fragment(lexer, depth + 1, Token::EndMap),
107
108        other => Err(YsonError::UnexpectedToken {
109            expected: "a YSON value",
110            found: format!("{other:?}"),
111            pos: lexer.pos(),
112        }),
113    }
114}
115
116/// `<list-fragment> = { <list-item>, ";" }, [ <list-item> ]` up to `]`.
117fn scan_list(lexer: &mut YsonIterator<'_>, depth: usize) -> Result<(), YsonError> {
118    if depth > MAX_DEPTH {
119        return Err(YsonError::Custom("Recursion limit exceeded".into()));
120    }
121
122    loop {
123        match lexer.peek_byte()? {
124            b']' => {
125                lexer.next_token()?;
126                return Ok(());
127            }
128            b';' => {
129                lexer.next_token()?;
130            }
131            _ => scan_tree(lexer, depth)?,
132        }
133    }
134}
135
136/// `<map-fragment> = { <key-value-pair>, ";" }, [ <key-value-pair> ]` up to
137/// `end` (`}` for a map, `>` for an attribute block).
138fn scan_fragment(
139    lexer: &mut YsonIterator<'_>,
140    depth: usize,
141    end: Token<'static>,
142) -> Result<(), YsonError> {
143    if depth > MAX_DEPTH {
144        return Err(YsonError::Custom("Recursion limit exceeded".into()));
145    }
146
147    let end_byte = match end {
148        Token::EndMap => b'}',
149        Token::EndAttributes => b'>',
150        _ => unreachable!("scan_fragment is only called for maps and attributes"),
151    };
152
153    loop {
154        let peeked = lexer.peek_byte()?;
155        if peeked == end_byte {
156            lexer.next_token()?;
157            return Ok(());
158        }
159        if peeked == b';' {
160            lexer.next_token()?;
161            continue;
162        }
163
164        // <key-value-pair> = <string>, "=", <tree>
165        match lexer.next_token()? {
166            Token::String(_) => {}
167            other => {
168                return Err(YsonError::UnexpectedToken {
169                    expected: "a map key",
170                    found: format!("{other:?}"),
171                    pos: lexer.pos(),
172                });
173            }
174        }
175        match lexer.next_token()? {
176            Token::KeyValueSeparator => {}
177            other => {
178                return Err(YsonError::UnexpectedToken {
179                    expected: "'='",
180                    found: format!("{other:?}"),
181                    pos: lexer.pos(),
182                });
183            }
184        }
185        scan_tree(lexer, depth)?;
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    fn complete(input: &[u8], format: YsonFormat) -> usize {
194        match scan_value(input, format).expect("scan must not fail") {
195            Scan::Complete { len } => len,
196            Scan::Incomplete => panic!("expected a complete value in {input:?}"),
197        }
198    }
199
200    #[test]
201    fn scans_text_scalars() {
202        assert_eq!(complete(b"42", YsonFormat::Text), 2);
203        assert_eq!(complete(b"42;43", YsonFormat::Text), 2);
204        assert_eq!(complete(b"#", YsonFormat::Text), 1);
205        assert_eq!(complete(b"%true", YsonFormat::Text), 5);
206        assert_eq!(complete(br#""a;b""#, YsonFormat::Text), 5);
207    }
208
209    #[test]
210    fn scans_text_composites() {
211        assert_eq!(complete(b"{a=1}", YsonFormat::Text), 5);
212        assert_eq!(complete(b"{a=1};{b=2}", YsonFormat::Text), 5);
213        assert_eq!(complete(b"[1;2;3]", YsonFormat::Text), 7);
214        assert_eq!(complete(b"{a={b=[1;2]}}", YsonFormat::Text), 13);
215        assert_eq!(complete(b"{a=1;}", YsonFormat::Text), 6);
216    }
217
218    #[test]
219    fn scans_attributed_values() {
220        assert_eq!(complete(b"<a=1>#", YsonFormat::Text), 6);
221        assert_eq!(complete(b"<a=1>#;{b=2}", YsonFormat::Text), 6);
222        assert_eq!(complete(b"<a=1;b=2>[1]", YsonFormat::Text), 12);
223        assert_eq!(complete(b"<a=<b=1>#>#", YsonFormat::Text), 11);
224    }
225
226    #[test]
227    fn reports_truncation_as_incomplete() {
228        for input in [
229            b"{a=1".as_slice(),
230            b"{a=",
231            b"{",
232            b"[1;2",
233            b"<a=1>",
234            b"<a=1",
235            b"",
236            b"\"unterminated",
237        ] {
238            assert_eq!(
239                scan_value(input, YsonFormat::Text).expect("no error"),
240                Scan::Incomplete,
241                "input {:?}",
242                String::from_utf8_lossy(input)
243            );
244        }
245    }
246
247    #[test]
248    fn scans_binary_values() {
249        // 0x02 int64, zigzag(1) = 2
250        assert_eq!(complete(&[0x02, 0x02], YsonFormat::Binary), 2);
251        // 0x01 string, zigzag(3) = 6, "abc"
252        assert_eq!(complete(b"\x01\x06abc", YsonFormat::Binary), 5);
253        // A string whose bytes contain YSON punctuation must not confuse the scan.
254        assert_eq!(complete(b"\x01\x06};]", YsonFormat::Binary), 5);
255        // 0x03 double + 8 bytes
256        assert_eq!(
257            complete(&[0x03, 0, 0, 0, 0, 0, 0, 0, 0], YsonFormat::Binary),
258            9
259        );
260    }
261
262    #[test]
263    fn scans_a_binary_map_and_stops_at_the_boundary() {
264        // {a=1};{a=1}
265        let one = b"{\x01\x02a=\x02\x02}";
266        let mut two = one.to_vec();
267        two.push(b';');
268        two.extend_from_slice(one);
269
270        assert_eq!(complete(one, YsonFormat::Binary), one.len());
271        assert_eq!(complete(&two, YsonFormat::Binary), one.len());
272    }
273
274    #[test]
275    fn binary_truncation_is_incomplete() {
276        let full = b"{\x01\x02a=\x02\x02}";
277        for cut in 0..full.len() {
278            assert_eq!(
279                scan_value(&full[..cut], YsonFormat::Binary).expect("no error"),
280                Scan::Incomplete,
281                "cut at {cut}"
282            );
283        }
284        // A string header promising more bytes than are present.
285        assert_eq!(
286            scan_value(b"\x01\x14abc", YsonFormat::Binary).expect("no error"),
287            Scan::Incomplete
288        );
289    }
290
291    #[test]
292    fn rejects_malformed_input() {
293        // Undefined marker.
294        assert!(scan_value(&[0x07], YsonFormat::Binary).is_err());
295        // Map with no `=`.
296        assert!(scan_value(b"{a 1}", YsonFormat::Text).is_err());
297        // Closing bracket with nothing open.
298        assert!(scan_value(b"]", YsonFormat::Text).is_err());
299    }
300
301    #[test]
302    fn rejects_deep_nesting() {
303        let deep = vec![b'['; MAX_DEPTH + 10];
304        assert!(scan_value(&deep, YsonFormat::Text).is_err());
305    }
306
307    #[test]
308    fn text_comments_count_toward_the_value() {
309        // Leading trivia is included so the slice can be re-parsed as-is.
310        assert_eq!(complete(b"/* c */42", YsonFormat::Text), 9);
311        assert_eq!(complete(b"  42", YsonFormat::Text), 4);
312    }
313}