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