use std::sync::atomic::{AtomicU64, Ordering};
use oxttl::n3::{N3Parser, N3Term};
use crate::error::PodError;
use crate::ldp::{Graph, PatchOutcome, Term, Triple};
use crate::rdf_input::{literal_to_term, MAX_RDF_INPUT_BYTES};
const SOLID: &str = "http://www.w3.org/ns/solid/terms#";
const RDF_TYPE: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
const MAX_WHERE_PATTERNS: usize = 32;
const MAX_PARTIAL_BINDINGS: usize = 10_000;
const MAX_MATCH_STEPS: usize = 10_000_000;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PatchTerm {
Term(Term),
Variable(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PatchTriple {
pub subject: PatchTerm,
pub predicate: PatchTerm,
pub object: PatchTerm,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct N3Patch {
pub inserts: Vec<PatchTriple>,
pub deletes: Vec<PatchTriple>,
pub where_clause: Vec<PatchTriple>,
}
impl N3Patch {
pub fn parse(body: &str, base_iri: Option<&str>) -> Result<Self, PodError> {
if body.len() > MAX_RDF_INPUT_BYTES {
return Err(PodError::PayloadTooLarge(format!(
"N3 Patch of {} bytes exceeds the {MAX_RDF_INPUT_BYTES} byte limit",
body.len()
)));
}
let mut parser = N3Parser::new();
if let Some(base) = base_iri {
parser = parser
.with_base_iri(base)
.map_err(|e| bad(format!("invalid base IRI: {e}")))?;
}
let mut quads = Vec::new();
for item in parser.for_slice(body.as_bytes()) {
quads.push(item.map_err(|e| bad(format!("Invalid N3 Patch: {e}")))?);
}
let is_patch_prop = |p: &N3Term| {
matches!(p, N3Term::NamedNode(n) if [
format!("{SOLID}inserts"),
format!("{SOLID}deletes"),
format!("{SOLID}where"),
].iter().any(|x| x == n.as_str()))
};
let is_patch_type = |p: &N3Term, o: &N3Term| {
matches!(p, N3Term::NamedNode(n) if n.as_str() == RDF_TYPE)
&& matches!(o, N3Term::NamedNode(n) if n.as_str() == format!("{SOLID}InsertDeletePatch"))
};
let top: Vec<_> = quads
.iter()
.filter(|q| matches!(q.graph_name, oxrdf::GraphName::DefaultGraph))
.collect();
let patch_subjects: Vec<&N3Term> = top
.iter()
.filter(|q| is_patch_type(&q.predicate, &q.object) || is_patch_prop(&q.predicate))
.map(|q| &q.subject)
.collect();
if patch_subjects.is_empty() {
return Err(bad("Invalid N3 Patch: no solid:InsertDeletePatch found"));
}
let formula = |prop: &str| -> Result<Vec<PatchTriple>, PodError> {
let predicate = format!("{SOLID}{prop}");
let graphs: Vec<&str> = top
.iter()
.filter(|q| patch_subjects.contains(&&q.subject))
.filter(|q| matches!(&q.predicate, N3Term::NamedNode(n) if n.as_str() == predicate))
.filter_map(|q| match &q.object {
N3Term::BlankNode(b) => Some(b.as_str()),
_ => None,
})
.collect();
quads
.iter()
.filter(|q| {
matches!(&q.graph_name, oxrdf::GraphName::BlankNode(g) if graphs.contains(&g.as_str()))
})
.map(|q| {
Ok(PatchTriple {
subject: pattern_term(&q.subject, "subject")?,
predicate: pattern_term(&q.predicate, "predicate")?,
object: pattern_term(&q.object, "object")?,
})
})
.collect()
};
let where_clause = formula("where")?;
if where_clause.len() > MAX_WHERE_PATTERNS {
return Err(bad(format!(
"Invalid N3 Patch: the where clause has {} patterns, more than {MAX_WHERE_PATTERNS}",
where_clause.len()
)));
}
Ok(Self {
inserts: formula("inserts")?,
deletes: formula("deletes")?,
where_clause,
})
}
#[must_use]
pub fn is_insert_only(&self) -> bool {
self.deletes.is_empty() && self.where_clause.is_empty()
}
pub fn apply(&self, target: Graph) -> Result<PatchOutcome, PodError> {
let bindings = if self.where_clause.is_empty() {
Vec::new()
} else {
let found = solutions(&self.where_clause, &target)?;
match found.as_slice() {
[only] => only
.iter()
.map(|(name, term)| ((*name).to_string(), (*term).clone()))
.collect(),
[] => {
return Err(PodError::Conflict(
"the patch where clause does not match the resource".into(),
))
}
_ => {
return Err(PodError::Conflict(
"the patch where clause matches more than once".into(),
))
}
}
};
let mut fresh = FreshBlanks::default();
let deletes = self
.deletes
.iter()
.map(|t| substitute(t, &bindings, &mut fresh))
.collect::<Result<Vec<_>, _>>()?;
let inserts = self
.inserts
.iter()
.map(|t| substitute(t, &bindings, &mut fresh))
.collect::<Result<Vec<_>, _>>()?;
let mut graph = target;
let mut removal = Graph::new();
for d in &deletes {
if !graph.contains(d) {
return Err(PodError::Conflict(format!(
"triple to delete not found: {d:?}"
)));
}
removal.insert(d.clone());
}
graph.subtract(&removal);
let mut addition = Graph::new();
for i in inserts {
addition.insert(i);
}
let inserted = addition.len();
graph.extend(&addition);
Ok(PatchOutcome {
graph,
inserted,
deleted: removal.len(),
})
}
}
fn bad(msg: impl Into<String>) -> PodError {
PodError::BadRequest(msg.into())
}
fn pattern_term(t: &N3Term, role: &str) -> Result<PatchTerm, PodError> {
let term = match t {
N3Term::Variable(v) => return Ok(PatchTerm::Variable(v.as_str().to_string())),
N3Term::NamedNode(n) => Term::Iri(n.as_str().to_string()),
N3Term::BlankNode(b) => Term::BlankNode(b.as_str().to_string()),
N3Term::Literal(l) => {
if role != "object" {
return Err(bad(format!("a literal is not allowed in {role} position")));
}
literal_to_term(l)
}
#[allow(unreachable_patterns)]
_ => return Err(bad(format!("unsupported N3 term in {role} position"))),
};
if role == "predicate" && !matches!(term, Term::Iri(_)) {
return Err(bad("a patch predicate must be an IRI or a variable"));
}
Ok(PatchTerm::Term(term))
}
type Bindings = Vec<(String, Term)>;
fn bound<'a>(b: &'a Bindings, name: &str) -> Option<&'a Term> {
b.iter().find(|(k, _)| k == name).map(|(_, v)| v)
}
type BorrowedBindings<'a> = Vec<(&'a str, &'a Term)>;
const UNBOUND: u32 = u32::MAX;
#[derive(Clone, Copy)]
enum Slot {
Const(Option<u32>),
Var(usize),
}
fn extend(p: &[Slot; 3], t: &[u32; 3], b: &[u32]) -> Option<Vec<u32>> {
let mut tentative: [(usize, u32); 3] = [(usize::MAX, UNBOUND); 3];
for i in 0..3 {
match p[i] {
Slot::Const(Some(id)) if id == t[i] => {}
Slot::Const(_) => return None,
Slot::Var(v) => {
let earlier = if b[v] != UNBOUND {
Some(b[v])
} else {
tentative[..i]
.iter()
.find(|(k, _)| *k == v)
.map(|(_, id)| *id)
};
match earlier {
Some(id) if id != t[i] => return None,
Some(_) => {}
None => tentative[i] = (v, t[i]),
}
}
}
}
let mut next = b.to_vec();
for (v, id) in tentative {
if v != usize::MAX {
next[v] = id;
}
}
Some(next)
}
fn solutions<'a>(
patterns: &'a [PatchTriple],
g: &'a Graph,
) -> Result<Vec<BorrowedBindings<'a>>, PodError> {
let too_broad = || PodError::Conflict("the patch where clause is too broad to evaluate".into());
let mut ids: std::collections::HashMap<&'a Term, u32> = std::collections::HashMap::new();
let mut terms: Vec<&'a Term> = Vec::new();
let mut intern = |t: &'a Term| -> u32 {
*ids.entry(t).or_insert_with(|| {
terms.push(t);
u32::try_from(terms.len() - 1).unwrap_or(UNBOUND)
})
};
let triples: Vec<[u32; 3]> = g
.triples()
.map(|t| [intern(&t.subject), intern(&t.predicate), intern(&t.object)])
.collect();
let mut var_names: Vec<&'a str> = Vec::new();
let compiled: Vec<[Slot; 3]> = patterns
.iter()
.map(|p| {
[&p.subject, &p.predicate, &p.object].map(|term| match term {
PatchTerm::Term(t) => Slot::Const(ids.get(t).copied()),
PatchTerm::Variable(name) => {
Slot::Var(match var_names.iter().position(|n| *n == name.as_str()) {
Some(i) => i,
None => {
var_names.push(name.as_str());
var_names.len() - 1
}
})
}
})
})
.collect();
let empty = vec![UNBOUND; var_names.len()];
let counts: Vec<usize> = compiled
.iter()
.map(|p| {
triples
.iter()
.filter(|t| extend(p, t, &empty).is_some())
.count()
})
.collect();
let mut remaining: Vec<usize> = (0..compiled.len()).collect();
let mut bound = vec![false; var_names.len()];
let mut steps = 0usize;
let mut partial: Vec<Vec<u32>> = vec![empty.clone()];
while !remaining.is_empty() {
let pick = (0..remaining.len())
.min_by_key(|&i| {
let p = &compiled[remaining[i]];
let joined = p.iter().any(|s| matches!(s, Slot::Var(v) if bound[*v]));
(!joined, counts[remaining[i]], i)
})
.unwrap_or(0);
let p = compiled[remaining.remove(pick)];
for s in p {
if let Slot::Var(v) = s {
bound[v] = true;
}
}
let last = remaining.is_empty();
let mut next = Vec::new();
'extend: for b in &partial {
for t in &triples {
steps += 1;
if steps > MAX_MATCH_STEPS {
return Err(too_broad());
}
if let Some(nb) = extend(&p, t, b) {
next.push(nb);
if last && next.len() > 1 {
break 'extend;
}
if next.len() > MAX_PARTIAL_BINDINGS {
return Err(too_broad());
}
}
}
}
partial = next;
if partial.is_empty() {
break;
}
}
Ok(partial
.into_iter()
.map(|b| {
b.into_iter()
.enumerate()
.filter(|(_, id)| *id != UNBOUND)
.map(|(v, id)| (var_names[v], terms[id as usize]))
.collect()
})
.collect())
}
#[derive(Default)]
struct FreshBlanks {
map: Vec<(String, String)>,
}
impl FreshBlanks {
fn get(&mut self, label: &str) -> Term {
static COUNTER: AtomicU64 = AtomicU64::new(0);
if let Some((_, fresh)) = self.map.iter().find(|(l, _)| l == label) {
return Term::BlankNode(fresh.clone());
}
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let fresh = format!("p{}_{nonce:x}", COUNTER.fetch_add(1, Ordering::Relaxed));
self.map.push((label.to_string(), fresh.clone()));
Term::BlankNode(fresh)
}
}
fn substitute(t: &PatchTriple, b: &Bindings, fresh: &mut FreshBlanks) -> Result<Triple, PodError> {
let mut term = |p: &PatchTerm| -> Result<Term, PodError> {
match p {
PatchTerm::Variable(name) => bound(b, name)
.cloned()
.ok_or_else(|| PodError::Conflict(format!("unbound variable ?{name} in patch"))),
PatchTerm::Term(Term::BlankNode(label)) => Ok(fresh.get(label)),
PatchTerm::Term(t) => Ok(t.clone()),
}
};
let subject = term(&t.subject)?;
let predicate = term(&t.predicate)?;
let object = term(&t.object)?;
if matches!(subject, Term::Literal { .. }) || !matches!(predicate, Term::Iri(_)) {
return Err(PodError::Conflict(
"a variable bound to a literal cannot be a subject or predicate".into(),
));
}
Ok(Triple::new(subject, predicate, object))
}