1
//! Regression test: the struct type lookahead must stay linear on malformed strings.
2
//!
3
//! `Parser::check_struct_type` scans ahead to tell a newtype from a tuple. It
4
//! used to backtrack on any string error except a missing closing quote, so a
5
//! string like `"\"\"\"...\q"` was re-parsed from every escaped `"` up to the
6
//! invalid escape at its end — quadratic.
7
//!
8
//! The assertion is on *scaling*, not on absolute time, so it stays meaningful on
9
//! slow/noisy CI machines: doubling the input should roughly double the work
10
//! (ratio ~2). Before the fix the ratio is ~4. The threshold sits far from both.
11

            
12
use std::time::Instant;
13

            
14
use ron::error::{Error, SpannedError};
15
use serde::Deserialize;
16

            
17
#[derive(Debug, Deserialize)]
18
#[serde(untagged)]
19
enum Untagged {
20
    A(#[allow(dead_code)] ron::Value),
21
}
22

            
23
32
fn malformed_string_in_variant(prefix: &str, n: usize) -> String {
24
32
    let mut s = format!("A({}\"", prefix);
25
99216
    for _ in 0..n {
26
99216
        s.push_str("\\\"");
27
99216
    }
28
32
    s.push_str("\\q\")");
29
32
    s
30
32
}
31

            
32
24
fn parse_millis(prefix: &str, n: usize) -> f64 {
33
24
    let src = malformed_string_in_variant(prefix, n);
34
24
    let start = Instant::now();
35
24
    let _err = ron::from_str::<Untagged>(&src).unwrap_err();
36
24
    let elapsed = start.elapsed().as_secs_f64() * 1000.0;
37
24
    elapsed
38
24
}
39

            
40
8
fn assert_linear(prefix: &str) {
41
    const N: usize = 4_000;
42

            
43
    // Warm up so allocator/caches don't skew the first sample.
44
8
    parse_millis(prefix, N / 10);
45

            
46
8
    let base = parse_millis(prefix, N);
47
8
    let double = parse_millis(prefix, N * 2);
48

            
49
    // Floor the denominator so timer noise on a fast linear parse (a few
50
    // milliseconds at most) can't inflate the ratio. The quadratic parse takes
51
    // seconds, so the floor doesn't hide it.
52
8
    let ratio = double / base.max(10.0);
53
8
    assert!(
54
8
        ratio < 3.0,
55
        "a malformed {}string with {} escapes took {:.1}ms but {} escapes took {:.1}ms \
56
         (ratio {:.2}); expected ~2 (linear), ~4 means the quadratic rescan is back",
57
        prefix,
58
        N,
59
        base,
60
        N * 2,
61
        double,
62
        ratio
63
    );
64
8
}
65

            
66
#[test]
67
4
fn malformed_string_lookahead_scales_linearly() {
68
4
    assert_linear("");
69
4
}
70

            
71
#[test]
72
4
fn malformed_byte_string_lookahead_scales_linearly() {
73
4
    assert_linear("b");
74
4
}
75

            
76
#[test]
77
4
fn malformed_string_lookahead_reports_escape_error() {
78
8
    for prefix in ["", "b"] {
79
8
        assert!(matches!(
80
8
            ron::from_str::<Untagged>(&malformed_string_in_variant(prefix, 2)),
81
            Err(SpannedError {
82
8
                code: Error::InvalidEscape("Unknown escape character"),
83
                ..
84
            })
85
        ));
86
    }
87

            
88
    // raw identifiers and identifiers starting with `r` or `b` still backtrack
89
4
    assert!(ron::from_str::<Untagged>("A(r#rust, br, bar)").is_ok());
90
4
    assert!(ron::from_str::<Untagged>("A(\"a\\\"b\", b\"c\", r#\"d\"#)").is_ok());
91
4
}