1use oxttl::n3::{N3Parser, N3Term};
41use oxttl::TurtleParser;
42
43use crate::error::PodError;
44use crate::ldp::{Graph, Term, Triple};
45
46pub const MAX_RDF_INPUT_BYTES: usize = 4 * 1024 * 1024;
48
49pub const MAX_RDF_TRIPLES: usize = 250_000;
52
53const LOG_IMPLIES: &str = "http://www.w3.org/2000/10/swap/log#implies";
54const LOG_IMPLIED_BY: &str = "http://www.w3.org/2000/10/swap/log#impliedBy";
55const XSD_STRING: &str = "http://www.w3.org/2001/XMLSchema#string";
56const RDF_LANG_STRING: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#langString";
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum RdfInputSyntax {
61 Turtle,
63 N3,
65}
66
67impl RdfInputSyntax {
68 pub fn from_content_type(content_type: &str) -> Option<Self> {
78 let mime = content_type
79 .split(';')
80 .next()
81 .unwrap_or("")
82 .trim()
83 .to_ascii_lowercase();
84 match mime.as_str() {
85 "text/turtle" | "application/turtle" | "application/x-turtle" => Some(Self::Turtle),
86 "text/n3" => Some(Self::N3),
87 _ => None,
88 }
89 }
90}
91
92fn bad(msg: impl Into<String>) -> PodError {
93 PodError::BadRequest(msg.into())
94}
95
96fn push_checked(g: &mut Graph, t: Triple) -> Result<(), PodError> {
97 g.insert(t);
98 if g.len() > MAX_RDF_TRIPLES {
99 return Err(PodError::PayloadTooLarge(format!(
100 "RDF document expands to more than {MAX_RDF_TRIPLES} triples"
101 )));
102 }
103 Ok(())
104}
105
106pub(crate) fn literal_to_term(l: &oxrdf::Literal) -> Term {
107 let value = l.value().to_string();
108 if let Some(lang) = l.language() {
109 return Term::Literal {
110 value,
111 datatype: None,
112 language: Some(lang.to_string()),
113 };
114 }
115 let dt = l.datatype().as_str();
116 if dt == XSD_STRING || dt == RDF_LANG_STRING {
117 Term::Literal {
118 value,
119 datatype: None,
120 language: None,
121 }
122 } else {
123 Term::Literal {
124 value,
125 datatype: Some(dt.to_string()),
126 language: None,
127 }
128 }
129}
130
131fn subject_to_term(s: &oxrdf::Subject) -> Result<Term, PodError> {
132 match s {
133 oxrdf::Subject::NamedNode(n) => Ok(Term::Iri(n.as_str().to_string())),
134 oxrdf::Subject::BlankNode(b) => Ok(Term::BlankNode(b.as_str().to_string())),
135 #[allow(unreachable_patterns)]
136 _ => Err(PodError::Unsupported(
137 "quoted triples are not supported".into(),
138 )),
139 }
140}
141
142fn term_to_term(t: &oxrdf::Term) -> Result<Term, PodError> {
143 match t {
144 oxrdf::Term::NamedNode(n) => Ok(Term::Iri(n.as_str().to_string())),
145 oxrdf::Term::BlankNode(b) => Ok(Term::BlankNode(b.as_str().to_string())),
146 oxrdf::Term::Literal(l) => Ok(literal_to_term(l)),
147 #[allow(unreachable_patterns)]
148 _ => Err(PodError::Unsupported(
149 "quoted triples are not supported".into(),
150 )),
151 }
152}
153
154fn n3_term(t: &N3Term, role: &str) -> Result<Term, PodError> {
155 match t {
156 N3Term::NamedNode(n) => Ok(Term::Iri(n.as_str().to_string())),
157 N3Term::BlankNode(b) => Ok(Term::BlankNode(b.as_str().to_string())),
158 N3Term::Literal(l) => Ok(literal_to_term(l)),
159 N3Term::Variable(v) => Err(PodError::Unsupported(format!(
160 "N3 variable ?{} in {role} position is not supported",
161 v.as_str()
162 ))),
163 #[allow(unreachable_patterns)]
164 _ => Err(PodError::Unsupported(format!(
165 "unsupported N3 term in {role} position"
166 ))),
167 }
168}
169
170pub fn parse_rdf_body(
181 syntax: RdfInputSyntax,
182 body: &[u8],
183 base_iri: &str,
184) -> Result<Graph, PodError> {
185 if body.len() > MAX_RDF_INPUT_BYTES {
186 return Err(PodError::PayloadTooLarge(format!(
187 "RDF body of {} bytes exceeds the {MAX_RDF_INPUT_BYTES} byte limit",
188 body.len()
189 )));
190 }
191 if std::str::from_utf8(body).is_err() {
192 return Err(bad("RDF body is not valid UTF-8"));
193 }
194 match syntax {
195 RdfInputSyntax::Turtle => parse_turtle_inner(body, base_iri),
196 RdfInputSyntax::N3 => parse_n3_inner(body, base_iri),
197 }
198}
199
200pub fn parse_turtle(body: &[u8], base_iri: &str) -> Result<Graph, PodError> {
202 parse_rdf_body(RdfInputSyntax::Turtle, body, base_iri)
203}
204
205pub fn parse_n3(body: &[u8], base_iri: &str) -> Result<Graph, PodError> {
208 parse_rdf_body(RdfInputSyntax::N3, body, base_iri)
209}
210
211fn parse_turtle_inner(body: &[u8], base_iri: &str) -> Result<Graph, PodError> {
212 let parser = TurtleParser::new()
213 .with_base_iri(base_iri)
214 .map_err(|e| bad(format!("invalid base IRI: {e}")))?;
215 let mut g = Graph::new();
216 for item in parser.for_slice(body) {
217 let t = item.map_err(|e| bad(format!("Turtle syntax error: {e}")))?;
218 let triple = Triple::new(
219 subject_to_term(&t.subject)?,
220 Term::Iri(t.predicate.as_str().to_string()),
221 term_to_term(&t.object)?,
222 );
223 push_checked(&mut g, triple)?;
224 }
225 Ok(g)
226}
227
228fn parse_n3_inner(body: &[u8], base_iri: &str) -> Result<Graph, PodError> {
229 let parser = N3Parser::new()
230 .with_base_iri(base_iri)
231 .map_err(|e| bad(format!("invalid base IRI: {e}")))?;
232 let mut g = Graph::new();
233 for item in parser.for_slice(body) {
234 let q = item.map_err(|e| bad(format!("N3 syntax error: {e}")))?;
235 if !matches!(q.graph_name, oxrdf::GraphName::DefaultGraph) {
236 return Err(PodError::Unsupported(
237 "N3 formulas (quoted graphs) are not supported".into(),
238 ));
239 }
240 let subject = n3_term(&q.subject, "subject")?;
241 let predicate = n3_term(&q.predicate, "predicate")?;
242 let object = n3_term(&q.object, "object")?;
243 if matches!(subject, Term::Literal { .. }) {
244 return Err(bad("literal subjects are not valid RDF"));
245 }
246 match &predicate {
247 Term::Iri(p) if p == LOG_IMPLIES || p == LOG_IMPLIED_BY => {
248 return Err(PodError::Unsupported(
249 "N3 rules (log:implies) are not supported".into(),
250 ));
251 }
252 Term::Iri(_) => {}
253 _ => return Err(bad("predicate must be an IRI")),
254 }
255 push_checked(&mut g, Triple::new(subject, predicate, object))?;
256 }
257 Ok(g)
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263
264 const BASE: &str = "https://pod.example/data/doc";
265
266 fn nt(g: &Graph) -> String {
267 g.to_ntriples()
268 }
269
270 #[test]
271 fn turtle_resolves_relative_against_request_url() {
272 let g = parse_turtle(b"<> <#p> <../o> .", BASE).unwrap();
273 let t = g.triples().next().unwrap();
274 assert_eq!(t.subject, Term::iri(BASE));
275 assert_eq!(t.predicate, Term::iri("https://pod.example/data/doc#p"));
276 assert_eq!(t.object, Term::iri("https://pod.example/o"));
277 }
278
279 #[test]
280 fn turtle_prefixes_literals_and_lists() {
281 let g = parse_turtle(
282 b"@prefix ex: <http://e/> . ex:s ex:n 1 ; ex:l \"hi\"@en ; ex:t \"x\" ; ex:d \"1\"^^ex:T .",
283 BASE,
284 )
285 .unwrap();
286 let s = nt(&g);
287 assert!(s.contains("<http://e/n> \"1\"^^<http://www.w3.org/2001/XMLSchema#integer>"));
288 assert!(s.contains("\"hi\"@en"));
289 assert!(s.contains("<http://e/t> \"x\" ."));
290 assert!(s.contains("\"1\"^^<http://e/T>"));
291 }
292
293 #[test]
294 fn syntax_error_is_bad_request() {
295 assert!(matches!(
296 parse_turtle(b"<a> <b> .", BASE),
297 Err(PodError::BadRequest(_))
298 ));
299 }
300
301 #[test]
302 fn invalid_utf8_is_bad_request() {
303 assert!(matches!(
304 parse_turtle(&[0xff, 0xfe, 0x20], BASE),
305 Err(PodError::BadRequest(_))
306 ));
307 }
308
309 #[test]
310 fn oversize_body_is_rejected_before_parsing() {
311 let big = vec![b' '; MAX_RDF_INPUT_BYTES + 1];
312 assert!(matches!(
313 parse_turtle(&big, BASE),
314 Err(PodError::PayloadTooLarge(_))
315 ));
316 }
317
318 #[test]
319 fn bad_base_is_bad_request() {
320 assert!(matches!(
321 parse_turtle(b"<a:b> <a:c> <a:d> .", "not an iri"),
322 Err(PodError::BadRequest(_))
323 ));
324 }
325
326 #[test]
327 fn n3_plain_triples_parse() {
328 let g = parse_n3(b"@prefix : <http://e/> . :a :b :c , :d .", BASE).unwrap();
329 assert_eq!(g.len(), 2);
330 }
331
332 #[test]
333 fn n3_formula_is_unsupported() {
334 let r = parse_n3(b"@prefix : <http://e/> . { :a :b :c } :says :x .", BASE);
335 assert!(matches!(r, Err(PodError::Unsupported(_))), "{r:?}");
336 }
337
338 #[test]
339 fn n3_variable_is_unsupported() {
340 let r = parse_n3(b"@prefix : <http://e/> . ?x :b :c .", BASE);
341 assert!(matches!(r, Err(PodError::Unsupported(_))), "{r:?}");
342 }
343
344 #[test]
345 fn n3_rule_is_unsupported() {
346 let r = parse_n3(
347 b"@prefix : <http://e/> . { ?x :a :b } => { ?x :c :d } .",
348 BASE,
349 );
350 assert!(matches!(r, Err(PodError::Unsupported(_))), "{r:?}");
351 let r = parse_n3(b"@prefix : <http://e/> . {} => {} .", BASE);
352 assert!(r.is_err(), "empty-formula rule must be refused: {r:?}");
353 }
354
355 #[test]
356 fn n3_literal_subject_is_bad_request() {
357 let r = parse_n3(b"\"x\" <http://e/p> <http://e/o> .", BASE);
358 assert!(matches!(r, Err(PodError::BadRequest(_))), "{r:?}");
359 }
360
361 #[test]
362 fn content_type_mapping() {
363 assert_eq!(
364 RdfInputSyntax::from_content_type("Text/Turtle;charset=utf-8"),
365 Some(RdfInputSyntax::Turtle)
366 );
367 assert_eq!(
368 RdfInputSyntax::from_content_type("text/n3"),
369 Some(RdfInputSyntax::N3)
370 );
371 assert_eq!(RdfInputSyntax::from_content_type("text/plain"), None);
372 }
373}