Skip to main content

ikigai_script/
limits.rs

1// COPIED from ikigai-store `src/limits.rs` at commit 1cf3fa0 (ikigai-store 0.2.7; the file is
2// unchanged since 6abf030, ikigai-rs/ikigai-store PR #16, ledger #915), for ledger #954, and kept
3// byte-for-byte except where marked `COPY:` below, so a later shared limits crate can replace
4// this copy and ikigai-sparql's (ikigai-linkeddata, ledger #962) mechanically. ikigai-script does
5// not depend on ikigai-store for this: the store links an evaluator (oxigraph's spareval and
6// sparopt), and this crate's whole point is that it never evaluates a query in-process. Diff
7// the copies before editing any of them.
8
9//! Bounds on caller-supplied SPARQL, checked before the parser sees a byte (ledger #915).
10//!
11//! oxigraph's SPARQL parser (`spargebra`, a `peg` grammar) and its evaluator (`sparopt`,
12//! `spareval`) are **recursive**, so a query's shape is a claim on the stack of whatever
13//! thread parses it. Running out is not an error a caller gets back: Rust aborts the
14//! **whole process** on a stack overflow, on any thread. `SELECT * WHERE { FILTER(((…1…))) }`
15//! with ~1000 parentheses took down a host through its HTTP door, and the anonymous grant
16//! held the read authority that reached the parse.
17//!
18//! Two layers, because neither is enough alone:
19//!
20//! 1. **[`check_sparql`] refuses, never truncates.** A query or update over
21//!    [`MAX_SPARQL_BYTES`], or that nests deeper than [`MAX_SPARQL_NESTING`] — brackets,
22//!    and runs of the one prefix operator spargebra recurses on, `!` — is refused with a
23//!    typed `InvalidArgument` naming the bound, before anything is parsed. Those are the
24//!    cheapest ways to recurse (ONE byte a level; ~2.3 KiB of stack a level for a bracket
25//!    in a release build), so this is the check that turns a 3 KB query that killed a host
26//!    into a refusal.
27//! 2. **[`on_sparql_stack`] runs the parse AND the evaluation on a thread whose stack is
28//!    sized from the query's length.** Nesting is not the only recursion: measured on a
29//!    2 MiB thread in a release build, 1,879 triple patterns, 2,087 `FILTER`s, a 2,086-way
30//!    `||` or 3,283 `+` terms all overflow — and those are shapes a generated query can
31//!    legitimately have, so no lexical bound can refuse them without refusing real work.
32//!    What bounds them is the byte bound times the stack each byte can cost, and the thread
33//!    is where that budget lives: [`STACK_PER_BYTE`] per byte above a [`STACK_BASE`] floor.
34//!    A thread's stack is reserved address space, committed only as it is touched, so an
35//!    ordinary query costs the spawn and nothing else.
36//!
37//! ⚠ **The second layer is a RELEASE-build guarantee.** A debug build spends ~20–50× the
38//! stack per level (a 2 MiB thread holds 47 `+` terms when evaluating, unoptimized), so a
39//! debug host can still be overflowed by a long enough operator chain inside the byte bound.
40//! The bracket bound holds in both: a debug build at [`MAX_SPARQL_NESTING`] of the costliest
41//! bracket (`STR(` calls, ~60 KiB a level) needs ~4 MiB, well inside [`STACK_BASE`].
42//!
43//! ⚠ **And on wasm there is no thread**: [`on_sparql_stack`] runs inline there, so only the
44//! first layer applies, on whatever stack the host gave the module.
45//!
46//! ⚠ **Neither layer bounds TIME.** The evaluator is quadratic in an operator chain's
47//! length: 40,000 `||1` terms (120 KB) take ~30 s of a core in a release build and 80,000
48//! more than two minutes, and long property paths, long BGPs and deeply nested collections
49//! are slow well before they are deep. This module is about the stack — an abort takes
50//! down every request at once — and leaves a query budget to the host.
51//!
52//! # The scan is exact about what it skips, and that took an automaton
53//!
54//! The obvious scan — count `(`, `{`, `[`, skipping strings, `<IRI>`s and `#` comments —
55//! is **wrong in a way an attacker can use**, because SPARQL's lexing of `<` depends on
56//! where the parser is: in a triple pattern `<…>` is an IRI, but in an expression `?a<…`
57//! is a less-than. spargebra accepts `<` + anything-but-`>` + `>` as an IRI only where an
58//! IRI is allowed and the text inside is a valid IRI; elsewhere it is an operator, and the
59//! "IRI" is code. So `( ?a<'> ) ' && ( ?a<'> ) ' && (…` reads to a skip-the-IRI scan as one
60//! level, while the parser sees `(?a < "> ) " && (?a < "> ) " && (…` — a string it opens
61//! INSIDE what the scan skipped — and nests once per repetition. A `#` inside such a token
62//! does the same with a comment.
63//!
64//! So the scan follows **every reading** the grammar allows, as a small set of branches, and
65//! refuses on the deepest. At a `<` it forks: an IRI (when the text up to the next `>` could
66//! be one), a `<<` triple-term opener (when one follows), and a less-than — the last only
67//! when the innermost open bracket is `(`, because every SPARQL expression sits directly
68//! inside parentheses, so that is the only place a less-than can occur. Branches in the
69//! same lexical state with the same open brackets are merged, so an ordinary query runs as
70//! one branch and the whole scan stays linear. A closer pops only the opener it matches, so
71//! no reading can be made to look shallower than it is; and a query that keeps more than
72//! [`MAX_SCAN_BRANCHES`] readings alive at once is refused as too ambiguous to bound.
73//!
74//! ⚠ One more reading lives OUTSIDE the grammar: spargebra's `standard-unicode-escaping`
75//! feature rewrites `\uXXXX` and `\UXXXXXXXX` anywhere in the text **before** tokenizing,
76//! so `\u0028` is a `(`. oxigraph does not enable it, but feature unification is global and
77//! a sibling could; when the text contains such an escape, the scan also runs over the
78//! unescaped text and refuses on either.
79
80use ikigai_core::{Error, Result};
81
82/// The most bytes a SPARQL query or update may carry: **1 MiB**.
83///
84/// Generous on purpose. The largest legitimate queries in the ecosystem are FLAT — a
85/// `VALUES` list of every open item in a ledger is ~51 bytes an item, so this holds about
86/// 20,000 of them, and `VALUES` and `INSERT DATA` recurse not at all (measured to 40,000
87/// entries on a 2 MiB thread). What this bounds is the stack [`on_sparql_stack`] may need.
88pub const MAX_SPARQL_BYTES: usize = 1 << 20;
89
90/// The deepest a SPARQL query or update may nest `(`, `{`, `[` and `<<` — with a run of `!`
91/// counting one level for each — **64**.
92///
93/// The deepest legitimate query in the ecosystem's tests and stored queries nests about 5
94/// (a scan of ~700 of them, 2026-10-09). On a 2 MiB thread oxigraph 0.5.11's parser
95/// overflows at ~180 levels of `(`/`{` in a debug build and ~850 in release, and at 34
96/// (debug) or 426 (release) levels of function calls, the costliest bracket per level. 64 is
97/// an order of magnitude above real use, and the parse runs on [`on_sparql_stack`], where a
98/// debug build at this bound needs ~4 MiB of the [`STACK_BASE`] it gets.
99pub const MAX_SPARQL_NESTING: usize = 64;
100
101/// How many simultaneous readings the scan follows before refusing the text as too
102/// ambiguous to bound. An ordinary query keeps one; it takes a `<` inside parentheses
103/// followed by text that could be an IRI containing `'`, `#` or a bracket to make two.
104pub const MAX_SCAN_BRANCHES: usize = 64;
105
106/// The stack every SPARQL thread gets before the per-byte share: 16 MiB.
107pub const STACK_BASE: usize = 16 << 20;
108
109/// The stack reserved per byte of query, above [`STACK_BASE`]: 512 bytes.
110///
111/// The worst cost measured per byte in a release build, for the shapes the nesting bound
112/// does not refuse, is ~370 (a property path of one-letter prefixed names, `:a/:a/…`), then
113/// ~335 (`1||1||…`) and ~320 (`1+1+…`); ~260 for a `UNION` chain. 512 covers the worst with
114/// margin, so a query at [`MAX_SPARQL_BYTES`] gets a 528 MiB thread — reserved, and touched
115/// only as deep as the query actually recurses. (A run of `!` would cost ~650 a byte at
116/// scale, which is why it counts as nesting instead.) `tests/sparql_stack_measure.rs`
117/// re-measures all of these.
118pub const STACK_PER_BYTE: usize = 512;
119
120/// Refuse a SPARQL query or update that exceeds [`MAX_SPARQL_BYTES`] or nests deeper than
121/// [`MAX_SPARQL_NESTING`], naming the argument `arg` and the bound. Never truncates.
122///
123/// Every door in this crate that parses caller text calls this first. It is public so a
124/// host with its own SPARQL face over oxigraph can apply the same bound; that host must
125/// still parse on a big enough stack (see [`on_sparql_stack`]).
126///
127/// ```
128/// // COPY: this crate's path.
129/// use ikigai_script::limits::{check_sparql, MAX_SPARQL_NESTING};
130///
131/// assert!(check_sparql("SELECT * WHERE { ?s ?p ?o FILTER((1)) }", "query").is_ok());
132/// let deep = format!("SELECT * WHERE {{ FILTER({}1{}) }}", "(".repeat(100), ")".repeat(100));
133/// let refusal = check_sparql(&deep, "query").unwrap_err().to_string();
134/// assert!(refusal.contains("`query`") && refusal.contains(&MAX_SPARQL_NESTING.to_string()));
135/// // Brackets in a string, an IRI or a comment are not nesting.
136/// let quoted = format!("SELECT * WHERE {{ ?s ?p \"{}\" }} # {}", "(".repeat(100), "[".repeat(100));
137/// assert!(check_sparql(&quoted, "query").is_ok());
138/// ```
139pub fn check_sparql(text: &str, arg: &str) -> Result<()> {
140    if text.len() > MAX_SPARQL_BYTES {
141        return Err(Error::InvalidArgument {
142            name: arg.to_string(),
143            detail: format!(
144                "this SPARQL text is {} bytes, and this store refuses any over {MAX_SPARQL_BYTES} \
145                 (MAX_SPARQL_BYTES) before parsing it: the parser and evaluator are recursive, \
146                 and the bound is what keeps the stack a query can claim finite. Split the \
147                 work into several requests",
148                text.len()
149            ),
150        });
151    }
152    let depth = deepest(text.as_bytes()).map_err(|ambiguous| refuse_ambiguous(arg, ambiguous))?;
153    let depth = match unescaped(text) {
154        Some(unescaped) => depth.max(
155            deepest(unescaped.as_bytes()).map_err(|ambiguous| refuse_ambiguous(arg, ambiguous))?,
156        ),
157        None => depth,
158    };
159    if depth > MAX_SPARQL_NESTING {
160        return Err(Error::InvalidArgument {
161            name: arg.to_string(),
162            detail: format!(
163                "this SPARQL text nests its brackets deeper than {MAX_SPARQL_NESTING} \
164                 (MAX_SPARQL_NESTING), counting `(`, `{{`, `[`, `<<` and each `!` of a run, \
165                 outside strings, IRIs and comments. It is refused before parsing because the parser recurses once \
166                 per level and a stack overflow aborts the whole host, not one request. Real \
167                 queries nest well under 10; flatten this one"
168            ),
169        });
170    }
171    Ok(())
172}
173
174fn refuse_ambiguous(arg: &str, readings: usize) -> Error {
175    Error::InvalidArgument {
176        name: arg.to_string(),
177        detail: format!(
178            "this SPARQL text can be read more than {readings} ways at once (MAX_SCAN_BRANCHES): \
179             a `<` inside parentheses is either an IRI or a less-than, and this text keeps too \
180             many of those readings open for its nesting to be bounded before parsing. Put a \
181             space after a less-than `<`, or write IRIs containing `'`, `#` or brackets with a \
182             PREFIX"
183        ),
184    }
185}
186
187/// Run `work` — a parse and everything that evaluates what it parsed — on a thread whose
188/// stack is sized for `text` (see [`STACK_BASE`] and [`STACK_PER_BYTE`]), and wait for it.
189///
190/// A panic on that thread is resumed on this one, so the caller sees what it would have
191/// seen inline. A thread that cannot be spawned (the address space or the platform refused
192/// a stack this size) is a transient [`Error::Unavailable`], never a fallback to running
193/// inline — inline is the overflow this exists to prevent. On wasm there are no threads,
194/// and `work` runs inline.
195pub fn on_sparql_stack<T, F>(text: &str, work: F) -> Result<T>
196where
197    T: Send,
198    F: FnOnce() -> Result<T> + Send,
199{
200    #[cfg(not(target_family = "wasm"))]
201    {
202        let stack = STACK_BASE.saturating_add(text.len().saturating_mul(STACK_PER_BYTE));
203        std::thread::scope(|scope| {
204            let handle = std::thread::Builder::new()
205                // COPY: named for this crate, so a crash report names the right one.
206                .name("ikigai-script-sparql".to_string())
207                .stack_size(stack)
208                .spawn_scoped(scope, work)
209                .map_err(|e| {
210                    Error::Unavailable(format!(
211                        "could not start a {} MiB thread to parse this SPARQL text on: {e}",
212                        stack >> 20
213                    ))
214                })?;
215            match handle.join() {
216                Ok(result) => result,
217                Err(panic) => std::panic::resume_unwind(panic),
218            }
219        })
220    }
221    #[cfg(target_family = "wasm")]
222    {
223        let _ = text;
224        work()
225    }
226}
227
228/// The text with spargebra's `standard-unicode-escaping` applied, or `None` when it has no
229/// `\u` / `\U` escape and would be unchanged.
230fn unescaped(text: &str) -> Option<String> {
231    if !text.contains("\\u") && !text.contains("\\U") {
232        return None;
233    }
234    let mut out = String::with_capacity(text.len());
235    let mut rest = text;
236    while let Some(at) = rest.find('\\') {
237        out.push_str(&rest[..at]);
238        let after = &rest[at + 1..];
239        let width = match after.as_bytes().first() {
240            Some(b'u') => 4,
241            Some(b'U') => 8,
242            _ => 0,
243        };
244        let decoded = (width > 0)
245            .then(|| after.get(1..=width))
246            .flatten()
247            .and_then(|hex| u32::from_str_radix(hex, 16).ok())
248            .and_then(char::from_u32);
249        match decoded {
250            Some(c) => {
251                out.push(c);
252                rest = &after[1 + width..];
253            }
254            None => {
255                out.push('\\');
256                rest = after;
257            }
258        }
259    }
260    out.push_str(rest);
261    Some(out)
262}
263
264#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
265enum Lex {
266    Code,
267    Comment,
268    /// A string literal opened by `quote`; `long` for the triple-quoted forms.
269    Str {
270        quote: u8,
271        long: bool,
272    },
273    /// Inside a `<…>` read as an IRI, until the `>`.
274    Iri,
275}
276
277#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
278struct Reading {
279    lex: Lex,
280    /// Bytes still to pass over without reading them (an escape's second byte, the rest of
281    /// a `'''`, the second `<` of `<<`).
282    skip: u8,
283    /// The open brackets, innermost last; `<` stands for `<<`.
284    open: Vec<u8>,
285    /// The last byte of code read, whitespace and comments aside — enough to know whether
286    /// what came before a `<` could have been a less-than's left operand.
287    last: u8,
288    /// The `!`s in the run being read. spargebra parses `!!!x` as nesting — the one prefix
289    /// operator it recurses on (`-`, `+` and a path's `^` do not repeat) — so a run counts
290    /// toward the depth like brackets do, for as long as it lasts.
291    bangs: usize,
292}
293
294/// The deepest any reading of `text` nests, stopping early once one passes the bound.
295/// `Err(n)` when more than `n` readings were alive at once.
296fn deepest(text: &[u8]) -> std::result::Result<usize, usize> {
297    let mut readings = vec![Reading {
298        lex: Lex::Code,
299        skip: 0,
300        open: Vec::new(),
301        last: b'(',
302        bangs: 0,
303    }];
304    let mut forks: Vec<Reading> = Vec::new();
305    let mut deepest = 0;
306    for (at, &byte) in text.iter().enumerate() {
307        for reading in readings.iter_mut() {
308            step(reading, text, at, byte, &mut forks);
309        }
310        if !forks.is_empty() {
311            readings.append(&mut forks);
312        }
313        if readings.len() > 1 {
314            readings.sort_unstable();
315            readings.dedup();
316            if readings.len() > MAX_SCAN_BRANCHES {
317                return Err(MAX_SCAN_BRANCHES);
318            }
319        }
320        for reading in &readings {
321            deepest = deepest.max(reading.open.len() + reading.bangs);
322        }
323        if deepest > MAX_SPARQL_NESTING {
324            break;
325        }
326    }
327    Ok(deepest)
328}
329
330fn step(reading: &mut Reading, text: &[u8], at: usize, byte: u8, forks: &mut Vec<Reading>) {
331    if reading.skip > 0 {
332        reading.skip -= 1;
333        return;
334    }
335    let next = |n: usize| text.get(at + n).copied();
336    match reading.lex {
337        Lex::Comment => {
338            if byte == b'\n' || byte == b'\r' {
339                reading.lex = Lex::Code;
340            }
341        }
342        Lex::Iri => {
343            if byte == b'>' {
344                reading.lex = Lex::Code;
345            }
346        }
347        Lex::Str { quote, long } => match byte {
348            b'\\' => reading.skip = 1,
349            // A short string cannot hold a line break; spargebra fails there, and reading
350            // on as code can only count more, never less.
351            b'\n' | b'\r' if !long => reading.lex = Lex::Code,
352            b if b == quote && !long => reading.lex = Lex::Code,
353            b if b == quote && next(1) == Some(quote) && next(2) == Some(quote) => {
354                reading.lex = Lex::Code;
355                reading.skip = 2;
356            }
357            _ => {}
358        },
359        Lex::Code => {
360            match byte {
361                // A comment inside a run does not end it.
362                b if b <= b' ' || b == b'#' => {}
363                b'!' if next(1) != Some(b'=') => reading.bangs += 1,
364                _ => reading.bangs = 0,
365            }
366            code(reading, text, at, byte, forks);
367        }
368    }
369}
370
371fn code(reading: &mut Reading, text: &[u8], at: usize, byte: u8, forks: &mut Vec<Reading>) {
372    let next = |n: usize| text.get(at + n).copied();
373    match byte {
374        b if b <= b' ' => {}
375        b'#' => reading.lex = Lex::Comment,
376        // A prefixed name's `\(`, `\#`, `\'`… escapes one character into the name, and a
377        // name is an operand whatever the character was.
378        b'\\' => {
379            reading.skip = 1;
380            reading.last = b'a';
381        }
382        b'\'' | b'"' => {
383            let long = next(1) == Some(byte) && next(2) == Some(byte);
384            reading.lex = Lex::Str { quote: byte, long };
385            reading.last = byte;
386            if long {
387                reading.skip = 2;
388            }
389        }
390        b'(' | b'{' | b'[' => {
391            reading.open.push(byte);
392            reading.last = byte;
393        }
394        b')' => close(reading, b'('),
395        b'}' => close(reading, b'{'),
396        b']' => close(reading, b'['),
397        b'>' if next(1) == Some(b'>') && reading.open.last() == Some(&b'<') => {
398            reading.open.pop();
399            reading.skip = 1;
400            reading.last = byte;
401        }
402        b'<' => {
403            // Every reading the grammar allows at a `<`. This one becomes the first
404            // that applies; the rest are pushed as forks.
405            let mut alternatives: Vec<Reading> = Vec::with_capacity(3);
406            if could_be_iri(text, at) {
407                alternatives.push(Reading {
408                    lex: Lex::Iri,
409                    skip: 0,
410                    open: reading.open.clone(),
411                    last: b'>',
412                    bangs: 0,
413                });
414            }
415            if next(1) == Some(b'<') {
416                let mut open = reading.open.clone();
417                open.push(b'<');
418                alternatives.push(Reading {
419                    lex: Lex::Code,
420                    skip: 1,
421                    open,
422                    last: b'<',
423                    bangs: 0,
424                });
425            }
426            // A less-than (or `<=`) needs an expression — and every SPARQL expression
427            // sits directly inside `(` — and a left operand before it. With no other
428            // reading left, keep this one anyway: the parser fails here, and reading on
429            // can only count more.
430            let less_than = reading.open.last() == Some(&b'(')
431                && !NEVER_ENDS_AN_OPERAND.contains(&reading.last);
432            if less_than || alternatives.is_empty() {
433                let mut operator = reading.clone();
434                operator.last = b'<';
435                alternatives.push(operator);
436            }
437            let mut alternatives = alternatives.into_iter();
438            if let Some(first) = alternatives.next() {
439                *reading = first;
440            }
441            forks.extend(alternatives);
442        }
443        _ => reading.last = byte,
444    }
445}
446
447/// Bytes after which no operand has just ended, so a `<` that follows cannot be a
448/// less-than. Deliberately short — `-` and `.` are left out because a prefixed name can end
449/// in one, and a byte left out only adds a reading.
450const NEVER_ENDS_AN_OPERAND: &[u8] = b"(,=!&|+*/^<";
451
452/// Pop the innermost bracket if `opener` is what it is. A closer that does not match is
453/// one the parser fails on in this reading, so it closes nothing — never lowering the count.
454fn close(reading: &mut Reading, opener: u8) {
455    if reading.open.last() == Some(&opener) {
456        reading.open.pop();
457    }
458    reading.last = b')';
459}
460
461/// Whether the `<` at `at` could open an IRI: spargebra takes `<` + anything but `>` + `>`
462/// and keeps it only if the inside parses as an IRI, which no IRI does with whitespace, a
463/// control character, `<`, `"`, `{` or `}` in it. Erring towards "could" is the safe
464/// direction — it only adds a reading. The look-ahead stops at the first such byte, so the
465/// scan stays linear.
466fn could_be_iri(text: &[u8], at: usize) -> bool {
467    for &byte in &text[at + 1..] {
468        match byte {
469            b'>' => return true,
470            b if b <= b' ' => return false,
471            b'<' | b'"' | b'{' | b'}' => return false,
472            _ => {}
473        }
474    }
475    false
476}
477
478#[cfg(test)]
479mod tests {
480    use super::*;
481
482    fn depth(text: &str) -> usize {
483        deepest(text.as_bytes()).unwrap()
484    }
485
486    #[test]
487    fn brackets_of_every_kind_count_and_close_only_what_they_match() {
488        assert_eq!(depth("SELECT * WHERE { ?s ?p ?o }"), 1);
489        assert_eq!(depth("SELECT * WHERE { { [ <urn:p> ( 1 ) ] } }"), 4);
490        // A closer of the wrong kind closes nothing: it cannot be used to look shallower.
491        assert_eq!(depth("( ] ( ] ( ] ("), 4);
492        assert_eq!(depth(") ) ) ( ( ("), 3);
493    }
494
495    #[test]
496    fn strings_iris_and_comments_hide_brackets() {
497        assert_eq!(depth(r#"{ ?s ?p "(((([[[[{{{{" }"#), 1);
498        assert_eq!(depth(r"{ ?s ?p '(((' }"), 1);
499        assert_eq!(depth(r#"{ ?s ?p """((( " "" ((( """ }"#), 1);
500        assert_eq!(depth("{ ?s ?p '''((( ' '' ((('''}"), 1);
501        assert_eq!(depth(r#"{ ?s ?p "\"(((" }"#), 1);
502        assert_eq!(depth("{ ?s <http://x/a(b(c(d> ?o }"), 1);
503        assert_eq!(depth("{ # ((((((\n ?s ?p ?o }"), 1);
504        assert_eq!(depth("{ # ((((((\r ?s ?p ?o }"), 1);
505        // A prefixed name's escaped bracket is part of the name.
506        assert_eq!(depth(r"{ ?s ex:a\(\(\( ?o }"), 1);
507    }
508
509    #[test]
510    fn a_less_than_reading_is_followed_where_an_expression_can_be() {
511        // Inside `(`, `<…>` may be a less-than whose "IRI" is code: the scan counts both.
512        let attack = "SELECT * WHERE { FILTER(?a<(((((1)))))>0) }";
513        assert_eq!(depth(attack), 2 + 5);
514        // A string opened INSIDE what an IRI reading would skip.
515        let phase = format!(
516            "SELECT * WHERE {{ FILTER({}1) }}",
517            "( ?a<'> ) ' && ".repeat(20)
518        );
519        assert!(depth(&phase) >= 20, "{}", depth(&phase));
520        // A comment opened inside one, hiding the closers on the rest of the line.
521        let comment =
522            "SELECT * WHERE { FILTER(\n".to_string() + &"(( ?a<#> )) ?a < \n".repeat(10) + "1) }";
523        assert!(depth(&comment) >= 10, "{}", depth(&comment));
524        // After an operator there is no left operand, so `<…>` is an IRI there too.
525        assert_eq!(
526            depth("SELECT * WHERE { FILTER(?o != <urn:x:((((((> && ?o = <urn:y#(>) }"),
527            2
528        );
529        assert_eq!(
530            depth("SELECT * WHERE { FILTER(\"a\"^^<urn:t((((> = ?o) }"),
531            2
532        );
533        // …but a name ending in an escaped bracket is an operand, so not after one.
534        assert_eq!(
535            depth("SELECT * WHERE { FILTER(ex:a\\(<urn:x((((>) }"),
536            2 + 4
537        );
538        // Outside `(`, `<…>` can only be an IRI, so a `#` or `(` in one opens nothing.
539        let triple =
540            "SELECT * WHERE {\n".to_string() + &"{ ?s <http://x/v#p> ?o }\n".repeat(200) + "}";
541        assert_eq!(depth(&triple), 2);
542    }
543
544    #[test]
545    fn a_run_of_not_nests_and_not_equal_does_not() {
546        let run = format!("SELECT * WHERE {{ FILTER({}true) }}", "! #c\n!".repeat(30));
547        assert_eq!(depth(&run), 2 + 60);
548        assert_eq!(
549            depth("SELECT * WHERE { FILTER(?a != ?b && !?c && !(!?d)) }"),
550            2 + 2
551        );
552        // A path's `!` does not recurse, but counting it costs one level, never more.
553        assert_eq!(depth("SELECT * WHERE { ?s !<urn:p> ?o }"), 2);
554    }
555
556    #[test]
557    fn triple_terms_nest_and_the_unescaped_reading_is_scanned_too() {
558        assert_eq!(depth("{ << << ?s ?p ?o >> ?q ?r >> ?x ?y }"), 3);
559        let escaped = format!(
560            "SELECT * WHERE {{ FILTER({}1{}) }}",
561            r"\u0028".repeat(100),
562            r"\u0029".repeat(100)
563        );
564        assert!(depth(&escaped) < MAX_SPARQL_NESTING);
565        assert!(check_sparql(&escaped, "query").is_err());
566    }
567
568    #[test]
569    fn the_bound_is_the_number_the_integration_tests_restate() {
570        // COPY: kept from the source, where `tests/sparql_nesting.rs` must compile against an
571        // older release and so restates 64; here `tests/sparql.rs` restates it for the same
572        // reason (an integration test reads the bound as a reader would).
573        assert_eq!(MAX_SPARQL_NESTING, 64);
574    }
575
576    #[test]
577    fn the_bounds_refuse_by_name_and_admit_at_the_bound() {
578        let at = format!(
579            "SELECT * WHERE {{ FILTER({}1{}) }}",
580            "(".repeat(MAX_SPARQL_NESTING - 2),
581            ")".repeat(MAX_SPARQL_NESTING - 2)
582        );
583        assert_eq!(depth(&at), MAX_SPARQL_NESTING);
584        check_sparql(&at, "query").unwrap();
585        let over = at.replacen('(', "((", 1).replacen(')', "))", 1);
586        let err = check_sparql(&over, "content").unwrap_err();
587        assert!(
588            matches!(&err, Error::InvalidArgument { name, .. } if name == "content"),
589            "{err}"
590        );
591        let big = format!(
592            "SELECT * WHERE {{ VALUES ?x {{ {} }} }}",
593            "1 ".repeat(MAX_SPARQL_BYTES / 2)
594        );
595        let err = check_sparql(&big, "query").unwrap_err().to_string();
596        assert!(err.contains("MAX_SPARQL_BYTES"), "{err}");
597    }
598}