1use std::sync::atomic::{AtomicU64, Ordering};
15
16use oxttl::n3::{N3Parser, N3Term};
17
18use crate::error::PodError;
19use crate::ldp::{Graph, PatchOutcome, Term, Triple};
20use crate::rdf_input::{literal_to_term, MAX_RDF_INPUT_BYTES};
21
22const SOLID: &str = "http://www.w3.org/ns/solid/terms#";
23const RDF_TYPE: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
24
25const MAX_WHERE_PATTERNS: usize = 32;
28
29const MAX_PARTIAL_BINDINGS: usize = 10_000;
34
35const MAX_MATCH_STEPS: usize = 10_000_000;
39
40#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum PatchTerm {
43 Term(Term),
45 Variable(String),
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct PatchTriple {
53 pub subject: PatchTerm,
55 pub predicate: PatchTerm,
57 pub object: PatchTerm,
59}
60
61#[derive(Debug, Clone, Default, PartialEq, Eq)]
82pub struct N3Patch {
83 pub inserts: Vec<PatchTriple>,
85 pub deletes: Vec<PatchTriple>,
87 pub where_clause: Vec<PatchTriple>,
89}
90
91impl N3Patch {
92 pub fn parse(body: &str, base_iri: Option<&str>) -> Result<Self, PodError> {
109 if body.len() > MAX_RDF_INPUT_BYTES {
110 return Err(PodError::PayloadTooLarge(format!(
111 "N3 Patch of {} bytes exceeds the {MAX_RDF_INPUT_BYTES} byte limit",
112 body.len()
113 )));
114 }
115 let mut parser = N3Parser::new();
116 if let Some(base) = base_iri {
117 parser = parser
118 .with_base_iri(base)
119 .map_err(|e| bad(format!("invalid base IRI: {e}")))?;
120 }
121 let mut quads = Vec::new();
122 for item in parser.for_slice(body.as_bytes()) {
123 quads.push(item.map_err(|e| bad(format!("Invalid N3 Patch: {e}")))?);
124 }
125
126 let is_patch_prop = |p: &N3Term| {
127 matches!(p, N3Term::NamedNode(n) if [
128 format!("{SOLID}inserts"),
129 format!("{SOLID}deletes"),
130 format!("{SOLID}where"),
131 ].iter().any(|x| x == n.as_str()))
132 };
133 let is_patch_type = |p: &N3Term, o: &N3Term| {
134 matches!(p, N3Term::NamedNode(n) if n.as_str() == RDF_TYPE)
135 && matches!(o, N3Term::NamedNode(n) if n.as_str() == format!("{SOLID}InsertDeletePatch"))
136 };
137 let top: Vec<_> = quads
138 .iter()
139 .filter(|q| matches!(q.graph_name, oxrdf::GraphName::DefaultGraph))
140 .collect();
141 let patch_subjects: Vec<&N3Term> = top
142 .iter()
143 .filter(|q| is_patch_type(&q.predicate, &q.object) || is_patch_prop(&q.predicate))
144 .map(|q| &q.subject)
145 .collect();
146 if patch_subjects.is_empty() {
147 return Err(bad("Invalid N3 Patch: no solid:InsertDeletePatch found"));
148 }
149
150 let formula = |prop: &str| -> Result<Vec<PatchTriple>, PodError> {
151 let predicate = format!("{SOLID}{prop}");
152 let graphs: Vec<&str> = top
153 .iter()
154 .filter(|q| patch_subjects.contains(&&q.subject))
155 .filter(|q| matches!(&q.predicate, N3Term::NamedNode(n) if n.as_str() == predicate))
156 .filter_map(|q| match &q.object {
157 N3Term::BlankNode(b) => Some(b.as_str()),
158 _ => None,
159 })
160 .collect();
161 quads
162 .iter()
163 .filter(|q| {
164 matches!(&q.graph_name, oxrdf::GraphName::BlankNode(g) if graphs.contains(&g.as_str()))
165 })
166 .map(|q| {
167 Ok(PatchTriple {
168 subject: pattern_term(&q.subject, "subject")?,
169 predicate: pattern_term(&q.predicate, "predicate")?,
170 object: pattern_term(&q.object, "object")?,
171 })
172 })
173 .collect()
174 };
175
176 let where_clause = formula("where")?;
177 if where_clause.len() > MAX_WHERE_PATTERNS {
178 return Err(bad(format!(
179 "Invalid N3 Patch: the where clause has {} patterns, more than {MAX_WHERE_PATTERNS}",
180 where_clause.len()
181 )));
182 }
183 Ok(Self {
184 inserts: formula("inserts")?,
185 deletes: formula("deletes")?,
186 where_clause,
187 })
188 }
189
190 #[must_use]
193 pub fn is_insert_only(&self) -> bool {
194 self.deletes.is_empty() && self.where_clause.is_empty()
195 }
196
197 pub fn apply(&self, target: Graph) -> Result<PatchOutcome, PodError> {
206 let bindings = if self.where_clause.is_empty() {
207 Vec::new()
208 } else {
209 let found = solutions(&self.where_clause, &target)?;
210 match found.as_slice() {
211 [only] => only
212 .iter()
213 .map(|(name, term)| ((*name).to_string(), (*term).clone()))
214 .collect(),
215 [] => {
216 return Err(PodError::Conflict(
217 "the patch where clause does not match the resource".into(),
218 ))
219 }
220 _ => {
221 return Err(PodError::Conflict(
222 "the patch where clause matches more than once".into(),
223 ))
224 }
225 }
226 };
227
228 let mut fresh = FreshBlanks::default();
229 let deletes = self
230 .deletes
231 .iter()
232 .map(|t| substitute(t, &bindings, &mut fresh))
233 .collect::<Result<Vec<_>, _>>()?;
234 let inserts = self
235 .inserts
236 .iter()
237 .map(|t| substitute(t, &bindings, &mut fresh))
238 .collect::<Result<Vec<_>, _>>()?;
239
240 let mut graph = target;
241 let mut removal = Graph::new();
242 for d in &deletes {
243 if !graph.contains(d) {
244 return Err(PodError::Conflict(format!(
245 "triple to delete not found: {d:?}"
246 )));
247 }
248 removal.insert(d.clone());
249 }
250 graph.subtract(&removal);
251 let mut addition = Graph::new();
252 for i in inserts {
253 addition.insert(i);
254 }
255 let inserted = addition.len();
256 graph.extend(&addition);
257
258 Ok(PatchOutcome {
259 graph,
260 inserted,
261 deleted: removal.len(),
262 })
263 }
264}
265
266fn bad(msg: impl Into<String>) -> PodError {
267 PodError::BadRequest(msg.into())
268}
269
270fn pattern_term(t: &N3Term, role: &str) -> Result<PatchTerm, PodError> {
271 let term = match t {
272 N3Term::Variable(v) => return Ok(PatchTerm::Variable(v.as_str().to_string())),
273 N3Term::NamedNode(n) => Term::Iri(n.as_str().to_string()),
274 N3Term::BlankNode(b) => Term::BlankNode(b.as_str().to_string()),
275 N3Term::Literal(l) => {
276 if role != "object" {
277 return Err(bad(format!("a literal is not allowed in {role} position")));
278 }
279 literal_to_term(l)
280 }
281 #[allow(unreachable_patterns)]
282 _ => return Err(bad(format!("unsupported N3 term in {role} position"))),
283 };
284 if role == "predicate" && !matches!(term, Term::Iri(_)) {
285 return Err(bad("a patch predicate must be an IRI or a variable"));
286 }
287 Ok(PatchTerm::Term(term))
288}
289
290type Bindings = Vec<(String, Term)>;
292
293fn bound<'a>(b: &'a Bindings, name: &str) -> Option<&'a Term> {
294 b.iter().find(|(k, _)| k == name).map(|(_, v)| v)
295}
296
297type BorrowedBindings<'a> = Vec<(&'a str, &'a Term)>;
299
300const UNBOUND: u32 = u32::MAX;
302
303#[derive(Clone, Copy)]
305enum Slot {
306 Const(Option<u32>),
309 Var(usize),
311}
312
313fn extend(p: &[Slot; 3], t: &[u32; 3], b: &[u32]) -> Option<Vec<u32>> {
316 let mut tentative: [(usize, u32); 3] = [(usize::MAX, UNBOUND); 3];
317 for i in 0..3 {
318 match p[i] {
319 Slot::Const(Some(id)) if id == t[i] => {}
320 Slot::Const(_) => return None,
321 Slot::Var(v) => {
322 let earlier = if b[v] != UNBOUND {
323 Some(b[v])
324 } else {
325 tentative[..i]
326 .iter()
327 .find(|(k, _)| *k == v)
328 .map(|(_, id)| *id)
329 };
330 match earlier {
331 Some(id) if id != t[i] => return None,
332 Some(_) => {}
333 None => tentative[i] = (v, t[i]),
334 }
335 }
336 }
337 }
338 let mut next = b.to_vec();
339 for (v, id) in tentative {
340 if v != usize::MAX {
341 next[v] = id;
342 }
343 }
344 Some(next)
345}
346
347fn solutions<'a>(
364 patterns: &'a [PatchTriple],
365 g: &'a Graph,
366) -> Result<Vec<BorrowedBindings<'a>>, PodError> {
367 let too_broad = || PodError::Conflict("the patch where clause is too broad to evaluate".into());
368
369 let mut ids: std::collections::HashMap<&'a Term, u32> = std::collections::HashMap::new();
370 let mut terms: Vec<&'a Term> = Vec::new();
371 let mut intern = |t: &'a Term| -> u32 {
372 *ids.entry(t).or_insert_with(|| {
373 terms.push(t);
374 u32::try_from(terms.len() - 1).unwrap_or(UNBOUND)
375 })
376 };
377 let triples: Vec<[u32; 3]> = g
378 .triples()
379 .map(|t| [intern(&t.subject), intern(&t.predicate), intern(&t.object)])
380 .collect();
381
382 let mut var_names: Vec<&'a str> = Vec::new();
383 let compiled: Vec<[Slot; 3]> = patterns
384 .iter()
385 .map(|p| {
386 [&p.subject, &p.predicate, &p.object].map(|term| match term {
387 PatchTerm::Term(t) => Slot::Const(ids.get(t).copied()),
388 PatchTerm::Variable(name) => {
389 Slot::Var(match var_names.iter().position(|n| *n == name.as_str()) {
390 Some(i) => i,
391 None => {
392 var_names.push(name.as_str());
393 var_names.len() - 1
394 }
395 })
396 }
397 })
398 })
399 .collect();
400 let empty = vec![UNBOUND; var_names.len()];
401 let counts: Vec<usize> = compiled
404 .iter()
405 .map(|p| {
406 triples
407 .iter()
408 .filter(|t| extend(p, t, &empty).is_some())
409 .count()
410 })
411 .collect();
412
413 let mut remaining: Vec<usize> = (0..compiled.len()).collect();
414 let mut bound = vec![false; var_names.len()];
415 let mut steps = 0usize;
416 let mut partial: Vec<Vec<u32>> = vec![empty.clone()];
417 while !remaining.is_empty() {
418 let pick = (0..remaining.len())
419 .min_by_key(|&i| {
420 let p = &compiled[remaining[i]];
421 let joined = p.iter().any(|s| matches!(s, Slot::Var(v) if bound[*v]));
422 (!joined, counts[remaining[i]], i)
423 })
424 .unwrap_or(0);
425 let p = compiled[remaining.remove(pick)];
426 for s in p {
427 if let Slot::Var(v) = s {
428 bound[v] = true;
429 }
430 }
431 let last = remaining.is_empty();
432 let mut next = Vec::new();
433 'extend: for b in &partial {
434 for t in &triples {
435 steps += 1;
436 if steps > MAX_MATCH_STEPS {
437 return Err(too_broad());
438 }
439 if let Some(nb) = extend(&p, t, b) {
440 next.push(nb);
441 if last && next.len() > 1 {
442 break 'extend;
443 }
444 if next.len() > MAX_PARTIAL_BINDINGS {
445 return Err(too_broad());
446 }
447 }
448 }
449 }
450 partial = next;
451 if partial.is_empty() {
452 break;
453 }
454 }
455 Ok(partial
456 .into_iter()
457 .map(|b| {
458 b.into_iter()
459 .enumerate()
460 .filter(|(_, id)| *id != UNBOUND)
461 .map(|(v, id)| (var_names[v], terms[id as usize]))
462 .collect()
463 })
464 .collect())
465}
466
467#[derive(Default)]
469struct FreshBlanks {
470 map: Vec<(String, String)>,
471}
472
473impl FreshBlanks {
474 fn get(&mut self, label: &str) -> Term {
475 static COUNTER: AtomicU64 = AtomicU64::new(0);
476 if let Some((_, fresh)) = self.map.iter().find(|(l, _)| l == label) {
477 return Term::BlankNode(fresh.clone());
478 }
479 let nonce = std::time::SystemTime::now()
480 .duration_since(std::time::UNIX_EPOCH)
481 .map(|d| d.as_nanos())
482 .unwrap_or(0);
483 let fresh = format!("p{}_{nonce:x}", COUNTER.fetch_add(1, Ordering::Relaxed));
484 self.map.push((label.to_string(), fresh.clone()));
485 Term::BlankNode(fresh)
486 }
487}
488
489fn substitute(t: &PatchTriple, b: &Bindings, fresh: &mut FreshBlanks) -> Result<Triple, PodError> {
490 let mut term = |p: &PatchTerm| -> Result<Term, PodError> {
491 match p {
492 PatchTerm::Variable(name) => bound(b, name)
493 .cloned()
494 .ok_or_else(|| PodError::Conflict(format!("unbound variable ?{name} in patch"))),
495 PatchTerm::Term(Term::BlankNode(label)) => Ok(fresh.get(label)),
496 PatchTerm::Term(t) => Ok(t.clone()),
497 }
498 };
499 let subject = term(&t.subject)?;
500 let predicate = term(&t.predicate)?;
501 let object = term(&t.object)?;
502 if matches!(subject, Term::Literal { .. }) || !matches!(predicate, Term::Iri(_)) {
503 return Err(PodError::Conflict(
504 "a variable bound to a literal cannot be a subject or predicate".into(),
505 ));
506 }
507 Ok(Triple::new(subject, predicate, object))
508}