Skip to main content

ontologos_parser/
lib.rs

1//! OWL and RDF syntax parsers for OntoLogos.
2//!
3//! v0.2 loads OWL/XML, RDF/XML, Turtle, and OWL Functional Syntax into
4//! [`ontologos_core::Ontology`] via [`load_ontology`].
5//!
6//! # Example
7//!
8//! ```no_run
9//! use ontologos_parser::load_ontology;
10//!
11//! let ontology = load_ontology(std::path::Path::new("ontology.owl"))?;
12//! println!("axioms: {}", ontology.axiom_count());
13//! # Ok::<(), ontologos_parser::Error>(())
14//! ```
15//!
16//! See [load guide](https://github.com/eddiethedean/ontologos/blob/main/docs/getting-started/load-owl-file.md).
17
18#![warn(missing_docs)]
19
20mod error;
21mod limits;
22mod load;
23mod map;
24mod map_dl;
25mod map_swrl;
26mod rdf_preprocess;
27mod read;
28mod report;
29mod validate;
30
31pub use error::{Error, Result};
32pub use limits::ParseLimits;
33pub use load::{
34    load_ofn_from_str, load_ofn_from_str_with_limits, load_ofn_with_incremental,
35    load_ofn_with_incremental_and_limits, load_ontology, load_ontology_from_bytes,
36    load_ontology_from_bytes_lenient, load_ontology_from_bytes_with_limits, load_ontology_from_str,
37    load_ontology_from_str_lenient, load_ontology_in, load_ontology_lenient,
38    load_ontology_lenient_in, load_ontology_with_limits, load_ontology_with_limits_and_base,
39    validate_load_path,
40};
41pub use rdf_preprocess::{expand_xml_entities, expand_xml_entities_with_limit};
42pub use read::{detect_turtle_from_bytes, read_horned_owl_from_reader, sniff_file_header};
43pub use validate::{validate_loaded_ontology, validate_loaded_ontology_light};
44
45/// Supported ontology serialization formats.
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum Format {
48    /// OWL/XML syntax.
49    OwlXml,
50    /// RDF/XML syntax.
51    RdfXml,
52    /// Turtle / RDF Turtle.
53    Turtle,
54    /// OWL Functional Syntax (`.ofn`, `.func`).
55    Functional,
56}
57
58/// Detect format from file content bytes.
59#[must_use]
60pub fn detect_format_from_bytes(header: &[u8]) -> Option<Format> {
61    if detect_functional_from_bytes(header) {
62        return Some(Format::Functional);
63    }
64    if let Some(format) = detect_xml_format_from_bytes(header) {
65        return Some(format);
66    }
67    if detect_turtle_from_bytes(header) {
68        return Some(Format::Turtle);
69    }
70    None
71}
72
73fn strip_utf8_bom_bytes(header: &[u8]) -> &[u8] {
74    header.strip_prefix(&[0xEF, 0xBB, 0xBF]).unwrap_or(header)
75}
76
77fn trimmed_xml_header(header: &[u8]) -> Option<&str> {
78    let header = strip_utf8_bom_bytes(header);
79    std::str::from_utf8(header).ok().map(str::trim_start)
80}
81
82fn is_rdf_xml_bytes(header: &[u8]) -> bool {
83    let Some(text) = trimmed_xml_header(header) else {
84        return false;
85    };
86    if text.starts_with("<rdf:RDF") {
87        return true;
88    }
89    text.starts_with("<?xml") && text.contains("<rdf:RDF")
90}
91
92fn is_owl_xml_bytes(header: &[u8]) -> bool {
93    let Some(text) = trimmed_xml_header(header) else {
94        return false;
95    };
96    if text.contains("<rdf:RDF") {
97        return false;
98    }
99    if text.starts_with("<?xml") {
100        return text.contains("<Ontology ")
101            || text.contains(":Ontology ")
102            || text.contains("<owl:Ontology");
103    }
104    text.starts_with("<Ontology ")
105        || text.starts_with("<owl:Ontology")
106        || text.starts_with(":Ontology ")
107}
108
109fn detect_xml_format_from_bytes(header: &[u8]) -> Option<Format> {
110    let rdf = is_rdf_xml_bytes(header);
111    let owl = is_owl_xml_bytes(header);
112    match (rdf, owl) {
113        (true, _) => Some(Format::RdfXml),
114        (false, true) => Some(Format::OwlXml),
115        (false, false) => None,
116    }
117}
118
119/// Detect OWL Functional Syntax from a file header.
120#[must_use]
121pub fn detect_functional_from_bytes(header: &[u8]) -> bool {
122    let text = match std::str::from_utf8(header) {
123        Ok(t) => t.trim_start(),
124        Err(_) => return false,
125    };
126    text.starts_with("Prefix(") || text.starts_with("Ontology(")
127}
128
129/// Detect the most likely format from a file path and optional content sniffing.
130#[must_use]
131pub fn detect_format(path: &std::path::Path) -> Option<Format> {
132    match path.extension()?.to_str()? {
133        "owl" => sniff_xml_format(path),
134        "xml" => sniff_xml_format(path),
135        "rdf" => Some(Format::RdfXml),
136        "ttl" | "turtle" => Some(Format::Turtle),
137        "ofn" | "func" => Some(Format::Functional),
138        _ => None,
139    }
140}
141
142fn sniff_xml_format(path: &std::path::Path) -> Option<Format> {
143    let header = sniff_file_header(path, 4096).ok()?;
144    detect_format_from_bytes(&header)
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150    use std::io::Write;
151    use std::path::Path;
152
153    #[test]
154    fn detect_format_by_extension() {
155        assert_eq!(detect_format(Path::new("p.rdf")), Some(Format::RdfXml));
156        assert_eq!(detect_format(Path::new("p.ttl")), Some(Format::Turtle));
157        assert_eq!(detect_format(Path::new("p.turtle")), Some(Format::Turtle));
158        assert_eq!(detect_format(Path::new("p.ofn")), Some(Format::Functional));
159        assert_eq!(detect_format(Path::new("p.func")), Some(Format::Functional));
160        assert_eq!(detect_format(Path::new("p.txt")), None);
161        assert_eq!(detect_format(Path::new("noext")), None);
162    }
163
164    #[test]
165    fn detect_functional_from_bytes_header() {
166        let header = b"Prefix(:=<http://example.org/>)\nOntology(<http://example.org/o>)";
167        assert!(detect_functional_from_bytes(header));
168        assert_eq!(detect_format_from_bytes(header), Some(Format::Functional));
169    }
170
171    #[test]
172    fn detect_format_from_bytes_owl_xml() {
173        let header = br#"<?xml version="1.0"?><Ontology xmlns="http://www.w3.org/2002/07/owl#"/>"#;
174        assert_eq!(detect_format_from_bytes(header), Some(Format::OwlXml));
175    }
176
177    #[test]
178    fn detect_format_from_bytes_rdf_xml() {
179        let header = br#"<?xml version="1.0"?><rdf:RDF/>"#;
180        assert_eq!(detect_format_from_bytes(header), Some(Format::RdfXml));
181    }
182
183    #[test]
184    fn detect_format_from_bytes_rejects_non_root_rdf_marker() {
185        let header = br#"<config><rdf:RDF/></config>"#;
186        assert_eq!(detect_format_from_bytes(header), None);
187    }
188
189    #[test]
190    fn detect_format_from_bytes_rdf_with_embedded_owl_ontology() {
191        let header = br#"<?xml version="1.0"?>
192<rdf:RDF xmlns:owl="http://www.w3.org/2002/07/owl#">
193  <owl:Ontology rdf:about="http://example.org/o"/>
194</rdf:RDF>"#;
195        assert_eq!(detect_format_from_bytes(header), Some(Format::RdfXml));
196    }
197
198    #[test]
199    fn plain_xml_extension_without_sniff_returns_none() {
200        let path = std::env::temp_dir().join(format!(
201            "ontologos_parser_test_{}_{}.xml",
202            std::process::id(),
203            std::time::SystemTime::now()
204                .duration_since(std::time::UNIX_EPOCH)
205                .expect("time")
206                .as_nanos()
207        ));
208        {
209            let mut file = std::fs::File::create(&path).expect("create");
210            file.write_all(b"<config><item/></config>").expect("write");
211        }
212        assert_eq!(detect_format(&path), None);
213        let _ = std::fs::remove_file(&path);
214    }
215}