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("ed, "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}