1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
//! A cheap depth pre-check for untrusted HJSON.
//!
//! `serde_hjson`'s parser (and our comment-preserving config walker) recurse
//! once per object/array nesting level. Deeply-nested input therefore overflows
//! the native stack into an **uncatchable `SIGABRT`** — `std::panic::catch_unwind`
//! does not catch a stack overflow, so the graceful config-load fallbacks cannot
//! save it and the process dies (at startup, if it's `inkhaven.hjson`).
//!
//! Scanning the brace/bracket depth first — before handing the string to the
//! recursive parser — turns that hard crash into a clean, recoverable error.
/// Maximum object/array nesting depth accepted from an HJSON document. Real
/// config / world / lexicon files nest a handful of levels; 128 is far above any
/// legitimate use and far below the stack-overflow threshold.
pub const MAX_HJSON_DEPTH: usize = 128;
/// Return an error if `s` nests objects/arrays deeper than [`MAX_HJSON_DEPTH`].
///
/// A single cheap pass that skips braces inside quoted strings and `#` / `//` /
/// `/* */` comments. It is deliberately conservative: a stray unbalanced brace
/// inside a *quoteless* string is counted (leaning toward a false reject rather
/// than under-protecting), but the 128-level headroom absorbs that for any real
/// document — while a genuinely deep `{{{…}}}` payload is caught before it can
/// reach the recursive parser.
pub fn check_hjson_depth(s: &str) -> Result<(), String> {
let b = s.as_bytes();
let mut depth: usize = 0;
let mut in_str: Option<u8> = None; // Some(quote byte) while inside a "…" / '…'
let mut i = 0;
while i < b.len() {
let c = b[i];
if let Some(q) = in_str {
if c == b'\\' {
i += 2; // skip the escaped char
continue;
}
if c == q {
in_str = None;
}
i += 1;
continue;
}
match c {
b'"' | b'\'' => in_str = Some(c),
b'#' => {
while i < b.len() && b[i] != b'\n' {
i += 1;
}
continue;
}
b'/' if i + 1 < b.len() && b[i + 1] == b'/' => {
while i < b.len() && b[i] != b'\n' {
i += 1;
}
continue;
}
b'/' if i + 1 < b.len() && b[i + 1] == b'*' => {
i += 2;
while i + 1 < b.len() && !(b[i] == b'*' && b[i + 1] == b'/') {
i += 1;
}
i += 2;
continue;
}
b'{' | b'[' => {
depth += 1;
if depth > MAX_HJSON_DEPTH {
return Err(format!(
"HJSON nests deeper than {MAX_HJSON_DEPTH} levels — refusing to \
parse (stack-overflow guard)"
));
}
}
b'}' | b']' => depth = depth.saturating_sub(1),
_ => {}
}
i += 1;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accepts_ordinary_nesting() {
assert!(check_hjson_depth("{ a: { b: [ { c: 1 } ] } }").is_ok());
assert!(check_hjson_depth("{}").is_ok());
// braces inside a quoted string don't count
assert!(check_hjson_depth(r#"{ url: "http://x/{{{{{{" }"#).is_ok());
// braces inside a comment don't count
assert!(check_hjson_depth("{ a: 1 } # {{{{{{{{{").is_ok());
}
#[test]
fn rejects_pathological_nesting() {
let deep = "{".repeat(MAX_HJSON_DEPTH + 5);
assert!(check_hjson_depth(&deep).is_err());
}
#[test]
fn boundary_is_inclusive() {
assert!(check_hjson_depth(&"[".repeat(MAX_HJSON_DEPTH)).is_ok());
assert!(check_hjson_depth(&"[".repeat(MAX_HJSON_DEPTH + 1)).is_err());
}
}