diff --git a/.clusterfuzzlite/build.sh b/.clusterfuzzlite/build.sh index 698dd187..44671ce3 100755 --- a/.clusterfuzzlite/build.sh +++ b/.clusterfuzzlite/build.sh @@ -15,7 +15,7 @@ function build_seed_corpus() { cd "$SRC"/oxigraph cargo fuzz build -O --debug-assertions -for TARGET in sparql_eval sparql_results_json sparql_results_tsv n3 nquads trig # sparql_results_xml https://github.com/tafia/quick-xml/issues/608 +for TARGET in sparql_eval sparql_results_json sparql_results_tsv sparql_results_xml n3 nquads trig rdf_xml do cp fuzz/target/x86_64-unknown-linux-gnu/release/$TARGET "$OUT"/ done @@ -25,4 +25,4 @@ build_seed_corpus sparql_results_xml srx build_seed_corpus n3 n3 build_seed_corpus nquads nq build_seed_corpus trig trig - +build_seed_corpus rdf_xml rdf diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 106fba84..7098e3fd 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -32,6 +32,8 @@ jobs: working-directory: ./lib/oxsdatatypes - run: cargo clippy working-directory: ./lib/oxrdf + - run: cargo clippy + working-directory: ./lib/oxrdfxml - run: cargo clippy working-directory: ./lib/oxttl - run: cargo clippy @@ -76,6 +78,8 @@ jobs: working-directory: ./lib/oxsdatatypes - run: cargo clippy -- -D warnings -D clippy::all working-directory: ./lib/oxrdf + - run: cargo clippy -- -D warnings -D clippy::all + working-directory: ./lib/oxrdfxml - run: cargo clippy -- -D warnings -D clippy::all working-directory: ./lib/oxttl - run: cargo clippy -- -D warnings -D clippy::all @@ -127,7 +131,7 @@ jobs: - run: rustup update - uses: Swatinem/rust-cache@v2 - run: cargo install cargo-semver-checks || true - - run: cargo semver-checks check-release --exclude oxrocksdb-sys --exclude oxigraph_js --exclude pyoxigraph --exclude oxigraph_testsuite --exclude oxigraph_server --exclude oxttl --exclude sparopt + - run: cargo semver-checks check-release --exclude oxrocksdb-sys --exclude oxigraph_js --exclude pyoxigraph --exclude oxigraph_testsuite --exclude oxigraph_server --exclude oxrdfxml --exclude oxttl --exclude sparopt test_linux: runs-on: ubuntu-latest diff --git a/Cargo.lock b/Cargo.lock index affef9d4..d2568bac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -946,13 +946,12 @@ dependencies = [ "oxilangtag", "oxiri", "oxrdf", + "oxrdfxml", "oxrocksdb-sys", "oxsdatatypes", "oxttl", "rand", "regex", - "rio_api", - "rio_xml", "sha-1", "sha2", "siphasher", @@ -1031,6 +1030,16 @@ dependencies = [ "rand", ] +[[package]] +name = "oxrdfxml" +version = "0.1.0-alpha.1-dev" +dependencies = [ + "oxilangtag", + "oxiri", + "oxrdf", + "quick-xml", +] + [[package]] name = "oxrocksdb-sys" version = "0.4.0-alpha.1-dev" @@ -1279,9 +1288,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.28.2" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce5e73202a820a31f8a0ee32ada5e21029c81fd9e3ebf668a40832e4219d9d1" +checksum = "81b9228215d82c7b61490fec1de287136b5de6f5700f6e58ea9ad61a7964ca51" dependencies = [ "memchr", ] @@ -1411,18 +1420,6 @@ dependencies = [ "rio_api", ] -[[package]] -name = "rio_xml" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2edda57b877119dc326c612ba822e3ca1ee22bfc86781a4e9dc0884756b58c3" -dependencies = [ - "oxilangtag", - "oxiri", - "quick-xml", - "rio_api", -] - [[package]] name = "rustc-hash" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index 0da9b1c9..0ad8536d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "js", "lib", "lib/oxrdf", + "lib/oxrdfxml", "lib/oxsdatatypes", "lib/oxttl", "lib/spargebra", diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 216ba902..8bef5528 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -13,6 +13,7 @@ anyhow = "1" lazy_static = "1" libfuzzer-sys = "0.4" oxttl = { path = "../lib/oxttl", features = ["rdf-star"] } +oxrdfxml = { path = "../lib/oxrdfxml" } spargebra = { path = "../lib/spargebra", features = ["rdf-star", "sep-0006"] } sparesults = { path = "../lib/sparesults", features = ["rdf-star"] } sparql-smith = { path = "../lib/sparql-smith", features = ["sep-0006"] } @@ -32,6 +33,10 @@ path = "fuzz_targets/nquads.rs" name = "n3" path = "fuzz_targets/n3.rs" +[[bin]] +name = "rdf_xml" +path = "fuzz_targets/rdf_xml.rs" + [[bin]] name = "sparql_eval" path = "fuzz_targets/sparql_eval.rs" diff --git a/fuzz/fuzz_targets/rdf_xml.rs b/fuzz/fuzz_targets/rdf_xml.rs new file mode 100644 index 00000000..ae0cb6b1 --- /dev/null +++ b/fuzz/fuzz_targets/rdf_xml.rs @@ -0,0 +1,37 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +use oxrdfxml::{RdfXmlParser, RdfXmlSerializer}; + +fuzz_target!(|data: &[u8]| { + // We parse + let mut triples = Vec::new(); + for triple in RdfXmlParser::new().parse_from_read(data) { + if let Ok(triple) = triple { + triples.push(triple); + } + } + + // We serialize + let mut writer = RdfXmlSerializer::new().serialize_to_write(Vec::new()); + for triple in &triples { + writer.write_triple(triple).unwrap(); + } + let new_serialization = writer.finish().unwrap(); + + // We parse the serialization + let new_triples = RdfXmlParser::new() + .parse_from_read(new_serialization.as_slice()) + .collect::, _>>() + .map_err(|e| { + format!( + "Error on {:?} from {triples:?} based on {:?}: {e}", + String::from_utf8_lossy(&new_serialization), + String::from_utf8_lossy(data) + ) + }) + .unwrap(); + + // We check the roundtrip has not changed anything + assert_eq!(new_triples, triples); +}); diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 9c3fb465..f4d3cae1 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -31,13 +31,12 @@ digest = "0.10" regex = "1" oxilangtag = "0.1" oxiri = "0.2" -rio_api = "0.8" -rio_xml = "0.8" hex = "0.4" siphasher = "0.3" lazy_static = "1" json-event-parser = "0.1" oxrdf = { version = "0.2.0-alpha.1-dev", path = "oxrdf", features = ["rdf-star", "oxsdatatypes"] } +oxrdfxml = { version = "0.1.0-alpha.1-dev", path = "oxrdfxml" } oxsdatatypes = { version = "0.2.0-alpha.1-dev", path="oxsdatatypes" } oxttl = { version = "0.1.0-alpha.1-dev" , path = "oxttl", features = ["rdf-star"] } spargebra = { version = "0.3.0-alpha.1-dev", path = "spargebra", features = ["rdf-star", "sep-0002", "sep-0006"] } diff --git a/lib/oxrdf/src/blank_node.rs b/lib/oxrdf/src/blank_node.rs index 0b485beb..bfd27231 100644 --- a/lib/oxrdf/src/blank_node.rs +++ b/lib/oxrdf/src/blank_node.rs @@ -111,7 +111,14 @@ impl Default for BlankNode { /// Builds a new RDF [blank node](https://www.w3.org/TR/rdf11-concepts/#dfn-blank-node) with a unique id. #[inline] fn default() -> Self { - Self::new_from_unique_id(random::()) + // We ensure the ID does not start with a number to be also valid with RDF/XML + loop { + let id = random(); + let str = IdStr::new(id); + if matches!(str.as_str().as_bytes().first(), Some(b'a'..=b'f')) { + return Self(BlankNodeContent::Anonymous { id, str }); + } + } } } diff --git a/lib/oxrdfxml/Cargo.toml b/lib/oxrdfxml/Cargo.toml new file mode 100644 index 00000000..25e19c72 --- /dev/null +++ b/lib/oxrdfxml/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "oxrdfxml" +version = "0.1.0-alpha.1-dev" +authors = ["Tpt "] +license = "MIT OR Apache-2.0" +readme = "README.md" +keywords = ["RDF/XML", "RDF"] +repository = "https://github.com/oxigraph/oxigraph/tree/master/lib/oxrdfxml" +homepage = "https://oxigraph.org/" +description = """ +Parser for the RDF/XML language +""" +edition = "2021" +rust-version = "1.65" + +[dependencies] +oxrdf = { version = "0.2.0-alpha.1-dev", path = "../oxrdf" } +oxilangtag = "0.1" +oxiri = "0.2" +quick-xml = "0.29" + +[package.metadata.docs.rs] +all-features = true diff --git a/lib/oxrdfxml/README.md b/lib/oxrdfxml/README.md new file mode 100644 index 00000000..66f9f563 --- /dev/null +++ b/lib/oxrdfxml/README.md @@ -0,0 +1,52 @@ +OxRDF/XML +========= + +[![Latest Version](https://img.shields.io/crates/v/oxrdfxml.svg)](https://crates.io/crates/oxrdfxml) +[![Released API docs](https://docs.rs/oxrdfxml/badge.svg)](https://docs.rs/oxrdfxml) +[![Crates.io downloads](https://img.shields.io/crates/d/oxrdfxml)](https://crates.io/crates/oxrdfxml) +[![actions status](https://github.com/oxigraph/oxigraph/workflows/build/badge.svg)](https://github.com/oxigraph/oxigraph/actions) +[![Gitter](https://badges.gitter.im/oxigraph/community.svg)](https://gitter.im/oxigraph/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) + +OxRdfXml is a parser and serializer for [RDF/XML](https://www.w3.org/TR/rdf-syntax-grammar/). + +Usage example counting the number of people in a RDF/XML file: +```rust +use oxrdf::{NamedNodeRef, vocab::rdf}; +use oxrdfxml::RdfXmlParser; + +let file = b" + + + + Foo + + +"; + +let schema_person = NamedNodeRef::new("http://schema.org/Person").unwrap(); +let mut count = 0; +for triple in RdfXmlParser::new().parse_from_read(file.as_ref()) { + let triple = triple.unwrap(); + if triple.predicate == rdf::TYPE && triple.object == schema_person.into() { + count += 1; + } +} +assert_eq!(2, count); +``` + + +## License + +This project is licensed under either of + +* Apache License, Version 2.0, ([LICENSE-APACHE](../LICENSE-APACHE) or + ``) +* MIT license ([LICENSE-MIT](../LICENSE-MIT) or + ``) + +at your option. + + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in Oxigraph by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/lib/oxrdfxml/src/error.rs b/lib/oxrdfxml/src/error.rs new file mode 100644 index 00000000..413d3c4a --- /dev/null +++ b/lib/oxrdfxml/src/error.rs @@ -0,0 +1,107 @@ +use oxilangtag::LanguageTagParseError; +use oxiri::IriParseError; +use std::error::Error; +use std::sync::Arc; +use std::{fmt, io}; + +/// Error that might be returned during parsing. +/// +/// It might wrap an IO error or be a parsing error. +#[derive(Debug)] +pub struct RdfXmlError { + pub(crate) kind: RdfXmlErrorKind, +} + +#[derive(Debug)] +pub(crate) enum RdfXmlErrorKind { + Xml(quick_xml::Error), + XmlAttribute(quick_xml::events::attributes::AttrError), + InvalidIri { + iri: String, + error: IriParseError, + }, + InvalidLanguageTag { + tag: String, + error: LanguageTagParseError, + }, + Other(String), +} + +impl RdfXmlError { + pub(crate) fn msg(msg: impl Into) -> Self { + Self { + kind: RdfXmlErrorKind::Other(msg.into()), + } + } +} + +impl fmt::Display for RdfXmlError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.kind { + RdfXmlErrorKind::Xml(error) => error.fmt(f), + RdfXmlErrorKind::XmlAttribute(error) => error.fmt(f), + RdfXmlErrorKind::InvalidIri { iri, error } => { + write!(f, "error while parsing IRI '{}': {}", iri, error) + } + RdfXmlErrorKind::InvalidLanguageTag { tag, error } => { + write!(f, "error while parsing language tag '{}': {}", tag, error) + } + RdfXmlErrorKind::Other(message) => write!(f, "{}", message), + } + } +} + +impl Error for RdfXmlError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match &self.kind { + RdfXmlErrorKind::Xml(error) => Some(error), + RdfXmlErrorKind::XmlAttribute(error) => Some(error), + RdfXmlErrorKind::InvalidIri { error, .. } => Some(error), + RdfXmlErrorKind::InvalidLanguageTag { error, .. } => Some(error), + RdfXmlErrorKind::Other(_) => None, + } + } +} + +impl From for RdfXmlError { + fn from(error: quick_xml::Error) -> Self { + Self { + kind: RdfXmlErrorKind::Xml(error), + } + } +} + +impl From for RdfXmlError { + fn from(error: quick_xml::events::attributes::AttrError) -> Self { + Self { + kind: RdfXmlErrorKind::XmlAttribute(error), + } + } +} + +impl From for RdfXmlError { + fn from(error: io::Error) -> Self { + Self { + kind: RdfXmlErrorKind::Xml(quick_xml::Error::Io(Arc::new(error))), + } + } +} + +impl From for io::Error { + fn from(error: RdfXmlError) -> Self { + match error.kind { + RdfXmlErrorKind::Xml(error) => match error { + quick_xml::Error::Io(error) => match Arc::try_unwrap(error) { + Ok(error) => error, + Err(error) => io::Error::new(error.kind(), error), + }, + quick_xml::Error::UnexpectedEof(error) => { + io::Error::new(io::ErrorKind::UnexpectedEof, error) + } + error => io::Error::new(io::ErrorKind::InvalidData, error), + }, + RdfXmlErrorKind::Other(error) => io::Error::new(io::ErrorKind::InvalidData, error), + _ => io::Error::new(io::ErrorKind::InvalidData, error), + } + } +} diff --git a/lib/oxrdfxml/src/lib.rs b/lib/oxrdfxml/src/lib.rs new file mode 100644 index 00000000..e04e0d86 --- /dev/null +++ b/lib/oxrdfxml/src/lib.rs @@ -0,0 +1,14 @@ +#![doc = include_str!("../README.md")] +#![doc(test(attr(deny(warnings))))] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc(html_favicon_url = "https://raw.githubusercontent.com/oxigraph/oxigraph/main/logo.svg")] +#![doc(html_logo_url = "https://raw.githubusercontent.com/oxigraph/oxigraph/main/logo.svg")] + +mod error; +mod parser; +mod serializer; +mod utils; + +pub use crate::serializer::{RdfXmlSerializer, ToWriteRdfXmlWriter}; +pub use error::RdfXmlError; +pub use parser::{FromReadRdfXmlReader, RdfXmlParser}; diff --git a/lib/oxrdfxml/src/parser.rs b/lib/oxrdfxml/src/parser.rs new file mode 100644 index 00000000..3493fe54 --- /dev/null +++ b/lib/oxrdfxml/src/parser.rs @@ -0,0 +1,1081 @@ +use crate::error::{RdfXmlError, RdfXmlErrorKind}; +use crate::utils::*; +use oxilangtag::LanguageTag; +use oxiri::{Iri, IriParseError}; +use oxrdf::vocab::rdf; +use oxrdf::{BlankNode, Literal, NamedNode, Subject, Term, Triple}; +use quick_xml::escape::unescape_with; +use quick_xml::events::attributes::Attribute; +use quick_xml::events::*; +use quick_xml::name::{LocalName, QName, ResolveResult}; +use quick_xml::{NsReader, Writer}; +use std::collections::{HashMap, HashSet}; +use std::io::{BufRead, BufReader, Read}; +use std::str; + +/// A [RDF/XML](https://www.w3.org/TR/rdf-syntax-grammar/) streaming parser. +/// +/// It reads the file in streaming. +/// It does not keep data in memory except a stack for handling nested XML tags, and a set of all +/// seen `rdf:ID`s to detect duplicate ids and fail according to the specification. +/// +/// Its performances are not optimized yet and hopefully could be significantly enhanced by reducing the +/// number of allocations and copies done by the parser. +/// +/// Count the number of people: +/// ``` +/// use oxrdf::{NamedNodeRef, vocab::rdf}; +/// use oxrdfxml::RdfXmlParser; +/// +/// let file = b" +/// +/// +/// +/// Foo +/// +/// +/// "; +/// +/// let schema_person = NamedNodeRef::new("http://schema.org/Person")?; +/// let mut count = 0; +/// for triple in RdfXmlParser::new().parse_from_read(file.as_ref()) { +/// let triple = triple?; +/// if triple.predicate == rdf::TYPE && triple.object == schema_person.into() { +/// count += 1; +/// } +/// } +/// assert_eq!(2, count); +/// # Result::<_,Box>::Ok(()) +/// ``` +#[derive(Default)] +pub struct RdfXmlParser { + base: Option>, +} + +impl RdfXmlParser { + /// Builds a new [`RdfXmlParser`]. + #[inline] + pub fn new() -> Self { + Self::default() + } + + #[inline] + pub fn with_base_iri(mut self, base_iri: impl Into) -> Result { + self.base = Some(Iri::parse(base_iri.into())?); + Ok(self) + } + + /// Parses a RDF/XML file from a [`Read`] implementation. + /// + /// Count the number of people: + /// ``` + /// use oxrdf::{NamedNodeRef, vocab::rdf}; + /// use oxrdfxml::RdfXmlParser; + /// + /// let file = b" + /// + /// + /// + /// Foo + /// + /// + /// "; + /// + /// let schema_person = NamedNodeRef::new("http://schema.org/Person")?; + /// let mut count = 0; + /// for triple in RdfXmlParser::new().parse_from_read(file.as_ref()) { + /// let triple = triple?; + /// if triple.predicate == rdf::TYPE && triple.object == schema_person.into() { + /// count += 1; + /// } + /// } + /// assert_eq!(2, count); + /// # Result::<_,Box>::Ok(()) + /// ``` + pub fn parse_from_read(&self, read: R) -> FromReadRdfXmlReader { + let mut reader = NsReader::from_reader(BufReader::new(read)); + reader.expand_empty_elements(true); + FromReadRdfXmlReader { + results: Vec::new(), + reader: RdfXmlReader { + reader, + state: vec![RdfXmlState::Doc { + base_iri: self.base.clone(), + }], + custom_entities: HashMap::default(), + in_literal_depth: 0, + known_rdf_id: HashSet::default(), + is_end: false, + }, + reader_buffer: Vec::default(), + } + } +} + +/// Parses a RDF/XML file from a [`Read`] implementation. Can be built using [`RdfXmlParser::parse_from_read`]. +/// +/// Count the number of people: +/// ``` +/// use oxrdf::{NamedNodeRef, vocab::rdf}; +/// use oxrdfxml::RdfXmlParser; +/// +/// let file = b" +/// +/// +/// +/// Foo +/// +/// +/// "; +/// +/// let schema_person = NamedNodeRef::new("http://schema.org/Person")?; +/// let mut count = 0; +/// for triple in RdfXmlParser::new().parse_from_read(file.as_ref()) { +/// let triple = triple?; +/// if triple.predicate == rdf::TYPE && triple.object == schema_person.into() { +/// count += 1; +/// } +/// } +/// assert_eq!(2, count); +/// # Result::<_,Box>::Ok(()) +/// ``` +pub struct FromReadRdfXmlReader { + results: Vec, + reader: RdfXmlReader>, + reader_buffer: Vec, +} + +impl Iterator for FromReadRdfXmlReader { + type Item = Result; + + fn next(&mut self) -> Option> { + loop { + if let Some(triple) = self.results.pop() { + return Some(Ok(triple)); + } else if self.reader.is_end { + return None; + } + if let Err(e) = self.parse_step() { + return Some(Err(e)); + } + } + } +} + +impl FromReadRdfXmlReader { + /// The current byte position in the input data. + pub fn buffer_position(&self) -> usize { + self.reader.reader.buffer_position() + } + + fn parse_step(&mut self) -> Result<(), RdfXmlError> { + self.reader_buffer.clear(); + let event = self + .reader + .reader + .read_event_into(&mut self.reader_buffer)?; + self.reader.parse_event(event, &mut self.results) + } +} + +const RDF_ABOUT: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#about"; +const RDF_ABOUT_EACH: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#aboutEach"; +const RDF_ABOUT_EACH_PREFIX: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#aboutEachPrefix"; +const RDF_BAG_ID: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#bagID"; +const RDF_DATATYPE: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#datatype"; +const RDF_DESCRIPTION: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#Description"; +const RDF_ID: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#ID"; +const RDF_LI: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#li"; +const RDF_NODE_ID: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#nodeID"; +const RDF_PARSE_TYPE: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#parseType"; +const RDF_RDF: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#RDF"; +const RDF_RESOURCE: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#resource"; + +const RESERVED_RDF_ELEMENTS: [&str; 11] = [ + RDF_ABOUT, + RDF_ABOUT_EACH, + RDF_ABOUT_EACH_PREFIX, + RDF_BAG_ID, + RDF_DATATYPE, + RDF_ID, + RDF_LI, + RDF_NODE_ID, + RDF_PARSE_TYPE, + RDF_RDF, + RDF_RESOURCE, +]; +const RESERVED_RDF_ATTRIBUTES: [&str; 5] = [ + RDF_ABOUT_EACH, + RDF_ABOUT_EACH_PREFIX, + RDF_LI, + RDF_RDF, + RDF_RESOURCE, +]; + +#[derive(Clone, Debug)] +enum NodeOrText { + Node(Subject), + Text(String), +} + +enum RdfXmlState { + Doc { + base_iri: Option>, + }, + Rdf { + base_iri: Option>, + language: Option, + }, + NodeElt { + base_iri: Option>, + language: Option, + subject: Subject, + li_counter: u64, + }, + PropertyElt { + //Resource, Literal or Empty property element + iri: NamedNode, + base_iri: Option>, + language: Option, + subject: Subject, + object: Option, + id_attr: Option, + datatype_attr: Option, + }, + ParseTypeCollectionPropertyElt { + iri: NamedNode, + base_iri: Option>, + language: Option, + subject: Subject, + objects: Vec, + id_attr: Option, + }, + ParseTypeLiteralPropertyElt { + iri: NamedNode, + base_iri: Option>, + language: Option, + subject: Subject, + writer: Writer>, + id_attr: Option, + emit: bool, //false for parseTypeOtherPropertyElt support + }, +} + +impl RdfXmlState { + fn base_iri(&self) -> Option<&Iri> { + match self { + RdfXmlState::Doc { base_iri, .. } + | RdfXmlState::Rdf { base_iri, .. } + | RdfXmlState::NodeElt { base_iri, .. } + | RdfXmlState::PropertyElt { base_iri, .. } + | RdfXmlState::ParseTypeCollectionPropertyElt { base_iri, .. } + | RdfXmlState::ParseTypeLiteralPropertyElt { base_iri, .. } => base_iri.as_ref(), + } + } + + fn language(&self) -> Option<&String> { + match self { + RdfXmlState::Doc { .. } => None, + RdfXmlState::Rdf { language, .. } + | RdfXmlState::NodeElt { language, .. } + | RdfXmlState::PropertyElt { language, .. } + | RdfXmlState::ParseTypeCollectionPropertyElt { language, .. } + | RdfXmlState::ParseTypeLiteralPropertyElt { language, .. } => language.as_ref(), + } + } +} + +struct RdfXmlReader { + reader: NsReader, + state: Vec, + custom_entities: HashMap, + in_literal_depth: usize, + known_rdf_id: HashSet, + is_end: bool, +} + +impl RdfXmlReader { + fn parse_event(&mut self, event: Event, results: &mut Vec) -> Result<(), RdfXmlError> { + match event { + Event::Start(event) => self.parse_start_event(&event, results), + Event::End(event) => self.parse_end_event(&event, results), + Event::Empty(_) => unreachable!("The expand_empty_elements option must be enabled"), + Event::Text(event) => self.parse_text_event(&event), + Event::CData(event) => self.parse_text_event(&event.escape()?), + Event::Comment(_) | Event::PI(_) => Ok(()), + Event::Decl(decl) => { + if let Some(encoding) = decl.encoding() { + if !is_utf8(&encoding?) { + return Err(RdfXmlError::msg( + "Only UTF-8 is supported by the RDF/XML parser", + )); + } + } + Ok(()) + } + Event::DocType(dt) => self.parse_doctype(&dt), + Event::Eof => { + self.is_end = true; + Ok(()) + } + } + } + + fn parse_doctype(&mut self, dt: &BytesText<'_>) -> Result<(), RdfXmlError> { + // we extract entities + for input in self + .reader + .decoder() + .decode(dt.as_ref())? + .split('<') + .skip(1) + { + if let Some(input) = input.strip_prefix("!ENTITY") { + let input = input.trim_start().strip_prefix('%').unwrap_or(input); + let (entity_name, input) = input.trim_start().split_once(|c: char| c.is_ascii_whitespace()).ok_or_else(|| { + RdfXmlError::msg( + "').ok_or_else(|| { + RdfXmlError::msg("") + })?; + + // Resolves custom entities within the current entity definition. + let entity_value = unescape_with(entity_value, |e| self.resolve_entity(e)) + .map_err(quick_xml::Error::from)?; + self.custom_entities + .insert(entity_name.to_owned(), entity_value.to_string()); + } + } + Ok(()) + } + + fn parse_start_event( + &mut self, + event: &BytesStart<'_>, + results: &mut Vec, + ) -> Result<(), RdfXmlError> { + #[derive(PartialEq, Eq)] + enum RdfXmlParseType { + Default, + Collection, + Literal, + Resource, + Other, + } + + #[derive(PartialEq, Eq)] + enum RdfXmlNextProduction { + Rdf, + NodeElt, + PropertyElt { subject: Subject }, + } + + //Literal case + if let Some(RdfXmlState::ParseTypeLiteralPropertyElt { writer, .. }) = self.state.last_mut() + { + let mut clean_event = BytesStart::new( + self.reader + .decoder() + .decode(event.name().as_ref())? + .to_string(), + ); + for attr in event.attributes() { + clean_event.push_attribute(attr?); + } + writer.write_event(Event::Start(clean_event))?; + self.in_literal_depth += 1; + return Ok(()); + } + + let tag_name = self.resolve_tag_name(event.name())?; + + //We read attributes + let (mut language, mut base_iri) = if let Some(current_state) = self.state.last() { + ( + current_state.language().cloned(), + current_state.base_iri().cloned(), + ) + } else { + (None, None) + }; + + let mut id_attr = None; + let mut node_id_attr = None; + let mut about_attr = None; + let mut property_attrs = Vec::default(); + let mut resource_attr = None; + let mut datatype_attr = None; + let mut parse_type = RdfXmlParseType::Default; + let mut type_attr = None; + + for attribute in event.attributes() { + let attribute = attribute?; + if attribute.key.as_ref().starts_with(b"xml") { + if attribute.key.as_ref() == b"xml:lang" { + let tag = self.convert_attribute(&attribute)?; + language = Some( + LanguageTag::parse(tag.to_ascii_lowercase()) + .map_err(|error| RdfXmlError { + kind: RdfXmlErrorKind::InvalidLanguageTag { tag, error }, + })? + .into_inner(), + ); + } else if attribute.key.as_ref() == b"xml:base" { + let iri = self.convert_attribute(&attribute)?; + base_iri = Some(Iri::parse(iri.clone()).map_err(|error| RdfXmlError { + kind: RdfXmlErrorKind::InvalidIri { iri, error }, + })?) + } else { + // We ignore other xml attributes + } + } else { + let attribute_url = self.resolve_attribute_name(attribute.key)?; + if *attribute_url == *RDF_ID { + let mut id = self.convert_attribute(&attribute)?; + if !is_nc_name(&id) { + return Err(RdfXmlError::msg(format!( + "{} is not a valid rdf:ID value", + &id + ))); + } + id.insert(0, '#'); + id_attr = Some(id); + } else if *attribute_url == *RDF_BAG_ID { + let bag_id = self.convert_attribute(&attribute)?; + if !is_nc_name(&bag_id) { + return Err(RdfXmlError::msg(format!( + "{} is not a valid rdf:bagID value", + &bag_id + ))); + } + } else if *attribute_url == *RDF_NODE_ID { + let id = self.convert_attribute(&attribute)?; + if !is_nc_name(&id) { + return Err(RdfXmlError::msg(format!( + "{} is not a valid rdf:nodeID value", + &id + ))); + } + node_id_attr = Some(BlankNode::new_unchecked(id)); + } else if *attribute_url == *RDF_ABOUT { + about_attr = Some(attribute); + } else if *attribute_url == *RDF_RESOURCE { + resource_attr = Some(attribute); + } else if *attribute_url == *RDF_DATATYPE { + datatype_attr = Some(attribute); + } else if *attribute_url == *RDF_PARSE_TYPE { + parse_type = match attribute.value.as_ref() { + b"Collection" => RdfXmlParseType::Collection, + b"Literal" => RdfXmlParseType::Literal, + b"Resource" => RdfXmlParseType::Resource, + _ => RdfXmlParseType::Other, + }; + } else if attribute_url == rdf::TYPE.as_str() { + type_attr = Some(attribute); + } else if RESERVED_RDF_ATTRIBUTES.contains(&&*attribute_url) { + return Err(RdfXmlError::msg(format!( + "{} is not a valid attribute", + &attribute_url + ))); + } else { + property_attrs.push(( + NamedNode::new(attribute_url.clone()).map_err(|error| RdfXmlError { + kind: RdfXmlErrorKind::InvalidIri { + iri: attribute_url, + error, + }, + })?, + self.convert_attribute(&attribute)?, + )); + } + } + } + + //Parsing with the base URI + let id_attr = match id_attr { + Some(iri) => { + let iri = resolve(&base_iri, iri)?; + if self.known_rdf_id.contains(iri.as_str()) { + return Err(RdfXmlError::msg(format!( + "{} has already been used as rdf:ID value", + &iri + ))); + } + self.known_rdf_id.insert(iri.as_str().into()); + Some(iri) + } + None => None, + }; + let about_attr = match about_attr { + Some(attr) => Some(self.convert_iri_attribute(&base_iri, &attr)?), + None => None, + }; + let resource_attr = match resource_attr { + Some(attr) => Some(self.convert_iri_attribute(&base_iri, &attr)?), + None => None, + }; + let datatype_attr = match datatype_attr { + Some(attr) => Some(self.convert_iri_attribute(&base_iri, &attr)?), + None => None, + }; + let type_attr = match type_attr { + Some(attr) => Some(self.convert_iri_attribute(&base_iri, &attr)?), + None => None, + }; + + let expected_production = match self.state.last() { + Some(RdfXmlState::Doc { .. }) => RdfXmlNextProduction::Rdf, + Some( + RdfXmlState::Rdf { .. } + | RdfXmlState::PropertyElt { .. } + | RdfXmlState::ParseTypeCollectionPropertyElt { .. }, + ) => RdfXmlNextProduction::NodeElt, + Some(RdfXmlState::NodeElt { subject, .. }) => RdfXmlNextProduction::PropertyElt { + subject: subject.clone(), + }, + Some(RdfXmlState::ParseTypeLiteralPropertyElt { .. }) => { + panic!("ParseTypeLiteralPropertyElt production children should never be considered as a RDF/XML content") + } + None => { + return Err(RdfXmlError::msg( + "No state in the stack: the XML is not balanced", + )); + } + }; + + let new_state = match expected_production { + RdfXmlNextProduction::Rdf => { + if *tag_name == *RDF_RDF { + RdfXmlState::Rdf { base_iri, language } + } else if RESERVED_RDF_ELEMENTS.contains(&&*tag_name) { + return Err(RdfXmlError::msg(format!( + "Invalid node element tag name: {}", + &tag_name + ))); + } else { + Self::build_node_elt( + NamedNode::new(tag_name.clone()).map_err(|error| RdfXmlError { + kind: RdfXmlErrorKind::InvalidIri { + iri: tag_name, + error, + }, + })?, + base_iri, + language, + id_attr, + node_id_attr, + about_attr, + type_attr, + property_attrs, + results, + )? + } + } + RdfXmlNextProduction::NodeElt => { + if RESERVED_RDF_ELEMENTS.contains(&&*tag_name) { + return Err(RdfXmlError::msg(format!( + "Invalid property element tag name: {}", + &tag_name + ))); + } + Self::build_node_elt( + NamedNode::new(tag_name.clone()).map_err(|error| RdfXmlError { + kind: RdfXmlErrorKind::InvalidIri { + iri: tag_name, + error, + }, + })?, + base_iri, + language, + id_attr, + node_id_attr, + about_attr, + type_attr, + property_attrs, + results, + )? + } + RdfXmlNextProduction::PropertyElt { subject } => { + let iri = if *tag_name == *RDF_LI { + let Some(RdfXmlState::NodeElt { li_counter, .. }) = self.state.last_mut() else { + return Err(RdfXmlError::msg(format!( + "Invalid property element tag name: {}", + &tag_name + ))); + }; + *li_counter += 1; + NamedNode::new_unchecked(format!( + "http://www.w3.org/1999/02/22-rdf-syntax-ns#_{}", + li_counter + )) + } else if RESERVED_RDF_ELEMENTS.contains(&&*tag_name) + || *tag_name == *RDF_DESCRIPTION + { + return Err(RdfXmlError::msg(format!( + "Invalid property element tag name: {}", + &tag_name + ))); + } else { + NamedNode::new(tag_name.clone()).map_err(|error| RdfXmlError { + kind: RdfXmlErrorKind::InvalidIri { + iri: tag_name, + error, + }, + })? + }; + match parse_type { + RdfXmlParseType::Default => { + if resource_attr.is_some() + || node_id_attr.is_some() + || !property_attrs.is_empty() + { + let object = match (resource_attr, node_id_attr) + { + (Some(resource_attr), None) => Subject::from(resource_attr), + (None, Some(node_id_attr)) => node_id_attr.into(), + (None, None) => BlankNode::default().into(), + (Some(_), Some(_)) => return Err(RdfXmlError::msg("Not both rdf:resource and rdf:nodeID could be set at the same time")) + }; + Self::emit_property_attrs(&object, property_attrs, &language, results); + if let Some(type_attr) = type_attr { + results.push(Triple::new(object.clone(), rdf::TYPE, type_attr)); + } + RdfXmlState::PropertyElt { + iri, + base_iri, + language, + subject, + object: Some(NodeOrText::Node(object)), + id_attr, + datatype_attr, + } + } else { + RdfXmlState::PropertyElt { + iri, + base_iri, + language, + subject, + object: None, + id_attr, + datatype_attr, + } + } + } + RdfXmlParseType::Literal => RdfXmlState::ParseTypeLiteralPropertyElt { + iri, + base_iri, + language, + subject, + writer: Writer::new(Vec::default()), + id_attr, + emit: true, + }, + RdfXmlParseType::Resource => Self::build_parse_type_resource_property_elt( + iri, base_iri, language, subject, id_attr, results, + ), + RdfXmlParseType::Collection => RdfXmlState::ParseTypeCollectionPropertyElt { + iri, + base_iri, + language, + subject, + objects: Vec::default(), + id_attr, + }, + RdfXmlParseType::Other => RdfXmlState::ParseTypeLiteralPropertyElt { + iri, + base_iri, + language, + subject, + writer: Writer::new(Vec::default()), + id_attr, + emit: false, + }, + } + } + }; + self.state.push(new_state); + Ok(()) + } + + fn parse_end_event( + &mut self, + event: &BytesEnd<'_>, + results: &mut Vec, + ) -> Result<(), RdfXmlError> { + //Literal case + if self.in_literal_depth > 0 { + if let Some(RdfXmlState::ParseTypeLiteralPropertyElt { writer, .. }) = + self.state.last_mut() + { + writer.write_event(Event::End(BytesEnd::new( + self.reader.decoder().decode(event.name().as_ref())?, + )))?; + self.in_literal_depth -= 1; + return Ok(()); + } + } + + if let Some(current_state) = self.state.pop() { + self.end_state(current_state, results)?; + } + Ok(()) + } + + fn parse_text_event(&mut self, event: &BytesText<'_>) -> Result<(), RdfXmlError> { + let text = event.unescape_with(|e| self.resolve_entity(e))?.to_string(); + match self.state.last_mut() { + Some(RdfXmlState::PropertyElt { object, .. }) => { + if !event.iter().copied().all(is_whitespace) { + *object = Some(NodeOrText::Text(text)); + } + Ok(()) + } + Some(RdfXmlState::ParseTypeLiteralPropertyElt { writer, .. }) => { + writer.write_event(Event::Text(BytesText::new(&text)))?; + Ok(()) + } + _ => { + if event.iter().copied().all(is_whitespace) { + Ok(()) + } else { + Err(RdfXmlError::msg(format!( + "Unexpected text event: '{}'", + text + ))) + } + } + } + } + + fn resolve_tag_name(&self, qname: QName<'_>) -> Result { + let (namespace, local_name) = self.reader.resolve_element(qname); + self.resolve_ns_name(namespace, local_name) + } + + fn resolve_attribute_name(&self, qname: QName<'_>) -> Result { + let (namespace, local_name) = self.reader.resolve_attribute(qname); + self.resolve_ns_name(namespace, local_name) + } + + fn resolve_ns_name( + &self, + namespace: ResolveResult, + local_name: LocalName<'_>, + ) -> Result { + match namespace { + ResolveResult::Bound(ns) => { + let mut value = Vec::with_capacity(ns.as_ref().len() + local_name.as_ref().len()); + value.extend_from_slice(ns.as_ref()); + value.extend_from_slice(local_name.as_ref()); + Ok(unescape_with(&self.reader.decoder().decode(&value)?, |e| { + self.resolve_entity(e) + }) + .map_err(quick_xml::Error::from)? + .to_string()) + } + ResolveResult::Unbound => { + Err(RdfXmlError::msg("XML namespaces are required in RDF/XML")) + } + ResolveResult::Unknown(v) => Err(RdfXmlError::msg(format!( + "Unknown prefix {}:", + self.reader.decoder().decode(&v)? + ))), + } + } + + #[allow(clippy::too_many_arguments)] + fn build_node_elt( + iri: NamedNode, + base_iri: Option>, + language: Option, + id_attr: Option, + node_id_attr: Option, + about_attr: Option, + type_attr: Option, + property_attrs: Vec<(NamedNode, String)>, + results: &mut Vec, + ) -> Result { + let subject = match (id_attr, node_id_attr, about_attr) { + (Some(id_attr), None, None) => Subject::from(id_attr), + (None, Some(node_id_attr), None) => node_id_attr.into(), + (None, None, Some(about_attr)) => about_attr.into(), + (None, None, None) => BlankNode::default().into(), + (Some(_), Some(_), _) => { + return Err(RdfXmlError::msg( + "Not both rdf:ID and rdf:nodeID could be set at the same time", + )) + } + (_, Some(_), Some(_)) => { + return Err(RdfXmlError::msg( + "Not both rdf:nodeID and rdf:resource could be set at the same time", + )) + } + (Some(_), _, Some(_)) => { + return Err(RdfXmlError::msg( + "Not both rdf:ID and rdf:resource could be set at the same time", + )) + } + }; + + Self::emit_property_attrs(&subject, property_attrs, &language, results); + + if let Some(type_attr) = type_attr { + results.push(Triple::new(subject.clone(), rdf::TYPE, type_attr)); + } + + if iri != *RDF_DESCRIPTION { + results.push(Triple::new(subject.clone(), rdf::TYPE, iri)); + } + Ok(RdfXmlState::NodeElt { + base_iri, + language, + subject, + li_counter: 0, + }) + } + + fn build_parse_type_resource_property_elt( + iri: NamedNode, + base_iri: Option>, + language: Option, + subject: Subject, + id_attr: Option, + results: &mut Vec, + ) -> RdfXmlState { + let object = BlankNode::default(); + let triple = Triple::new(subject, iri, object.clone()); + if let Some(id_attr) = id_attr { + Self::reify(triple.clone(), id_attr, results); + } + results.push(triple); + RdfXmlState::NodeElt { + base_iri, + language, + subject: object.into(), + li_counter: 0, + } + } + + fn end_state( + &mut self, + state: RdfXmlState, + results: &mut Vec, + ) -> Result<(), RdfXmlError> { + match state { + RdfXmlState::PropertyElt { + iri, + language, + subject, + id_attr, + datatype_attr, + object, + .. + } => { + let object = match object { + Some(NodeOrText::Node(node)) => Term::from(node), + Some(NodeOrText::Text(text)) => { + Self::new_literal(text, language, datatype_attr).into() + } + None => Self::new_literal(String::new(), language, datatype_attr).into(), + }; + let triple = Triple::new(subject, iri, object); + if let Some(id_attr) = id_attr { + Self::reify(triple.clone(), id_attr, results); + } + results.push(triple); + } + RdfXmlState::ParseTypeCollectionPropertyElt { + iri, + subject, + id_attr, + objects, + .. + } => { + let mut current_node = Subject::from(rdf::NIL); + for object in objects.into_iter().rev() { + let subject = Subject::from(BlankNode::default()); + results.push(Triple::new(subject.clone(), rdf::FIRST, object)); + results.push(Triple::new(subject.clone(), rdf::REST, current_node)); + current_node = subject; + } + let triple = Triple::new(subject, iri, current_node); + if let Some(id_attr) = id_attr { + Self::reify(triple.clone(), id_attr, results); + } + results.push(triple); + } + RdfXmlState::ParseTypeLiteralPropertyElt { + iri, + subject, + id_attr, + writer, + emit, + .. + } => { + if emit { + let object = writer.into_inner(); + if object.is_empty() { + return Err(RdfXmlError::msg(format!( + "No value found for rdf:XMLLiteral value of property {}", + iri + ))); + } + let triple = Triple::new( + subject, + iri, + Literal::new_typed_literal( + str::from_utf8(&object).map_err(|_| { + RdfXmlError::msg("The XML literal is not in valid UTF-8".to_owned()) + })?, + rdf::XML_LITERAL, + ), + ); + if let Some(id_attr) = id_attr { + Self::reify(triple.clone(), id_attr, results); + } + results.push(triple); + } + } + RdfXmlState::NodeElt { subject, .. } => match self.state.last_mut() { + Some(RdfXmlState::PropertyElt { object, .. }) => { + *object = Some(NodeOrText::Node(subject)) + } + Some(RdfXmlState::ParseTypeCollectionPropertyElt { objects, .. }) => { + objects.push(subject) + } + _ => (), + }, + _ => (), + } + Ok(()) + } + + fn new_literal( + value: String, + language: Option, + datatype: Option, + ) -> Literal { + if let Some(datatype) = datatype { + Literal::new_typed_literal(value, datatype) + } else if let Some(language) = language { + Literal::new_language_tagged_literal_unchecked(value, language) + } else { + Literal::new_simple_literal(value) + } + } + + fn reify(triple: Triple, statement_id: NamedNode, results: &mut Vec) { + results.push(Triple::new(statement_id.clone(), rdf::TYPE, rdf::STATEMENT)); + results.push(Triple::new( + statement_id.clone(), + rdf::SUBJECT, + triple.subject, + )); + results.push(Triple::new( + statement_id.clone(), + rdf::PREDICATE, + triple.predicate, + )); + results.push(Triple::new(statement_id, rdf::OBJECT, triple.object)); + } + + fn emit_property_attrs( + subject: &Subject, + literal_attributes: Vec<(NamedNode, String)>, + language: &Option, + results: &mut Vec, + ) { + for (literal_predicate, literal_value) in literal_attributes { + results.push(Triple::new( + subject.clone(), + literal_predicate, + if let Some(language) = language.clone() { + Literal::new_language_tagged_literal_unchecked(literal_value, language) + } else { + Literal::new_simple_literal(literal_value) + }, + )); + } + } + + fn convert_attribute(&self, attribute: &Attribute) -> Result { + Ok(attribute + .decode_and_unescape_value_with(&self.reader, |e| self.resolve_entity(e))? + .to_string()) + } + + fn convert_iri_attribute( + &self, + base_iri: &Option>, + attribute: &Attribute<'_>, + ) -> Result { + resolve(base_iri, self.convert_attribute(attribute)?) + } + + fn resolve_entity(&self, e: &str) -> Option<&str> { + self.custom_entities.get(e).map(String::as_str) + } +} + +fn resolve(base_iri: &Option>, relative_iri: String) -> Result { + if let Some(base_iri) = base_iri { + Ok(NamedNode::new_unchecked( + base_iri + .resolve(&relative_iri) + .map_err(|error| RdfXmlError { + kind: RdfXmlErrorKind::InvalidIri { + iri: relative_iri, + error, + }, + })? + .into_inner(), + )) + } else { + NamedNode::new(relative_iri.clone()).map_err(|error| RdfXmlError { + kind: RdfXmlErrorKind::InvalidIri { + iri: relative_iri, + error, + }, + }) + } +} + +fn is_nc_name(name: &str) -> bool { + // Name - (Char* ':' Char*) + is_name(name) && name.chars().all(|c| c != ':') +} + +fn is_name(name: &str) -> bool { + // NameStartChar (NameChar)* + let mut c = name.chars(); + if !c.next().map_or(false, is_name_start_char) { + return false; + } + c.all(is_name_char) +} + +fn is_whitespace(c: u8) -> bool { + matches!(c, b' ' | b'\t' | b'\n' | b'\r') +} + +fn is_utf8(encoding: &[u8]) -> bool { + matches!( + encoding.to_ascii_lowercase().as_slice(), + b"unicode-1-1-utf-8" + | b"unicode11utf8" + | b"unicode20utf8" + | b"utf-8" + | b"utf8" + | b"x-unicode20utf8" + ) +} diff --git a/lib/oxrdfxml/src/serializer.rs b/lib/oxrdfxml/src/serializer.rs new file mode 100644 index 00000000..03de4fb9 --- /dev/null +++ b/lib/oxrdfxml/src/serializer.rs @@ -0,0 +1,229 @@ +use crate::utils::*; +use oxrdf::{Subject, SubjectRef, TermRef, TripleRef}; +use quick_xml::events::*; +use quick_xml::Writer; +use std::io; +use std::io::Write; +use std::sync::Arc; + +/// A [RDF/XML](https://www.w3.org/TR/rdf-syntax-grammar/) serializer. +/// +/// ``` +/// use oxrdf::{NamedNodeRef, TripleRef}; +/// use oxrdfxml::RdfXmlSerializer; +/// +/// let mut writer = RdfXmlSerializer::new().serialize_to_write(Vec::new()); +/// writer.write_triple(TripleRef::new( +/// NamedNodeRef::new("http://example.com#me")?, +/// NamedNodeRef::new("http://www.w3.org/1999/02/22-rdf-syntax-ns#type")?, +/// NamedNodeRef::new("http://schema.org/Person")?, +/// ))?; +/// assert_eq!( +/// b"\n\n\t\n\t\t\n\t\n", +/// writer.finish()?.as_slice() +/// ); +/// # Result::<_,Box>::Ok(()) +/// ``` +#[derive(Default)] +pub struct RdfXmlSerializer; + +impl RdfXmlSerializer { + /// Builds a new [`RdfXmlSerializer`]. + #[inline] + pub fn new() -> Self { + Self + } + + /// Writes a RdfXml file to a [`Write`] implementation. + /// + /// ``` + /// use oxrdf::{NamedNodeRef, TripleRef}; + /// use oxrdfxml::RdfXmlSerializer; + /// + /// let mut writer = RdfXmlSerializer::new().serialize_to_write(Vec::new()); + /// writer.write_triple(TripleRef::new( + /// NamedNodeRef::new("http://example.com#me")?, + /// NamedNodeRef::new("http://www.w3.org/1999/02/22-rdf-syntax-ns#type")?, + /// NamedNodeRef::new("http://schema.org/Person")?, + /// ))?; + /// assert_eq!( + /// b"\n\n\t\n\t\t\n\t\n", + /// writer.finish()?.as_slice() + /// ); + /// # Result::<_,Box>::Ok(()) + /// ``` + #[allow(clippy::unused_self)] + pub fn serialize_to_write(&self, write: W) -> ToWriteRdfXmlWriter { + ToWriteRdfXmlWriter { + writer: Writer::new_with_indent(write, b'\t', 1), + current_subject: None, + } + } +} + +/// Writes a RDF/XML file to a [`Write`] implementation. Can be built using [`RdfXmlSerializer::serialize_to_write`]. +/// +/// ``` +/// use oxrdf::{NamedNodeRef, TripleRef}; +/// use oxrdfxml::RdfXmlSerializer; +/// +/// let mut writer = RdfXmlSerializer::new().serialize_to_write(Vec::new()); +/// writer.write_triple(TripleRef::new( +/// NamedNodeRef::new("http://example.com#me")?, +/// NamedNodeRef::new("http://www.w3.org/1999/02/22-rdf-syntax-ns#type")?, +/// NamedNodeRef::new("http://schema.org/Person")?, +/// ))?; +/// assert_eq!( +/// b"\n\n\t\n\t\t\n\t\n", +/// writer.finish()?.as_slice() +/// ); +/// # Result::<_,Box>::Ok(()) +/// ``` +pub struct ToWriteRdfXmlWriter { + writer: Writer, + current_subject: Option, +} + +impl ToWriteRdfXmlWriter { + /// Writes an extra triple. + #[allow(clippy::match_wildcard_for_single_variants, unreachable_patterns)] + pub fn write_triple<'a>(&mut self, t: impl Into>) -> io::Result<()> { + if self.current_subject.is_none() { + self.write_start()?; + } + + let triple = t.into(); + // We open a new rdf:Description if useful + if self.current_subject.as_ref().map(Subject::as_ref) != Some(triple.subject) { + if self.current_subject.is_some() { + self.writer + .write_event(Event::End(BytesEnd::new("rdf:Description"))) + .map_err(map_err)?; + } + + let mut description_open = BytesStart::new("rdf:Description"); + match triple.subject { + SubjectRef::NamedNode(node) => { + description_open.push_attribute(("rdf:about", node.as_str())) + } + SubjectRef::BlankNode(node) => { + description_open.push_attribute(("rdf:nodeID", node.as_str())) + } + _ => { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "RDF/XML only supports named or blank subject", + )) + } + } + self.writer + .write_event(Event::Start(description_open)) + .map_err(map_err)?; + } + + let (prop_prefix, prop_value) = split_iri(triple.predicate.as_str()); + let (prop_qname, prop_xmlns) = if prop_value.is_empty() { + ("prop:", ("xmlns:prop", prop_prefix)) + } else { + (prop_value, ("xmlns", prop_prefix)) + }; + let property_element = self.writer.create_element(prop_qname); + let property_element = property_element.with_attribute(prop_xmlns); + + match triple.object { + TermRef::NamedNode(node) => property_element + .with_attribute(("rdf:resource", node.as_str())) + .write_empty(), + TermRef::BlankNode(node) => property_element + .with_attribute(("rdf:nodeID", node.as_str())) + .write_empty(), + TermRef::Literal(literal) => { + let property_element = if let Some(language) = literal.language() { + property_element.with_attribute(("xml:lang", language)) + } else if !literal.is_plain() { + property_element.with_attribute(("rdf:datatype", literal.datatype().as_str())) + } else { + property_element + }; + property_element.write_text_content(BytesText::new(literal.value())) + } + _ => { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "RDF/XML only supports named, blank or literal object", + )) + } + } + .map_err(map_err)?; + self.current_subject = Some(triple.subject.into_owned()); + Ok(()) + } + + pub fn write_start(&mut self) -> io::Result<()> { + // We open the file + self.writer + .write_event(Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), None))) + .map_err(map_err)?; + let mut rdf_open = BytesStart::new("rdf:RDF"); + rdf_open.push_attribute(("xmlns:rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#")); + self.writer + .write_event(Event::Start(rdf_open)) + .map_err(map_err) + } + + /// Ends the write process and returns the underlying [`Write`]. + pub fn finish(mut self) -> io::Result { + if self.current_subject.is_some() { + self.writer + .write_event(Event::End(BytesEnd::new("rdf:Description"))) + .map_err(map_err)?; + } else { + self.write_start()?; + } + self.writer + .write_event(Event::End(BytesEnd::new("rdf:RDF"))) + .map_err(map_err)?; + Ok(self.writer.into_inner()) + } +} + +fn map_err(error: quick_xml::Error) -> io::Error { + if let quick_xml::Error::Io(error) = error { + match Arc::try_unwrap(error) { + Ok(error) => error, + Err(error) => io::Error::new(error.kind(), error), + } + } else { + io::Error::new(io::ErrorKind::Other, error) + } +} + +fn split_iri(iri: &str) -> (&str, &str) { + if let Some(position_base) = iri.rfind(|c| !is_name_char(c) || c == ':') { + if let Some(position_add) = iri[position_base..].find(|c| is_name_start_char(c) && c != ':') + { + ( + &iri[..position_base + position_add], + &iri[position_base + position_add..], + ) + } else { + (iri, "") + } + } else { + (iri, "") + } +} + +#[test] +fn test_split_iri() { + assert_eq!( + split_iri("http://schema.org/Person"), + ("http://schema.org/", "Person") + ); + assert_eq!(split_iri("http://schema.org/"), ("http://schema.org/", "")); + assert_eq!( + split_iri("http://schema.org#foo"), + ("http://schema.org#", "foo") + ); + assert_eq!(split_iri("urn:isbn:foo"), ("urn:isbn:", "foo")); +} diff --git a/lib/oxrdfxml/src/utils.rs b/lib/oxrdfxml/src/utils.rs new file mode 100644 index 00000000..b8fb2447 --- /dev/null +++ b/lib/oxrdfxml/src/utils.rs @@ -0,0 +1,26 @@ +pub fn is_name_start_char(c: char) -> bool { + // ":" | [A-Z] | "_" | [a-z] | [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x2FF] | [#x370-#x37D] | [#x37F-#x1FFF] | [#x200C-#x200D] | [#x2070-#x218F] | [#x2C00-#x2FEF] | [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] | [#x10000-#xEFFFF] + matches!(c, + ':' + | 'A'..='Z' + | '_' + | 'a'..='z' + | '\u{C0}'..='\u{D6}' + | '\u{D8}'..='\u{F6}' + | '\u{F8}'..='\u{2FF}' + | '\u{370}'..='\u{37D}' + | '\u{37F}'..='\u{1FFF}' + | '\u{200C}'..='\u{200D}' + | '\u{2070}'..='\u{218F}' + | '\u{2C00}'..='\u{2FEF}' + | '\u{3001}'..='\u{D7FF}' + | '\u{F900}'..='\u{FDCF}' + | '\u{FDF0}'..='\u{FFFD}' + | '\u{10000}'..='\u{EFFFF}') +} + +pub fn is_name_char(c: char) -> bool { + // NameStartChar | "-" | "." | [0-9] | #xB7 | [#x0300-#x036F] | [#x203F-#x2040] + is_name_start_char(c) + || matches!(c, '-' | '.' | '0'..='9' | '\u{B7}' | '\u{0300}'..='\u{036F}' | '\u{203F}'..='\u{2040}') +} diff --git a/lib/sparesults/Cargo.toml b/lib/sparesults/Cargo.toml index baddda71..29973e23 100644 --- a/lib/sparesults/Cargo.toml +++ b/lib/sparesults/Cargo.toml @@ -20,7 +20,7 @@ rdf-star = ["oxrdf/rdf-star"] [dependencies] json-event-parser = "0.1" oxrdf = { version = "0.2.0-alpha.1-dev", path="../oxrdf" } -quick-xml = "0.28" +quick-xml = "0.29" [package.metadata.docs.rs] all-features = true diff --git a/lib/sparopt/src/lib.rs b/lib/sparopt/src/lib.rs index d6f62207..2182ff1e 100644 --- a/lib/sparopt/src/lib.rs +++ b/lib/sparopt/src/lib.rs @@ -1,3 +1,9 @@ +#![doc = include_str!("../README.md")] +#![doc(test(attr(deny(warnings))))] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc(html_favicon_url = "https://raw.githubusercontent.com/oxigraph/oxigraph/main/logo.svg")] +#![doc(html_logo_url = "https://raw.githubusercontent.com/oxigraph/oxigraph/main/logo.svg")] + pub use crate::optimizer::Optimizer; pub mod algebra; diff --git a/lib/src/io/error.rs b/lib/src/io/error.rs index 54696363..6d49148d 100644 --- a/lib/src/io/error.rs +++ b/lib/src/io/error.rs @@ -1,5 +1,5 @@ use oxiri::IriParseError; -use rio_xml::RdfXmlError; +use oxrdfxml::RdfXmlError; use std::error::Error; use std::{fmt, io}; diff --git a/lib/src/io/read.rs b/lib/src/io/read.rs index 3b2a107b..9489a168 100644 --- a/lib/src/io/read.rs +++ b/lib/src/io/read.rs @@ -3,14 +3,12 @@ pub use crate::io::error::{ParseError, SyntaxError}; use crate::io::{DatasetFormat, GraphFormat}; use crate::model::*; -use oxiri::{Iri, IriParseError}; +use oxiri::IriParseError; +use oxrdfxml::{FromReadRdfXmlReader, RdfXmlParser}; use oxttl::nquads::{FromReadNQuadsReader, NQuadsParser}; use oxttl::ntriples::{FromReadNTriplesReader, NTriplesParser}; use oxttl::trig::{FromReadTriGReader, TriGParser}; use oxttl::turtle::{FromReadTurtleReader, TurtleParser}; -use rio_api::model as rio; -use rio_api::parser::TriplesParser; -use rio_xml::RdfXmlParser; use std::collections::HashMap; use std::io::BufRead; @@ -40,7 +38,7 @@ pub struct GraphParser { enum GraphParserKind { NTriples(NTriplesParser), Turtle(TurtleParser), - RdfXml { base_iri: Option> }, + RdfXml(RdfXmlParser), } impl GraphParser { @@ -55,7 +53,7 @@ impl GraphParser { GraphFormat::Turtle => { GraphParserKind::Turtle(TurtleParser::new().with_quoted_triples()) } - GraphFormat::RdfXml => GraphParserKind::RdfXml { base_iri: None }, + GraphFormat::RdfXml => GraphParserKind::RdfXml(RdfXmlParser::new()), }, } } @@ -80,9 +78,7 @@ impl GraphParser { inner: match self.inner { GraphParserKind::NTriples(p) => GraphParserKind::NTriples(p), GraphParserKind::Turtle(p) => GraphParserKind::Turtle(p.with_base_iri(base_iri)?), - GraphParserKind::RdfXml { .. } => GraphParserKind::RdfXml { - base_iri: Some(Iri::parse(base_iri.into())?), - }, + GraphParserKind::RdfXml(p) => GraphParserKind::RdfXml(p.with_base_iri(base_iri)?), }, }) } @@ -96,11 +92,8 @@ impl GraphParser { TripleReaderKind::NTriples(p.parse_from_read(reader)) } GraphParserKind::Turtle(p) => TripleReaderKind::Turtle(p.parse_from_read(reader)), - GraphParserKind::RdfXml { base_iri } => { - TripleReaderKind::RdfXml(RdfXmlParser::new(reader, base_iri.clone())) - } + GraphParserKind::RdfXml(p) => TripleReaderKind::RdfXml(p.parse_from_read(reader)), }, - buffer: Vec::new(), } } } @@ -124,48 +117,33 @@ impl GraphParser { pub struct TripleReader { mapper: BlankNodeMapper, parser: TripleReaderKind, - buffer: Vec, } #[allow(clippy::large_enum_variant)] enum TripleReaderKind { NTriples(FromReadNTriplesReader), Turtle(FromReadTurtleReader), - RdfXml(RdfXmlParser), + RdfXml(FromReadRdfXmlReader), } impl Iterator for TripleReader { type Item = Result; fn next(&mut self) -> Option> { - loop { - if let Some(r) = self.buffer.pop() { - return Some(Ok(r)); - } - - return Some(match &mut self.parser { - TripleReaderKind::NTriples(parser) => match parser.next()? { - Ok(triple) => Ok(self.mapper.triple(triple)), - Err(e) => Err(e.into()), - }, - TripleReaderKind::Turtle(parser) => match parser.next()? { - Ok(triple) => Ok(self.mapper.triple(triple)), - Err(e) => Err(e.into()), - }, - TripleReaderKind::RdfXml(parser) => { - if parser.is_end() { - return None; - } else if let Err(e) = parser.parse_step(&mut |t| { - self.buffer.push(self.mapper.triple(RioMapper::triple(&t))); - Ok(()) - }) { - Err(e) - } else { - continue; - } - } - }); - } + Some(match &mut self.parser { + TripleReaderKind::NTriples(parser) => match parser.next()? { + Ok(triple) => Ok(self.mapper.triple(triple)), + Err(e) => Err(e.into()), + }, + TripleReaderKind::Turtle(parser) => match parser.next()? { + Ok(triple) => Ok(self.mapper.triple(triple)), + Err(e) => Err(e.into()), + }, + TripleReaderKind::RdfXml(parser) => match parser.next()? { + Ok(triple) => Ok(self.mapper.triple(triple)), + Err(e) => Err(e.into()), + }, + }) } } @@ -291,55 +269,6 @@ impl Iterator for QuadReader { } } -struct RioMapper; - -impl<'a> RioMapper { - fn named_node(node: rio::NamedNode<'a>) -> NamedNode { - NamedNode::new_unchecked(node.iri) - } - - fn blank_node(node: rio::BlankNode<'a>) -> BlankNode { - BlankNode::new_unchecked(node.id) - } - - fn literal(literal: rio::Literal<'a>) -> Literal { - match literal { - rio::Literal::Simple { value } => Literal::new_simple_literal(value), - rio::Literal::LanguageTaggedString { value, language } => { - Literal::new_language_tagged_literal_unchecked(value, language) - } - rio::Literal::Typed { value, datatype } => { - Literal::new_typed_literal(value, Self::named_node(datatype)) - } - } - } - - fn subject(node: rio::Subject<'a>) -> Subject { - match node { - rio::Subject::NamedNode(node) => Self::named_node(node).into(), - rio::Subject::BlankNode(node) => Self::blank_node(node).into(), - rio::Subject::Triple(triple) => Self::triple(triple).into(), - } - } - - fn term(node: rio::Term<'a>) -> Term { - match node { - rio::Term::NamedNode(node) => Self::named_node(node).into(), - rio::Term::BlankNode(node) => Self::blank_node(node).into(), - rio::Term::Literal(literal) => Self::literal(literal).into(), - rio::Term::Triple(triple) => Self::triple(triple).into(), - } - } - - fn triple(triple: &rio::Triple<'a>) -> Triple { - Triple { - subject: Self::subject(triple.subject), - predicate: Self::named_node(triple.predicate), - object: Self::term(triple.object), - } - } -} - #[derive(Default)] struct BlankNodeMapper { bnode_map: HashMap, diff --git a/lib/src/io/write.rs b/lib/src/io/write.rs index 051ea202..1661343f 100644 --- a/lib/src/io/write.rs +++ b/lib/src/io/write.rs @@ -2,13 +2,11 @@ use crate::io::{DatasetFormat, GraphFormat}; use crate::model::*; +use oxrdfxml::{RdfXmlSerializer, ToWriteRdfXmlWriter}; use oxttl::nquads::{NQuadsSerializer, ToWriteNQuadsWriter}; use oxttl::ntriples::{NTriplesSerializer, ToWriteNTriplesWriter}; use oxttl::trig::{ToWriteTriGWriter, TriGSerializer}; use oxttl::turtle::{ToWriteTurtleWriter, TurtleSerializer}; -use rio_api::formatter::TriplesFormatter; -use rio_api::model as rio; -use rio_xml::RdfXmlFormatter; use std::io::{self, Write}; /// A serializer for RDF graph serialization formats. @@ -23,7 +21,7 @@ use std::io::{self, Write}; /// use oxigraph::model::*; /// /// let mut buffer = Vec::new(); -/// let mut writer = GraphSerializer::from_format(GraphFormat::NTriples).triple_writer(&mut buffer)?; +/// let mut writer = GraphSerializer::from_format(GraphFormat::NTriples).triple_writer(&mut buffer); /// writer.write(&Triple { /// subject: NamedNode::new("http://example.com/s")?.into(), /// predicate: NamedNode::new("http://example.com/p")?, @@ -46,8 +44,8 @@ impl GraphSerializer { } /// Returns a [`TripleWriter`] allowing writing triples into the given [`Write`] implementation - pub fn triple_writer(&self, writer: W) -> io::Result> { - Ok(TripleWriter { + pub fn triple_writer(&self, writer: W) -> TripleWriter { + TripleWriter { formatter: match self.format { GraphFormat::NTriples => { TripleWriterKind::NTriples(NTriplesSerializer::new().serialize_to_write(writer)) @@ -55,9 +53,11 @@ impl GraphSerializer { GraphFormat::Turtle => { TripleWriterKind::Turtle(TurtleSerializer::new().serialize_to_write(writer)) } - GraphFormat::RdfXml => TripleWriterKind::RdfXml(RdfXmlFormatter::new(writer)?), + GraphFormat::RdfXml => { + TripleWriterKind::RdfXml(RdfXmlSerializer::new().serialize_to_write(writer)) + } }, - }) + } } } @@ -71,7 +71,7 @@ impl GraphSerializer { /// use oxigraph::model::*; /// /// let mut buffer = Vec::new(); -/// let mut writer = GraphSerializer::from_format(GraphFormat::NTriples).triple_writer(&mut buffer)?; +/// let mut writer = GraphSerializer::from_format(GraphFormat::NTriples).triple_writer(&mut buffer); /// writer.write(&Triple { /// subject: NamedNode::new("http://example.com/s")?.into(), /// predicate: NamedNode::new("http://example.com/p")?, @@ -90,7 +90,7 @@ pub struct TripleWriter { enum TripleWriterKind { NTriples(ToWriteNTriplesWriter), Turtle(ToWriteTurtleWriter), - RdfXml(RdfXmlFormatter), + RdfXml(ToWriteRdfXmlWriter), } impl TripleWriter { @@ -99,54 +99,7 @@ impl TripleWriter { match &mut self.formatter { TripleWriterKind::NTriples(writer) => writer.write_triple(triple), TripleWriterKind::Turtle(writer) => writer.write_triple(triple), - TripleWriterKind::RdfXml(formatter) => { - let triple = triple.into(); - formatter.format(&rio::Triple { - subject: match triple.subject { - SubjectRef::NamedNode(node) => rio::NamedNode { iri: node.as_str() }.into(), - SubjectRef::BlankNode(node) => rio::BlankNode { id: node.as_str() }.into(), - SubjectRef::Triple(_) => { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "RDF/XML does not support RDF-star yet", - )) - } - }, - predicate: rio::NamedNode { - iri: triple.predicate.as_str(), - }, - object: match triple.object { - TermRef::NamedNode(node) => rio::NamedNode { iri: node.as_str() }.into(), - TermRef::BlankNode(node) => rio::BlankNode { id: node.as_str() }.into(), - TermRef::Literal(literal) => if literal.is_plain() { - if let Some(language) = literal.language() { - rio::Literal::LanguageTaggedString { - value: literal.value(), - language, - } - } else { - rio::Literal::Simple { - value: literal.value(), - } - } - } else { - rio::Literal::Typed { - value: literal.value(), - datatype: rio::NamedNode { - iri: literal.datatype().as_str(), - }, - } - } - .into(), - TermRef::Triple(_) => { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "RDF/XML does not support RDF-star yet", - )) - } - }, - }) - } + TripleWriterKind::RdfXml(writer) => writer.write_triple(triple), } } @@ -155,7 +108,7 @@ impl TripleWriter { match self.formatter { TripleWriterKind::NTriples(writer) => writer.finish().flush(), TripleWriterKind::Turtle(writer) => writer.finish()?.flush(), - TripleWriterKind::RdfXml(formatter) => formatter.finish()?.flush(), //TODO: remove flush when the next version of Rio is going to be released + TripleWriterKind::RdfXml(formatter) => formatter.finish()?.flush(), } } } diff --git a/lib/src/sparql/model.rs b/lib/src/sparql/model.rs index b5b1a650..1aec94a1 100644 --- a/lib/src/sparql/model.rs +++ b/lib/src/sparql/model.rs @@ -115,7 +115,7 @@ impl QueryResults { format: GraphFormat, ) -> Result<(), EvaluationError> { if let Self::Graph(triples) = self { - let mut writer = GraphSerializer::from_format(format).triple_writer(write)?; + let mut writer = GraphSerializer::from_format(format).triple_writer(write); for triple in triples { writer.write(&triple?)?; } diff --git a/lib/src/store.rs b/lib/src/store.rs index beb8cfd1..c174bcb1 100644 --- a/lib/src/store.rs +++ b/lib/src/store.rs @@ -616,7 +616,7 @@ impl Store { format: GraphFormat, from_graph_name: impl Into>, ) -> Result<(), SerializerError> { - let mut writer = GraphSerializer::from_format(format).triple_writer(writer)?; + let mut writer = GraphSerializer::from_format(format).triple_writer(writer); for quad in self.quads_for_pattern(None, None, None, Some(from_graph_name.into())) { writer.write(quad?.as_ref())?; } diff --git a/python/src/io.rs b/python/src/io.rs index 35bb00eb..382a8c79 100644 --- a/python/src/io.rs +++ b/python/src/io.rs @@ -125,9 +125,7 @@ pub fn serialize(input: &PyAny, output: PyObject, mime_type: &str, py: Python<'_ PyWritable::from_data(output) }; if let Some(graph_format) = GraphFormat::from_media_type(mime_type) { - let mut writer = GraphSerializer::from_format(graph_format) - .triple_writer(output) - .map_err(map_io_err)?; + let mut writer = GraphSerializer::from_format(graph_format).triple_writer(output); for i in input.iter()? { writer .write(&*i?.extract::>()?) diff --git a/server/src/main.rs b/server/src/main.rs index 41c77ba8..7474cae4 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -512,9 +512,11 @@ pub fn main() -> anyhow::Result<()> { } writer.finish()?; } else { - let stdout = stdout(); // Not needed in Rust 1.61 let mut writer = QueryResultsSerializer::from_format(format) - .solutions_writer(stdout.lock(), solutions.variables().to_vec())?; + .solutions_writer( + stdout().lock(), + solutions.variables().to_vec(), + )?; for solution in solutions { writer.write(&solution?)?; } @@ -570,15 +572,14 @@ pub fn main() -> anyhow::Result<()> { }; if let Some(results_file) = results_file { let mut writer = GraphSerializer::from_format(format) - .triple_writer(BufWriter::new(File::create(results_file)?))?; + .triple_writer(BufWriter::new(File::create(results_file)?)); for triple in triples { writer.write(triple?.as_ref())?; } writer.finish()?; } else { - let stdout = stdout(); // Not needed in Rust 1.61 - let mut writer = GraphSerializer::from_format(format) - .triple_writer(stdout.lock())?; + let mut writer = + GraphSerializer::from_format(format).triple_writer(stdout().lock()); for triple in triples { writer.write(triple?.as_ref())?; } @@ -926,7 +927,7 @@ fn handle_request( ReadForWrite::build_response( move |w| { Ok(( - GraphSerializer::from_format(format).triple_writer(w)?, + GraphSerializer::from_format(format).triple_writer(w), triples, )) }, @@ -1232,7 +1233,7 @@ fn evaluate_sparql_query( ReadForWrite::build_response( move |w| { Ok(( - GraphSerializer::from_format(format).triple_writer(w)?, + GraphSerializer::from_format(format).triple_writer(w), triples, )) },