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_reader(data, base)
71 }
72
73 pub fn from_ntriples(data: &[u8]) -> Result<Self, ParseError> {
74 Self::from_ntriples_reader(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_reader(data, base),
84 RdfFormat::NTriples => Self::from_ntriples_reader(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_reader(reader: impl Read, base: Option<&str>) -> Result<Self, ParseError> {
156 let mut parser = TurtleParser::new();
157 if let Some(b) = base {
158 parser = parser
159 .with_base_iri(b)
160 .map_err(|e| ParseError(format!("invalid base IRI: {e}")))?;
161 }
162 let mut reader = parser.for_reader(reader);
163 let mut graph = Graph::new();
164 for triple in reader.by_ref() {
165 let triple = triple.map_err(|e| ParseError(format!("turtle syntax error: {e}")))?;
166 graph.insert(&triple);
167 }
168 let prefixes = reader
169 .prefixes()
170 .map(|(p, iri)| (p.to_string(), iri.to_string()))
171 .collect();
172 let base = reader.base_iri().map(|s| s.to_string());
173 Ok(Self {
174 graph,
175 prefixes,
176 base,
177 })
178 }
179
180 fn from_ntriples_reader(reader: impl Read) -> Result<Self, ParseError> {
181 let mut graph = Graph::new();
182 for triple in NTriplesParser::new().for_reader(reader) {
183 let triple = triple.map_err(|e| ParseError(format!("N-Triples syntax error: {e}")))?;
184 graph.insert(&triple);
185 }
186 Ok(Self {
187 graph,
188 prefixes: Vec::new(),
189 base: None,
190 })
191 }
192
193 fn from_oxrdfio(
194 data: &[u8],
195 format: RdfFormat,
196 base: Option<&str>,
197 ) -> Result<Self, ParseError> {
198 let mut parser = RdfParser::from_format(format.to_oxrdfio()).without_named_graphs();
199 if let Some(base) = base {
200 parser = parser
201 .with_base_iri(base)
202 .map_err(|e| ParseError(format!("invalid base IRI: {e}")))?;
203 }
204 let mut graph = Graph::new();
205 for quad in parser.for_slice(data) {
206 let quad = quad.map_err(|e| ParseError(format!("{format:?} syntax error: {e}")))?;
207 graph.insert(&Triple {
208 subject: quad.subject,
209 predicate: quad.predicate,
210 object: quad.object,
211 });
212 }
213 Ok(Self {
214 graph,
215 prefixes: Vec::new(),
216 base: base.map(ToOwned::to_owned),
217 })
218 }
219
220 pub fn objects(&self, subject: &NamedOrBlankNode, predicate: NamedNodeRef) -> Vec<Term> {
222 self.graph
223 .objects_for_subject_predicate(subject, predicate)
224 .map(|t| t.into_owned())
225 .collect()
226 }
227
228 pub fn object(&self, subject: &NamedOrBlankNode, predicate: NamedNodeRef) -> Option<Term> {
230 self.graph
231 .object_for_subject_predicate(subject, predicate)
232 .map(|t| t.into_owned())
233 }
234
235 pub fn has_type(&self, subject: &NamedOrBlankNode, ty: NamedNodeRef) -> bool {
237 self.objects(subject, vocab::RDF_TYPE)
238 .iter()
239 .any(|t| matches!(t, Term::NamedNode(n) if n.as_ref() == ty))
240 }
241
242 pub fn is_instance_of(&self, subject: &NamedOrBlankNode, ty: NamedNodeRef) -> bool {
244 let mut pending: Vec<NamedNode> = self
245 .objects(subject, vocab::RDF_TYPE)
246 .into_iter()
247 .filter_map(|term| match term {
248 Term::NamedNode(node) => Some(node),
249 _ => None,
250 })
251 .collect();
252 let mut seen = HashSet::new();
253 while let Some(class) = pending.pop() {
254 if class.as_ref() == ty {
255 return true;
256 }
257 if !seen.insert(class.clone()) {
258 continue;
259 }
260 pending.extend(
261 self.objects(&NamedOrBlankNode::NamedNode(class), vocab::RDFS_SUBCLASSOF)
262 .into_iter()
263 .filter_map(|term| match term {
264 Term::NamedNode(node) => Some(node),
265 _ => None,
266 }),
267 );
268 }
269 false
270 }
271
272 pub fn merge_from(&mut self, other: &Loaded) {
274 for triple in other.graph.iter() {
275 self.graph.insert(triple);
276 }
277 }
278
279 pub fn read_list(&self, head: &Term) -> Vec<Term> {
281 let mut out = Vec::new();
282 let mut cursor = head.clone();
283 while let Some(node) = term_to_node(&cursor) {
284 if is_nil(&cursor) {
285 break;
286 }
287 if let Some(first) = self.object(&node, vocab::RDF_FIRST) {
288 out.push(first);
289 }
290 match self.object(&node, vocab::RDF_REST) {
291 Some(rest) => cursor = rest,
292 None => break,
293 }
294 }
295 out
296 }
297}
298
299fn format_from_source(source: &str) -> Option<RdfFormat> {
300 let path = source.split(['?', '#']).next().unwrap_or(source);
301 let extension = path.rsplit_once('.')?.1;
302 RdfFormat::from_extension(extension)
303}
304
305fn sniff_format(data: &[u8]) -> Option<RdfFormat> {
306 let prefix = &data[..data.len().min(4096)];
308 let text = std::str::from_utf8(prefix).ok()?;
309
310 let first_token = text
312 .lines()
313 .map(|l| l.trim_start())
314 .find(|l| !l.is_empty() && !l.starts_with('#'))?;
315
316 if first_token.starts_with("<?xml") || first_token.starts_with("<rdf:RDF") {
317 return Some(RdfFormat::RdfXml);
318 }
319 if first_token.starts_with("@prefix")
320 || first_token.starts_with("@base")
321 || first_token.starts_with("PREFIX")
322 || first_token.starts_with("BASE")
323 {
324 return Some(RdfFormat::Turtle);
325 }
326 if first_token.starts_with('<') && first_token.contains("> <") {
328 return Some(RdfFormat::NTriples);
329 }
330 None
331}
332
333pub fn term_to_node(term: &Term) -> Option<NamedOrBlankNode> {
335 match term {
336 Term::NamedNode(n) => Some(NamedOrBlankNode::NamedNode(n.clone())),
337 Term::BlankNode(b) => Some(NamedOrBlankNode::BlankNode(b.clone())),
338 Term::Literal(_) => None,
339 }
340}
341
342pub fn is_nil(term: &Term) -> bool {
344 matches!(term, Term::NamedNode(n) if n.as_ref() == vocab::RDF_NIL)
345}
346
347pub fn as_named(term: &Term) -> Option<NamedNode> {
349 match term {
350 Term::NamedNode(n) => Some(n.clone()),
351 _ => None,
352 }
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358
359 #[test]
362 fn sniff_rdfxml_xml_declaration() {
363 assert_eq!(
364 sniff_format(b"<?xml version=\"1.0\"?>\n<rdf:RDF>"),
365 Some(RdfFormat::RdfXml)
366 );
367 }
368
369 #[test]
370 fn sniff_rdfxml_bare_tag() {
371 assert_eq!(
372 sniff_format(b"<rdf:RDF xmlns:rdf=\"...\">"),
373 Some(RdfFormat::RdfXml)
374 );
375 }
376
377 #[test]
378 fn sniff_turtle_at_prefix() {
379 assert_eq!(
380 sniff_format(b"@prefix sh: <http://www.w3.org/ns/shacl#> ."),
381 Some(RdfFormat::Turtle)
382 );
383 }
384
385 #[test]
386 fn sniff_turtle_at_base() {
387 assert_eq!(
388 sniff_format(b"@base <http://example.org/> ."),
389 Some(RdfFormat::Turtle)
390 );
391 }
392
393 #[test]
394 fn sniff_turtle_sparql_prefix() {
395 assert_eq!(
396 sniff_format(b"PREFIX sh: <http://www.w3.org/ns/shacl#>"),
397 Some(RdfFormat::Turtle)
398 );
399 }
400
401 #[test]
402 fn sniff_turtle_sparql_base() {
403 assert_eq!(
404 sniff_format(b"BASE <http://example.org/>"),
405 Some(RdfFormat::Turtle)
406 );
407 }
408
409 #[test]
410 fn sniff_turtle_after_comment_lines() {
411 let data = b"# Copyright 2024\n# Licensed under ...\n@prefix ex: <http://ex/> .";
412 assert_eq!(sniff_format(data), Some(RdfFormat::Turtle));
413 }
414
415 #[test]
416 fn sniff_turtle_after_blank_and_comment_lines() {
417 let data = b"\n\n# A comment\n\nPREFIX ex: <http://ex/>";
418 assert_eq!(sniff_format(data), Some(RdfFormat::Turtle));
419 }
420
421 #[test]
422 fn sniff_ntriples_first_line() {
423 let data = b"<http://ex/s> <http://ex/p> <http://ex/o> .\n";
424 assert_eq!(sniff_format(data), Some(RdfFormat::NTriples));
425 }
426
427 #[test]
428 fn sniff_ntriples_after_comment() {
429 let data = b"# generated by riot\n<http://ex/s> <http://ex/p> \"value\" .\n";
430 assert_eq!(sniff_format(data), Some(RdfFormat::NTriples));
432 }
433
434 #[test]
435 fn sniff_ntriples_contains_check_uses_first_line_only() {
436 let data =
439 b"# comment\n_:b0 <http://ex/p> _:b1 .\n<http://ex/s> <http://ex/p> <http://ex/o> .\n";
440 assert_eq!(sniff_format(data), None);
441 }
442
443 #[test]
444 fn sniff_returns_none_for_unknown() {
445 assert_eq!(sniff_format(b"SELECT ?s WHERE { ?s a ex:Thing }"), None);
446 }
447
448 #[test]
449 fn sniff_only_reads_prefix() {
450 let mut data = b"@prefix ex: <http://ex/> .\n".to_vec();
452 data.extend(std::iter::repeat_n(b'x', 8000));
453 assert_eq!(sniff_format(&data), Some(RdfFormat::Turtle));
454 }
455
456 #[test]
457 fn sniff_ignores_format_marker_beyond_prefix_window() {
458 let mut data = vec![b' '; 5000];
460 data.extend_from_slice(b"@prefix ex: <http://ex/> .");
461 assert_eq!(sniff_format(&data), None);
462 }
463}