1use crate::diagnostics::ParseError;
4use crate::vocab;
5use oxrdf::{Graph, NamedNode, NamedNodeRef, NamedOrBlankNode, Term, Triple};
6use oxrdfio::RdfParser;
7use oxttl::{NTriplesParser, TurtleParser};
8use std::collections::HashSet;
9use std::fs::File;
10use std::io::{BufReader, Read};
11use std::path::Path;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum RdfFormat {
15 Turtle,
16 NTriples,
17 RdfXml,
18 NQuads,
19 TriG,
20 N3,
21}
22
23impl RdfFormat {
24 pub fn from_media_type(media_type: &str) -> Option<Self> {
25 match oxrdfio::RdfFormat::from_media_type(media_type)? {
26 oxrdfio::RdfFormat::Turtle => Some(Self::Turtle),
27 oxrdfio::RdfFormat::NTriples => Some(Self::NTriples),
28 oxrdfio::RdfFormat::RdfXml => Some(Self::RdfXml),
29 oxrdfio::RdfFormat::NQuads => Some(Self::NQuads),
30 oxrdfio::RdfFormat::TriG => Some(Self::TriG),
31 oxrdfio::RdfFormat::N3 => Some(Self::N3),
32 _ => None,
33 }
34 }
35
36 pub fn from_extension(extension: &str) -> Option<Self> {
37 match oxrdfio::RdfFormat::from_extension(extension)? {
38 oxrdfio::RdfFormat::Turtle => Some(Self::Turtle),
39 oxrdfio::RdfFormat::NTriples => Some(Self::NTriples),
40 oxrdfio::RdfFormat::RdfXml => Some(Self::RdfXml),
41 oxrdfio::RdfFormat::NQuads => Some(Self::NQuads),
42 oxrdfio::RdfFormat::TriG => Some(Self::TriG),
43 oxrdfio::RdfFormat::N3 => Some(Self::N3),
44 _ => None,
45 }
46 }
47
48 pub fn name(self) -> &'static str {
50 match self {
51 Self::Turtle => "turtle",
52 Self::NTriples => "n-triples",
53 Self::RdfXml => "rdf/xml",
54 Self::NQuads => "n-quads",
55 Self::TriG => "trig",
56 Self::N3 => "n3",
57 }
58 }
59
60 fn to_oxrdfio(self) -> oxrdfio::RdfFormat {
61 match self {
62 Self::Turtle => oxrdfio::RdfFormat::Turtle,
63 Self::NTriples => oxrdfio::RdfFormat::NTriples,
64 Self::RdfXml => oxrdfio::RdfFormat::RdfXml,
65 Self::NQuads => oxrdfio::RdfFormat::NQuads,
66 Self::TriG => oxrdfio::RdfFormat::TriG,
67 Self::N3 => oxrdfio::RdfFormat::N3,
68 }
69 }
70}
71
72impl std::fmt::Display for RdfFormat {
73 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 f.write_str(self.name())
75 }
76}
77
78pub struct Loaded {
80 pub graph: Graph,
81 pub prefixes: Vec<(String, String)>,
82 pub base: Option<String>,
83}
84
85impl Loaded {
86 pub fn from_turtle(data: &[u8], base: Option<&str>) -> Result<Self, ParseError> {
88 Self::from_turtle_slice(data, base)
89 }
90
91 pub fn from_ntriples(data: &[u8]) -> Result<Self, ParseError> {
92 Self::from_ntriples_slice(data)
93 }
94
95 pub fn from_rdf(
96 data: &[u8],
97 format: RdfFormat,
98 base: Option<&str>,
99 ) -> Result<Self, ParseError> {
100 match format {
101 RdfFormat::Turtle => Self::from_turtle_slice(data, base),
102 RdfFormat::NTriples => Self::from_ntriples_slice(data),
103 _ => Self::from_oxrdfio(data, format, base),
104 }
105 }
106
107 pub fn from_rdf_auto(
108 data: &[u8],
109 content_type: Option<&str>,
110 source: Option<&str>,
111 base: Option<&str>,
112 ) -> Result<Self, ParseError> {
113 Self::from_rdf_auto_reporting(data, content_type, source, base).map(|(loaded, _)| loaded)
114 }
115
116 pub fn from_rdf_auto_reporting(
123 data: &[u8],
124 content_type: Option<&str>,
125 source: Option<&str>,
126 base: Option<&str>,
127 ) -> Result<(Self, RdfFormat), ParseError> {
128 let sniffed = sniff_format(data);
129 let hinted = content_type
130 .and_then(RdfFormat::from_media_type)
131 .or_else(|| source.and_then(format_from_source))
132 .or(sniffed);
133
134 if let Some(format) = hinted {
135 match Self::from_rdf(data, format, base) {
136 Ok(loaded) => return Ok((loaded, format)),
137 Err(first_error)
138 if content_type.is_some() || source.is_some() || sniffed.is_some() =>
139 {
140 return Err(first_error);
141 }
142 Err(_) => {}
143 }
144 }
145
146 let mut errors = Vec::new();
147 for format in [
148 RdfFormat::Turtle,
149 RdfFormat::RdfXml,
150 RdfFormat::NTriples,
151 RdfFormat::NQuads,
152 RdfFormat::TriG,
153 RdfFormat::N3,
154 ] {
155 match Self::from_rdf(data, format, base) {
156 Ok(loaded) => return Ok((loaded, format)),
157 Err(e) => errors.push(format!("{format:?}: {e}")),
158 }
159 }
160 Err(ParseError(format!(
161 "failed to parse RDF using common formats: {}",
162 errors.join("; ")
163 )))
164 }
165
166 pub fn from_path(
167 path: &Path,
168 format: RdfFormat,
169 base: Option<&str>,
170 ) -> Result<Self, ParseError> {
171 let file = File::open(path)
172 .map_err(|e| ParseError(format!("failed to open {}: {e}", path.display())))?;
173 let reader = BufReader::new(file);
174 match format {
175 RdfFormat::Turtle => Self::from_turtle_reader(reader, base),
176 RdfFormat::NTriples => Self::from_ntriples_reader(reader),
177 _ => {
178 let mut bytes = Vec::new();
179 let mut reader = reader;
180 reader
181 .read_to_end(&mut bytes)
182 .map_err(|e| ParseError(format!("failed to read RDF input: {e}")))?;
183 Self::from_oxrdfio(&bytes, format, base)
184 }
185 }
186 }
187
188 fn from_turtle_slice(data: &[u8], base: Option<&str>) -> Result<Self, ParseError> {
195 let mut parser = TurtleParser::new();
196 if let Some(b) = base {
197 parser = parser
198 .with_base_iri(b)
199 .map_err(|e| ParseError(format!("invalid base IRI: {e}")))?;
200 }
201 let mut reader = parser.for_slice(data);
202 let mut graph = Graph::new();
203 for triple in reader.by_ref() {
204 let triple = triple.map_err(|e| ParseError(format!("turtle syntax error: {e}")))?;
205 graph.insert(&triple);
206 }
207 let prefixes = reader
208 .prefixes()
209 .map(|(p, iri)| (p.to_string(), iri.to_string()))
210 .collect();
211 let base = reader.base_iri().map(|s| s.to_string());
212 Ok(Self {
213 graph,
214 prefixes,
215 base,
216 })
217 }
218
219 fn from_ntriples_slice(data: &[u8]) -> Result<Self, ParseError> {
221 let mut graph = Graph::new();
222 for triple in NTriplesParser::new().for_slice(data) {
223 let triple = triple.map_err(|e| ParseError(format!("N-Triples syntax error: {e}")))?;
224 graph.insert(&triple);
225 }
226 Ok(Self {
227 graph,
228 prefixes: Vec::new(),
229 base: None,
230 })
231 }
232
233 fn from_turtle_reader(reader: impl Read, base: Option<&str>) -> Result<Self, ParseError> {
234 let mut parser = TurtleParser::new();
235 if let Some(b) = base {
236 parser = parser
237 .with_base_iri(b)
238 .map_err(|e| ParseError(format!("invalid base IRI: {e}")))?;
239 }
240 let mut reader = parser.for_reader(reader);
241 let mut graph = Graph::new();
242 for triple in reader.by_ref() {
243 let triple = triple.map_err(|e| ParseError(format!("turtle syntax error: {e}")))?;
244 graph.insert(&triple);
245 }
246 let prefixes = reader
247 .prefixes()
248 .map(|(p, iri)| (p.to_string(), iri.to_string()))
249 .collect();
250 let base = reader.base_iri().map(|s| s.to_string());
251 Ok(Self {
252 graph,
253 prefixes,
254 base,
255 })
256 }
257
258 fn from_ntriples_reader(reader: impl Read) -> Result<Self, ParseError> {
259 let mut graph = Graph::new();
260 for triple in NTriplesParser::new().for_reader(reader) {
261 let triple = triple.map_err(|e| ParseError(format!("N-Triples syntax error: {e}")))?;
262 graph.insert(&triple);
263 }
264 Ok(Self {
265 graph,
266 prefixes: Vec::new(),
267 base: None,
268 })
269 }
270
271 fn from_oxrdfio(
272 data: &[u8],
273 format: RdfFormat,
274 base: Option<&str>,
275 ) -> Result<Self, ParseError> {
276 let mut parser = RdfParser::from_format(format.to_oxrdfio()).without_named_graphs();
277 if let Some(base) = base {
278 parser = parser
279 .with_base_iri(base)
280 .map_err(|e| ParseError(format!("invalid base IRI: {e}")))?;
281 }
282 let mut graph = Graph::new();
283 for quad in parser.for_slice(data) {
284 let quad = quad.map_err(|e| ParseError(format!("{format:?} syntax error: {e}")))?;
285 graph.insert(&Triple {
286 subject: quad.subject,
287 predicate: quad.predicate,
288 object: quad.object,
289 });
290 }
291 Ok(Self {
292 graph,
293 prefixes: Vec::new(),
294 base: base.map(ToOwned::to_owned),
295 })
296 }
297
298 pub fn objects(&self, subject: &NamedOrBlankNode, predicate: NamedNodeRef) -> Vec<Term> {
300 self.graph
301 .objects_for_subject_predicate(subject, predicate)
302 .map(|t| t.into_owned())
303 .collect()
304 }
305
306 pub fn object(&self, subject: &NamedOrBlankNode, predicate: NamedNodeRef) -> Option<Term> {
308 self.graph
309 .object_for_subject_predicate(subject, predicate)
310 .map(|t| t.into_owned())
311 }
312
313 pub fn has_type(&self, subject: &NamedOrBlankNode, ty: NamedNodeRef) -> bool {
315 self.objects(subject, vocab::RDF_TYPE)
316 .iter()
317 .any(|t| matches!(t, Term::NamedNode(n) if n.as_ref() == ty))
318 }
319
320 pub fn is_instance_of(&self, subject: &NamedOrBlankNode, ty: NamedNodeRef) -> bool {
322 let mut pending: Vec<NamedNode> = self
323 .objects(subject, vocab::RDF_TYPE)
324 .into_iter()
325 .filter_map(|term| match term {
326 Term::NamedNode(node) => Some(node),
327 _ => None,
328 })
329 .collect();
330 let mut seen = HashSet::new();
331 while let Some(class) = pending.pop() {
332 if class.as_ref() == ty {
333 return true;
334 }
335 if !seen.insert(class.clone()) {
336 continue;
337 }
338 pending.extend(
339 self.objects(&NamedOrBlankNode::NamedNode(class), vocab::RDFS_SUBCLASSOF)
340 .into_iter()
341 .filter_map(|term| match term {
342 Term::NamedNode(node) => Some(node),
343 _ => None,
344 }),
345 );
346 }
347 false
348 }
349
350 pub fn merge_from(&mut self, other: &Loaded) {
352 for triple in other.graph.iter() {
353 self.graph.insert(triple);
354 }
355 }
356
357 pub fn read_list(&self, head: &Term) -> Vec<Term> {
359 let mut out = Vec::new();
360 let mut cursor = head.clone();
361 while let Some(node) = term_to_node(&cursor) {
362 if is_nil(&cursor) {
363 break;
364 }
365 if let Some(first) = self.object(&node, vocab::RDF_FIRST) {
366 out.push(first);
367 }
368 match self.object(&node, vocab::RDF_REST) {
369 Some(rest) => cursor = rest,
370 None => break,
371 }
372 }
373 out
374 }
375}
376
377fn format_from_source(source: &str) -> Option<RdfFormat> {
378 let path = source.split(['?', '#']).next().unwrap_or(source);
379 let extension = path.rsplit_once('.')?.1;
380 RdfFormat::from_extension(extension)
381}
382
383fn sniff_format(data: &[u8]) -> Option<RdfFormat> {
384 let prefix = &data[..data.len().min(4096)];
386 let text = std::str::from_utf8(prefix).ok()?;
387
388 let first_token = text
390 .lines()
391 .map(|l| l.trim_start())
392 .find(|l| !l.is_empty() && !l.starts_with('#'))?;
393
394 if first_token.starts_with("<?xml") || first_token.starts_with("<rdf:RDF") {
395 return Some(RdfFormat::RdfXml);
396 }
397 if first_token.starts_with("@prefix")
398 || first_token.starts_with("@base")
399 || first_token.starts_with("PREFIX")
400 || first_token.starts_with("BASE")
401 {
402 return Some(RdfFormat::Turtle);
403 }
404 if first_token.starts_with('<') && first_token.contains("> <") {
406 return Some(RdfFormat::NTriples);
407 }
408 None
409}
410
411pub fn term_to_node(term: &Term) -> Option<NamedOrBlankNode> {
413 match term {
414 Term::NamedNode(n) => Some(NamedOrBlankNode::NamedNode(n.clone())),
415 Term::BlankNode(b) => Some(NamedOrBlankNode::BlankNode(b.clone())),
416 Term::Literal(_) => None,
417 }
418}
419
420pub fn is_nil(term: &Term) -> bool {
422 matches!(term, Term::NamedNode(n) if n.as_ref() == vocab::RDF_NIL)
423}
424
425pub fn as_named(term: &Term) -> Option<NamedNode> {
427 match term {
428 Term::NamedNode(n) => Some(n.clone()),
429 _ => None,
430 }
431}
432
433#[cfg(test)]
434mod tests {
435 use super::*;
436
437 #[test]
440 fn sniff_rdfxml_xml_declaration() {
441 assert_eq!(
442 sniff_format(b"<?xml version=\"1.0\"?>\n<rdf:RDF>"),
443 Some(RdfFormat::RdfXml)
444 );
445 }
446
447 #[test]
448 fn sniff_rdfxml_bare_tag() {
449 assert_eq!(
450 sniff_format(b"<rdf:RDF xmlns:rdf=\"...\">"),
451 Some(RdfFormat::RdfXml)
452 );
453 }
454
455 #[test]
456 fn sniff_turtle_at_prefix() {
457 assert_eq!(
458 sniff_format(b"@prefix sh: <http://www.w3.org/ns/shacl#> ."),
459 Some(RdfFormat::Turtle)
460 );
461 }
462
463 #[test]
464 fn sniff_turtle_at_base() {
465 assert_eq!(
466 sniff_format(b"@base <http://example.org/> ."),
467 Some(RdfFormat::Turtle)
468 );
469 }
470
471 #[test]
472 fn sniff_turtle_sparql_prefix() {
473 assert_eq!(
474 sniff_format(b"PREFIX sh: <http://www.w3.org/ns/shacl#>"),
475 Some(RdfFormat::Turtle)
476 );
477 }
478
479 #[test]
480 fn sniff_turtle_sparql_base() {
481 assert_eq!(
482 sniff_format(b"BASE <http://example.org/>"),
483 Some(RdfFormat::Turtle)
484 );
485 }
486
487 #[test]
488 fn sniff_turtle_after_comment_lines() {
489 let data = b"# Copyright 2024\n# Licensed under ...\n@prefix ex: <http://ex/> .";
490 assert_eq!(sniff_format(data), Some(RdfFormat::Turtle));
491 }
492
493 #[test]
494 fn sniff_turtle_after_blank_and_comment_lines() {
495 let data = b"\n\n# A comment\n\nPREFIX ex: <http://ex/>";
496 assert_eq!(sniff_format(data), Some(RdfFormat::Turtle));
497 }
498
499 #[test]
500 fn sniff_ntriples_first_line() {
501 let data = b"<http://ex/s> <http://ex/p> <http://ex/o> .\n";
502 assert_eq!(sniff_format(data), Some(RdfFormat::NTriples));
503 }
504
505 #[test]
506 fn sniff_ntriples_after_comment() {
507 let data = b"# generated by riot\n<http://ex/s> <http://ex/p> \"value\" .\n";
508 assert_eq!(sniff_format(data), Some(RdfFormat::NTriples));
510 }
511
512 #[test]
513 fn sniff_ntriples_contains_check_uses_first_line_only() {
514 let data =
517 b"# comment\n_:b0 <http://ex/p> _:b1 .\n<http://ex/s> <http://ex/p> <http://ex/o> .\n";
518 assert_eq!(sniff_format(data), None);
519 }
520
521 #[test]
522 fn sniff_returns_none_for_unknown() {
523 assert_eq!(sniff_format(b"SELECT ?s WHERE { ?s a ex:Thing }"), None);
524 }
525
526 #[test]
527 fn sniff_only_reads_prefix() {
528 let mut data = b"@prefix ex: <http://ex/> .\n".to_vec();
530 data.extend(std::iter::repeat_n(b'x', 8000));
531 assert_eq!(sniff_format(&data), Some(RdfFormat::Turtle));
532 }
533
534 #[test]
535 fn sniff_ignores_format_marker_beyond_prefix_window() {
536 let mut data = vec![b' '; 5000];
538 data.extend_from_slice(b"@prefix ex: <http://ex/> .");
539 assert_eq!(sniff_format(&data), None);
540 }
541}