1use std::fs::File;
2use std::io::Read;
3use std::path::Path;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum SemanticFormat {
8 NTriples,
9 NTriplesStar,
10 NQuads,
11 NQuadsStar,
12 Turtle,
13 TurtleStar,
14 TriG,
15 TriGStar,
16 N3,
17 RdfXml,
18 JsonLd,
19 JsonLdStar,
20 CborLd,
21 Kml,
22 Chk,
23 Q42,
24 AgentIntentJsonl,
25 Mesh,
27}
28
29impl SemanticFormat {
30 pub fn label(self) -> &'static str {
31 match self {
32 SemanticFormat::NTriples => "N-Triples",
33 SemanticFormat::NTriplesStar => "N-Triples-Star",
34 SemanticFormat::NQuads => "N-Quads",
35 SemanticFormat::NQuadsStar => "N-Quads-Star",
36 SemanticFormat::Turtle => "Turtle",
37 SemanticFormat::TurtleStar => "Turtle-Star",
38 SemanticFormat::TriG => "TriG",
39 SemanticFormat::TriGStar => "TriG-Star",
40 SemanticFormat::N3 => "N3",
41 SemanticFormat::RdfXml => "RDF/XML",
42 SemanticFormat::JsonLd => "JSON-LD",
43 SemanticFormat::JsonLdStar => "JSON-LD-Star",
44 SemanticFormat::CborLd => "CBOR-LD",
45 SemanticFormat::Kml => "KML",
46 SemanticFormat::Chk => "CHK",
47 SemanticFormat::Q42 => "Q42",
48 SemanticFormat::AgentIntentJsonl => "Agent-Intent-JSONL",
49 SemanticFormat::Mesh => "Mesh (OBJ/STL/GLB)",
50 }
51 }
52}
53
54pub fn detect_format(path: &Path) -> Option<SemanticFormat> {
61 let ext = path
62 .extension()
63 .and_then(|e| e.to_str())
64 .map(|s| s.to_ascii_lowercase());
65
66 let mut magic = [0u8; 16];
68 let magic_len = File::open(path)
69 .ok()
70 .and_then(|mut f| f.read(&mut magic).ok())
71 .unwrap_or(0);
72 let magic = &magic[..magic_len];
73
74 if magic.len() >= 3 && &magic[..3] == b"Q42" {
78 return Some(SemanticFormat::Q42);
79 }
80 if magic.len() >= 4 && &magic[..4] == b"QCHK" {
82 return Some(SemanticFormat::Chk);
83 }
84 if magic.starts_with(b"glTF") {
86 return Some(SemanticFormat::Mesh);
87 }
88 if magic.len() >= 3 && magic[0] == 0xd9 && magic[1] == 0xd9 && magic[2] == 0xf7 {
90 return Some(SemanticFormat::CborLd);
91 }
92 if magic.len() >= 1 && (0xa0..=0xb7).contains(&magic[0]) {
94 if matches!(ext.as_deref(), Some("cbor") | Some("cborld")) {
95 return Some(SemanticFormat::CborLd);
96 }
97 }
98
99 let starts_xml =
101 magic.starts_with(b"<?xml") || magic.starts_with(b"<rdf:") || magic.starts_with(b"<RDF:");
102 if starts_xml {
103 return match ext.as_deref() {
104 Some("kml") => Some(SemanticFormat::Kml),
105 _ => Some(SemanticFormat::RdfXml),
106 };
107 }
108
109 if magic.first().copied() == Some(b'{') || magic.first().copied() == Some(b'[') {
111 return match ext.as_deref() {
112 Some("jsonld-star") | Some("json-ld-star") => Some(SemanticFormat::JsonLdStar),
113 Some("jsonl") => Some(SemanticFormat::AgentIntentJsonl),
114 _ => Some(SemanticFormat::JsonLd),
115 };
116 }
117
118 match ext.as_deref() {
120 Some("nt") => Some(SemanticFormat::NTriples),
121 Some("nts") | Some("nt-star") => Some(SemanticFormat::NTriplesStar),
122 Some("nq") => Some(SemanticFormat::NQuads),
123 Some("nqs") | Some("nq-star") => Some(SemanticFormat::NQuadsStar),
124 Some("ttl") => Some(SemanticFormat::Turtle),
125 Some("ttls") | Some("ttl-star") => Some(SemanticFormat::TurtleStar),
126 Some("trig") => Some(SemanticFormat::TriG),
127 Some("trigs") | Some("trig-star") => Some(SemanticFormat::TriGStar),
128 Some("n3") => Some(SemanticFormat::N3),
129 Some("rdf") | Some("owl") => Some(SemanticFormat::RdfXml),
130 Some("xml") => Some(SemanticFormat::RdfXml),
131 Some("jsonld") | Some("json-ld") | Some("json") => Some(SemanticFormat::JsonLd),
132 Some("jsonl") => Some(SemanticFormat::AgentIntentJsonl),
133 Some("cbor") | Some("cborld") => Some(SemanticFormat::CborLd),
134 Some("kml") => Some(SemanticFormat::Kml),
135 Some("chk") | Some("qchk") => Some(SemanticFormat::Chk),
136 Some("q42") => Some(SemanticFormat::Q42),
137 Some("obj") | Some("stl") | Some("glb") | Some("gltf") => Some(SemanticFormat::Mesh),
138 _ => None,
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use std::io::Write;
146
147 fn tmp_file(name: &str, content: &[u8]) -> std::path::PathBuf {
148 let path = std::env::temp_dir().join(name);
149 let mut f = std::fs::File::create(&path).expect("create tmp file");
150 f.write_all(content).expect("write tmp file");
151 path
152 }
153
154 #[test]
155 fn detect_by_extension_nt() {
156 let p = tmp_file("test_detect.nt", b"<s> <p> <o> .\n");
157 assert_eq!(detect_format(&p), Some(SemanticFormat::NTriples));
158 }
159
160 #[test]
161 fn detect_by_extension_ttl() {
162 let p = tmp_file("test_detect.ttl", b"@prefix ex: <http://ex.org/> .\n");
163 assert_eq!(detect_format(&p), Some(SemanticFormat::Turtle));
164 }
165
166 #[test]
167 fn detect_by_extension_jsonld() {
168 let p = tmp_file("test_detect.jsonld", b"{\"@context\": {}}");
169 assert_eq!(detect_format(&p), Some(SemanticFormat::JsonLd));
170 }
171
172 #[test]
173 fn detect_by_magic_q42() {
174 let p = tmp_file("test_detect_magic.bin", b"Q42V\x00\x00\x00\x00");
175 assert_eq!(detect_format(&p), Some(SemanticFormat::Q42));
176 }
177
178 #[test]
179 fn detect_by_magic_xml_rdf() {
180 let p = tmp_file("test_detect_magic.rdf", b"<?xml version=\"1.0\"?><rdf:RDF");
181 assert_eq!(detect_format(&p), Some(SemanticFormat::RdfXml));
182 }
183
184 #[test]
185 fn detect_by_magic_kml() {
186 let p = tmp_file("test_detect_magic.kml", b"<?xml version=\"1.0\"?><kml>");
187 assert_eq!(detect_format(&p), Some(SemanticFormat::Kml));
188 }
189
190 #[test]
191 fn detect_by_magic_cbor_ld() {
192 let p = tmp_file("test_detect_magic.cbor", b"\xd9\xd9\xf7\xa1");
193 assert_eq!(detect_format(&p), Some(SemanticFormat::CborLd));
194 }
195
196 #[test]
197 fn detect_json_content_without_ld_extension() {
198 let p = tmp_file(
199 "test_detect_json.json",
200 b"{\"@context\": {}, \"@id\": \"x\"}",
201 );
202 assert_eq!(detect_format(&p), Some(SemanticFormat::JsonLd));
203 }
204
205 #[test]
206 fn detect_unknown_returns_none() {
207 let p = tmp_file("test_detect.xyz", b"some unknown data");
208 assert_eq!(detect_format(&p), None);
209 }
210
211 #[test]
212 fn label_round_trips() {
213 assert_eq!(SemanticFormat::NTriples.label(), "N-Triples");
214 assert_eq!(SemanticFormat::TurtleStar.label(), "Turtle-Star");
215 assert_eq!(SemanticFormat::JsonLdStar.label(), "JSON-LD-Star");
216 }
217}