use ikigai_core::{Error, Result};
pub const MAX_SPARQL_BYTES: usize = 1 << 20;
pub const MAX_SPARQL_NESTING: usize = 64;
pub const MAX_SCAN_BRANCHES: usize = 64;
pub const STACK_BASE: usize = 16 << 20;
pub const STACK_PER_BYTE: usize = 512;
pub fn check_sparql(text: &str, arg: &str) -> Result<()> {
if text.len() > MAX_SPARQL_BYTES {
return Err(Error::InvalidArgument {
name: arg.to_string(),
detail: format!(
"this SPARQL text is {} bytes, and this store refuses any over {MAX_SPARQL_BYTES} \
(MAX_SPARQL_BYTES) before parsing it: the parser and evaluator are recursive, \
and the bound is what keeps the stack a query can claim finite. Split the \
work into several requests",
text.len()
),
});
}
let depth = deepest(text.as_bytes()).map_err(|ambiguous| refuse_ambiguous(arg, ambiguous))?;
let depth = match unescaped(text) {
Some(unescaped) => depth.max(
deepest(unescaped.as_bytes()).map_err(|ambiguous| refuse_ambiguous(arg, ambiguous))?,
),
None => depth,
};
if depth > MAX_SPARQL_NESTING {
return Err(Error::InvalidArgument {
name: arg.to_string(),
detail: format!(
"this SPARQL text nests its brackets deeper than {MAX_SPARQL_NESTING} \
(MAX_SPARQL_NESTING), counting `(`, `{{`, `[`, `<<` and each `!` of a run, \
outside strings, IRIs and comments. It is refused before parsing because the parser recurses once \
per level and a stack overflow aborts the whole host, not one request. Real \
queries nest well under 10; flatten this one"
),
});
}
Ok(())
}
fn refuse_ambiguous(arg: &str, readings: usize) -> Error {
Error::InvalidArgument {
name: arg.to_string(),
detail: format!(
"this SPARQL text can be read more than {readings} ways at once (MAX_SCAN_BRANCHES): \
a `<` inside parentheses is either an IRI or a less-than, and this text keeps too \
many of those readings open for its nesting to be bounded before parsing. Put a \
space after a less-than `<`, or write IRIs containing `'`, `#` or brackets with a \
PREFIX"
),
}
}
pub fn on_sparql_stack<T, F>(text: &str, work: F) -> Result<T>
where
T: Send,
F: FnOnce() -> Result<T> + Send,
{
#[cfg(not(target_family = "wasm"))]
{
let stack = STACK_BASE.saturating_add(text.len().saturating_mul(STACK_PER_BYTE));
std::thread::scope(|scope| {
let handle = std::thread::Builder::new()
.name("ikigai-script-sparql".to_string())
.stack_size(stack)
.spawn_scoped(scope, work)
.map_err(|e| {
Error::Unavailable(format!(
"could not start a {} MiB thread to parse this SPARQL text on: {e}",
stack >> 20
))
})?;
match handle.join() {
Ok(result) => result,
Err(panic) => std::panic::resume_unwind(panic),
}
})
}
#[cfg(target_family = "wasm")]
{
let _ = text;
work()
}
}
fn unescaped(text: &str) -> Option<String> {
if !text.contains("\\u") && !text.contains("\\U") {
return None;
}
let mut out = String::with_capacity(text.len());
let mut rest = text;
while let Some(at) = rest.find('\\') {
out.push_str(&rest[..at]);
let after = &rest[at + 1..];
let width = match after.as_bytes().first() {
Some(b'u') => 4,
Some(b'U') => 8,
_ => 0,
};
let decoded = (width > 0)
.then(|| after.get(1..=width))
.flatten()
.and_then(|hex| u32::from_str_radix(hex, 16).ok())
.and_then(char::from_u32);
match decoded {
Some(c) => {
out.push(c);
rest = &after[1 + width..];
}
None => {
out.push('\\');
rest = after;
}
}
}
out.push_str(rest);
Some(out)
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum Lex {
Code,
Comment,
Str {
quote: u8,
long: bool,
},
Iri,
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
struct Reading {
lex: Lex,
skip: u8,
open: Vec<u8>,
last: u8,
bangs: usize,
}
fn deepest(text: &[u8]) -> std::result::Result<usize, usize> {
let mut readings = vec![Reading {
lex: Lex::Code,
skip: 0,
open: Vec::new(),
last: b'(',
bangs: 0,
}];
let mut forks: Vec<Reading> = Vec::new();
let mut deepest = 0;
for (at, &byte) in text.iter().enumerate() {
for reading in readings.iter_mut() {
step(reading, text, at, byte, &mut forks);
}
if !forks.is_empty() {
readings.append(&mut forks);
}
if readings.len() > 1 {
readings.sort_unstable();
readings.dedup();
if readings.len() > MAX_SCAN_BRANCHES {
return Err(MAX_SCAN_BRANCHES);
}
}
for reading in &readings {
deepest = deepest.max(reading.open.len() + reading.bangs);
}
if deepest > MAX_SPARQL_NESTING {
break;
}
}
Ok(deepest)
}
fn step(reading: &mut Reading, text: &[u8], at: usize, byte: u8, forks: &mut Vec<Reading>) {
if reading.skip > 0 {
reading.skip -= 1;
return;
}
let next = |n: usize| text.get(at + n).copied();
match reading.lex {
Lex::Comment => {
if byte == b'\n' || byte == b'\r' {
reading.lex = Lex::Code;
}
}
Lex::Iri => {
if byte == b'>' {
reading.lex = Lex::Code;
}
}
Lex::Str { quote, long } => match byte {
b'\\' => reading.skip = 1,
b'\n' | b'\r' if !long => reading.lex = Lex::Code,
b if b == quote && !long => reading.lex = Lex::Code,
b if b == quote && next(1) == Some(quote) && next(2) == Some(quote) => {
reading.lex = Lex::Code;
reading.skip = 2;
}
_ => {}
},
Lex::Code => {
match byte {
b if b <= b' ' || b == b'#' => {}
b'!' if next(1) != Some(b'=') => reading.bangs += 1,
_ => reading.bangs = 0,
}
code(reading, text, at, byte, forks);
}
}
}
fn code(reading: &mut Reading, text: &[u8], at: usize, byte: u8, forks: &mut Vec<Reading>) {
let next = |n: usize| text.get(at + n).copied();
match byte {
b if b <= b' ' => {}
b'#' => reading.lex = Lex::Comment,
b'\\' => {
reading.skip = 1;
reading.last = b'a';
}
b'\'' | b'"' => {
let long = next(1) == Some(byte) && next(2) == Some(byte);
reading.lex = Lex::Str { quote: byte, long };
reading.last = byte;
if long {
reading.skip = 2;
}
}
b'(' | b'{' | b'[' => {
reading.open.push(byte);
reading.last = byte;
}
b')' => close(reading, b'('),
b'}' => close(reading, b'{'),
b']' => close(reading, b'['),
b'>' if next(1) == Some(b'>') && reading.open.last() == Some(&b'<') => {
reading.open.pop();
reading.skip = 1;
reading.last = byte;
}
b'<' => {
let mut alternatives: Vec<Reading> = Vec::with_capacity(3);
if could_be_iri(text, at) {
alternatives.push(Reading {
lex: Lex::Iri,
skip: 0,
open: reading.open.clone(),
last: b'>',
bangs: 0,
});
}
if next(1) == Some(b'<') {
let mut open = reading.open.clone();
open.push(b'<');
alternatives.push(Reading {
lex: Lex::Code,
skip: 1,
open,
last: b'<',
bangs: 0,
});
}
let less_than = reading.open.last() == Some(&b'(')
&& !NEVER_ENDS_AN_OPERAND.contains(&reading.last);
if less_than || alternatives.is_empty() {
let mut operator = reading.clone();
operator.last = b'<';
alternatives.push(operator);
}
let mut alternatives = alternatives.into_iter();
if let Some(first) = alternatives.next() {
*reading = first;
}
forks.extend(alternatives);
}
_ => reading.last = byte,
}
}
const NEVER_ENDS_AN_OPERAND: &[u8] = b"(,=!&|+*/^<";
fn close(reading: &mut Reading, opener: u8) {
if reading.open.last() == Some(&opener) {
reading.open.pop();
}
reading.last = b')';
}
fn could_be_iri(text: &[u8], at: usize) -> bool {
for &byte in &text[at + 1..] {
match byte {
b'>' => return true,
b if b <= b' ' => return false,
b'<' | b'"' | b'{' | b'}' => return false,
_ => {}
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
fn depth(text: &str) -> usize {
deepest(text.as_bytes()).unwrap()
}
#[test]
fn brackets_of_every_kind_count_and_close_only_what_they_match() {
assert_eq!(depth("SELECT * WHERE { ?s ?p ?o }"), 1);
assert_eq!(depth("SELECT * WHERE { { [ <urn:p> ( 1 ) ] } }"), 4);
assert_eq!(depth("( ] ( ] ( ] ("), 4);
assert_eq!(depth(") ) ) ( ( ("), 3);
}
#[test]
fn strings_iris_and_comments_hide_brackets() {
assert_eq!(depth(r#"{ ?s ?p "(((([[[[{{{{" }"#), 1);
assert_eq!(depth(r"{ ?s ?p '(((' }"), 1);
assert_eq!(depth(r#"{ ?s ?p """((( " "" ((( """ }"#), 1);
assert_eq!(depth("{ ?s ?p '''((( ' '' ((('''}"), 1);
assert_eq!(depth(r#"{ ?s ?p "\"(((" }"#), 1);
assert_eq!(depth("{ ?s <http://x/a(b(c(d> ?o }"), 1);
assert_eq!(depth("{ # ((((((\n ?s ?p ?o }"), 1);
assert_eq!(depth("{ # ((((((\r ?s ?p ?o }"), 1);
assert_eq!(depth(r"{ ?s ex:a\(\(\( ?o }"), 1);
}
#[test]
fn a_less_than_reading_is_followed_where_an_expression_can_be() {
let attack = "SELECT * WHERE { FILTER(?a<(((((1)))))>0) }";
assert_eq!(depth(attack), 2 + 5);
let phase = format!(
"SELECT * WHERE {{ FILTER({}1) }}",
"( ?a<'> ) ' && ".repeat(20)
);
assert!(depth(&phase) >= 20, "{}", depth(&phase));
let comment =
"SELECT * WHERE { FILTER(\n".to_string() + &"(( ?a<#> )) ?a < \n".repeat(10) + "1) }";
assert!(depth(&comment) >= 10, "{}", depth(&comment));
assert_eq!(
depth("SELECT * WHERE { FILTER(?o != <urn:x:((((((> && ?o = <urn:y#(>) }"),
2
);
assert_eq!(
depth("SELECT * WHERE { FILTER(\"a\"^^<urn:t((((> = ?o) }"),
2
);
assert_eq!(
depth("SELECT * WHERE { FILTER(ex:a\\(<urn:x((((>) }"),
2 + 4
);
let triple =
"SELECT * WHERE {\n".to_string() + &"{ ?s <http://x/v#p> ?o }\n".repeat(200) + "}";
assert_eq!(depth(&triple), 2);
}
#[test]
fn a_run_of_not_nests_and_not_equal_does_not() {
let run = format!("SELECT * WHERE {{ FILTER({}true) }}", "! #c\n!".repeat(30));
assert_eq!(depth(&run), 2 + 60);
assert_eq!(
depth("SELECT * WHERE { FILTER(?a != ?b && !?c && !(!?d)) }"),
2 + 2
);
assert_eq!(depth("SELECT * WHERE { ?s !<urn:p> ?o }"), 2);
}
#[test]
fn triple_terms_nest_and_the_unescaped_reading_is_scanned_too() {
assert_eq!(depth("{ << << ?s ?p ?o >> ?q ?r >> ?x ?y }"), 3);
let escaped = format!(
"SELECT * WHERE {{ FILTER({}1{}) }}",
r"\u0028".repeat(100),
r"\u0029".repeat(100)
);
assert!(depth(&escaped) < MAX_SPARQL_NESTING);
assert!(check_sparql(&escaped, "query").is_err());
}
#[test]
fn the_bound_is_the_number_the_integration_tests_restate() {
assert_eq!(MAX_SPARQL_NESTING, 64);
}
#[test]
fn the_bounds_refuse_by_name_and_admit_at_the_bound() {
let at = format!(
"SELECT * WHERE {{ FILTER({}1{}) }}",
"(".repeat(MAX_SPARQL_NESTING - 2),
")".repeat(MAX_SPARQL_NESTING - 2)
);
assert_eq!(depth(&at), MAX_SPARQL_NESTING);
check_sparql(&at, "query").unwrap();
let over = at.replacen('(', "((", 1).replacen(')', "))", 1);
let err = check_sparql(&over, "content").unwrap_err();
assert!(
matches!(&err, Error::InvalidArgument { name, .. } if name == "content"),
"{err}"
);
let big = format!(
"SELECT * WHERE {{ VALUES ?x {{ {} }} }}",
"1 ".repeat(MAX_SPARQL_BYTES / 2)
);
let err = check_sparql(&big, "query").unwrap_err().to_string();
assert!(err.contains("MAX_SPARQL_BYTES"), "{err}");
}
}