use std::borrow::Cow;
use super::SparqlError;
const RDF_REIFIES: &str = "<http://www.w3.org/1999/02/22-rdf-syntax-ns#reifies>";
pub(super) fn version(query: &str) -> Option<&str> {
let mut rest = query;
loop {
let t = rest.trim_start();
if let Some(after_hash) = t.strip_prefix('#') {
rest = after_hash.split_once('\n')?.1;
continue;
}
if t.len() < 7 || !t[..7].eq_ignore_ascii_case("VERSION") {
return None;
}
let after = t[7..].trim_start();
let q = after.chars().next().filter(|c| *c == '"' || *c == '\'')?;
let body = &after[1..];
return body.find(q).map(|end| &body[..end]);
}
}
pub(super) fn is_sparql12(version: Option<&str>) -> bool {
version.is_some_and(|v| v.starts_with("1.2"))
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Open {
TripleTerm,
Quoted,
}
pub(super) fn rewrite(query: &str, sparql12: bool) -> Result<Cow<'_, str>, SparqlError> {
let rewrites = if sparql12 {
query.contains("<<") || query.contains("{|")
} else {
query.contains("<<(")
};
if !rewrites {
return Ok(Cow::Borrowed(query));
}
let b = query.as_bytes();
let mut out = String::with_capacity(query.len() + 64);
let mut stack: Vec<Open> = Vec::new();
let mut i = 0;
let copy = |out: &mut String, from: usize, to: usize| out.push_str(&query[from..to]);
while i < b.len() {
match b[i] {
b'#' => {
let end = query[i..].find('\n').map_or(b.len(), |n| i + n);
copy(&mut out, i, end);
i = end;
}
b'"' | b'\'' => {
let end = string_end(b, i);
copy(&mut out, i, end);
i = end;
}
b'\\' => {
let end = (i + 1 + utf8_len(b.get(i + 1).copied())).min(b.len());
copy(&mut out, i, end);
i = end;
}
b'<' if b.get(i + 1) == Some(&b'<') => {
if b.get(i + 2) == Some(&b'(') {
stack.push(Open::TripleTerm);
out.push_str("<<");
i += 3;
} else {
stack.push(Open::Quoted);
out.push_str(if sparql12 {
"[ "
} else {
"<<"
});
if sparql12 {
out.push_str(RDF_REIFIES);
out.push_str(" <<");
}
i += 2;
}
}
b'<' => {
let end = iri_end(b, i);
copy(&mut out, i, end);
i = end;
}
b')' if b.get(i + 1) == Some(&b'>')
&& b.get(i + 2) == Some(&b'>')
&& stack.last() == Some(&Open::TripleTerm) =>
{
stack.pop();
out.push_str(">>");
i += 3;
}
b'>' if b.get(i + 1) == Some(&b'>') && stack.last() == Some(&Open::Quoted) => {
stack.pop();
out.push_str(if sparql12 { ">> ]" } else { ">>" });
i += 2;
}
b'~' if sparql12 && stack.last() == Some(&Open::Quoted) => {
return Err(SparqlError::Unsupported(
"a named reifier `<< s p o ~ ?r >>` under VERSION \"1.2\"; \
write `?r rdf:reifies <<( s p o )>>`",
));
}
b'{' if sparql12 && b.get(i + 1) == Some(&b'|') => {
return Err(SparqlError::Unsupported(
"the annotation syntax `{| … |}` under VERSION \"1.2\"; \
write `?r rdf:reifies <<( s p o )>>`",
));
}
_ => {
let n = utf8_len(Some(b[i]));
copy(&mut out, i, (i + n).min(b.len()));
i += n;
}
}
}
Ok(Cow::Owned(out))
}
fn utf8_len(lead: Option<u8>) -> usize {
match lead {
Some(c) if c >= 0xF0 => 4,
Some(c) if c >= 0xE0 => 3,
Some(c) if c >= 0xC0 => 2,
_ => 1,
}
}
fn string_end(b: &[u8], start: usize) -> usize {
let q = b[start];
let long = b.get(start + 1) == Some(&q) && b.get(start + 2) == Some(&q);
let mut i = start + if long { 3 } else { 1 };
while i < b.len() {
match b[i] {
b'\\' => i += 2,
c if c == q => {
if !long {
return i + 1;
}
if b.get(i + 1) == Some(&q) && b.get(i + 2) == Some(&q) {
return i + 3;
}
i += 1;
}
b'\n' | b'\r' if !long => return i,
_ => i += 1,
}
}
b.len()
}
fn iri_end(b: &[u8], start: usize) -> usize {
let mut i = start + 1;
while i < b.len() {
match b[i] {
b'>' => return i + 1,
b'<' | b'"' | b'{' | b'}' | b'|' | b'^' | b'`' | b'\\' => return start + 1,
c if c <= b' ' => return start + 1,
_ => i += 1,
}
}
start + 1
}
#[cfg(test)]
mod tests {
use super::*;
fn rw(q: &str, v12: bool) -> String {
rewrite(q, v12).unwrap().into_owned()
}
#[test]
fn triple_terms_become_quoted_triples_in_either_mode() {
let q = "SELECT * WHERE { ?s <http://ex/p> <<( ?a ?b \"x)>>\" )>> }";
let want = "SELECT * WHERE { ?s <http://ex/p> << ?a ?b \"x)>>\" >> }";
assert_eq!(rw(q, false), want);
assert_eq!(rw(q, true), want);
assert_eq!(
rw("<<( <<( :a :b :c )>> :d :e )>>", false),
"<< << :a :b :c >> :d :e >>"
);
}
#[test]
fn quoted_triples_keep_their_meaning_without_the_declaration() {
let q = "SELECT * WHERE { << ?s ?p ?o >> <http://ex/source> ?src }";
assert!(matches!(rewrite(q, false).unwrap(), Cow::Borrowed(_)));
}
#[test]
fn under_sparql12_a_quoted_triple_is_a_reifier() {
assert_eq!(
rw("{ << :a :b ?o >> :q ?v . ?x :r << :c :d <<( :e :f :g )>> >> }", true),
format!(
"{{ [ {RDF_REIFIES} << :a :b ?o >> ] :q ?v . ?x :r [ {RDF_REIFIES} << :c :d << :e :f :g >> >> ] }}"
)
);
}
#[test]
fn strings_iris_comments_and_operators_are_left_alone() {
let q = "# << not a triple\nSELECT * WHERE { ?s ?p \"<<x>>\" , '''a <<( b'''. \
FILTER(?a < ?b && ?c<<http://ex/x>) }";
let out = rw(q, false);
assert!(out.starts_with("# << not a triple\n"));
assert!(out.contains("\"<<x>>\""));
assert!(out.contains("'''a <<( b'''"));
}
#[test]
fn named_reifiers_and_annotations_are_refused_under_sparql12() {
assert!(rewrite("{ << :a :b :c ~ ?r >> :q ?v }", true).is_err());
assert!(rewrite("{ :a :b :c {| :q ?v |} }", true).is_err());
assert!(rewrite("{ :a :b :c {| :q ?v |} }", false).is_ok());
assert!(rewrite("{ << :a :b ex:c\\~d >> :q ?v }", true).is_ok());
}
#[test]
fn version_reads_the_leading_declaration() {
assert_eq!(version("VERSION \"1.2\" SELECT * {}"), Some("1.2"));
assert_eq!(
version("# c\n version '1.2-basic'\nASK {}"),
Some("1.2-basic")
);
assert_eq!(version("SELECT * { ?s ?p \"VERSION\" }"), None);
assert!(is_sparql12(Some("1.2")) && !is_sparql12(Some("1.1")) && !is_sparql12(None));
}
}