1#[allow(dead_code)]
4#[derive(Clone, PartialEq, Debug)]
5pub enum DetectedFormat {
6 Glb,
7 Gltf,
8 Obj,
9 Fbx,
10 Usdz,
11 Ply,
12 Stl,
13 Collada,
14 Png,
15 Jpeg,
16 Bmp,
17 Tga,
18 Hdr,
19 Exr,
20 Json,
21 Xml,
22 Csv,
23 Binary,
24 Unknown,
25}
26
27#[allow(dead_code)]
28pub struct FormatInfo {
29 pub format: DetectedFormat,
30 pub confidence: f32,
31 pub mime_type: String,
32 pub extensions: Vec<String>,
33}
34
35#[allow(dead_code)]
36pub fn glb_magic() -> [u8; 4] {
37 [0x67, 0x6C, 0x54, 0x46]
38}
39
40#[allow(dead_code)]
41pub fn png_magic() -> [u8; 8] {
42 [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]
43}
44
45#[allow(dead_code)]
46pub fn detect_from_bytes(data: &[u8]) -> DetectedFormat {
47 if data.len() >= 4 {
48 let magic4 = &data[..4];
49 if magic4 == glb_magic() {
51 return DetectedFormat::Glb;
52 }
53 if magic4 == b"Kayd" {
55 return DetectedFormat::Fbx;
56 }
57 if &data[..2] == b"BM" {
59 return DetectedFormat::Bmp;
60 }
61 if data.len() >= 3 && data[0] == 0xFF && data[1] == 0xD8 && data[2] == 0xFF {
63 return DetectedFormat::Jpeg;
64 }
65 if magic4 == [0x76, 0x2F, 0x31, 0x01] {
67 return DetectedFormat::Exr;
68 }
69 if &data[..4] == b"PK\x03\x04" {
71 return DetectedFormat::Usdz;
72 }
73 }
74 if data.len() >= 8 {
75 let magic8 = &data[..8];
76 if magic8 == png_magic() {
77 return DetectedFormat::Png;
78 }
79 }
80 if data.len() >= 6 {
82 if data.starts_with(b"ply\n") || data.starts_with(b"ply\r") {
83 return DetectedFormat::Ply;
84 }
85 if data.starts_with(b"solid ") || data.starts_with(b"solid\n") {
86 return DetectedFormat::Stl;
87 }
88 if data.starts_with(b"<?xml") || data.starts_with(b"<COLL") {
89 if data.windows(8).any(|w| w == b"COLLADA>") {
90 return DetectedFormat::Collada;
91 }
92 return DetectedFormat::Xml;
93 }
94 if data.starts_with(b"{") || data.starts_with(b"[") {
95 return DetectedFormat::Json;
96 }
97 if data.starts_with(b"v ") || data.starts_with(b"# ") || data.starts_with(b"mtllib") {
98 return DetectedFormat::Obj;
99 }
100 if data.starts_with(b"#EXTM3U")
101 || data.starts_with(b"#RADIANCE")
102 || data.starts_with(b"#?RADIANCE")
103 {
104 return DetectedFormat::Hdr;
105 }
106 }
107 if data.is_empty() {
108 return DetectedFormat::Unknown;
109 }
110 let sample = &data[..data.len().min(512)];
112 let non_printable = sample
113 .iter()
114 .filter(|&&b| b < 0x09 || (b > 0x0D && b < 0x20))
115 .count();
116 if non_printable > sample.len() / 10 {
117 DetectedFormat::Binary
118 } else {
119 DetectedFormat::Unknown
120 }
121}
122
123#[allow(dead_code)]
124pub fn detect_from_extension(ext: &str) -> DetectedFormat {
125 extension_to_format(ext)
126}
127
128#[allow(dead_code)]
129pub fn detect_from_path(path: &str) -> DetectedFormat {
130 if let Some(dot_pos) = path.rfind('.') {
131 let ext = &path[dot_pos..];
132 let fmt = extension_to_format(ext);
133 if fmt != DetectedFormat::Unknown {
134 return fmt;
135 }
136 }
137 DetectedFormat::Unknown
138}
139
140#[allow(dead_code)]
141pub fn extension_to_format(ext: &str) -> DetectedFormat {
142 match ext.to_lowercase().as_str() {
143 ".glb" => DetectedFormat::Glb,
144 ".gltf" => DetectedFormat::Gltf,
145 ".obj" => DetectedFormat::Obj,
146 ".fbx" => DetectedFormat::Fbx,
147 ".usdz" => DetectedFormat::Usdz,
148 ".ply" => DetectedFormat::Ply,
149 ".stl" => DetectedFormat::Stl,
150 ".dae" => DetectedFormat::Collada,
151 ".png" => DetectedFormat::Png,
152 ".jpg" | ".jpeg" => DetectedFormat::Jpeg,
153 ".bmp" => DetectedFormat::Bmp,
154 ".tga" => DetectedFormat::Tga,
155 ".hdr" => DetectedFormat::Hdr,
156 ".exr" => DetectedFormat::Exr,
157 ".json" => DetectedFormat::Json,
158 ".xml" => DetectedFormat::Xml,
159 ".csv" => DetectedFormat::Csv,
160 _ => DetectedFormat::Unknown,
161 }
162}
163
164#[allow(dead_code)]
165pub fn format_info(fmt: &DetectedFormat) -> FormatInfo {
166 match fmt {
167 DetectedFormat::Glb => FormatInfo {
168 format: DetectedFormat::Glb,
169 confidence: 1.0,
170 mime_type: "model/gltf-binary".to_string(),
171 extensions: vec![".glb".to_string()],
172 },
173 DetectedFormat::Gltf => FormatInfo {
174 format: DetectedFormat::Gltf,
175 confidence: 1.0,
176 mime_type: "model/gltf+json".to_string(),
177 extensions: vec![".gltf".to_string()],
178 },
179 DetectedFormat::Obj => FormatInfo {
180 format: DetectedFormat::Obj,
181 confidence: 0.9,
182 mime_type: "model/obj".to_string(),
183 extensions: vec![".obj".to_string()],
184 },
185 DetectedFormat::Fbx => FormatInfo {
186 format: DetectedFormat::Fbx,
187 confidence: 1.0,
188 mime_type: "application/octet-stream".to_string(),
189 extensions: vec![".fbx".to_string()],
190 },
191 DetectedFormat::Usdz => FormatInfo {
192 format: DetectedFormat::Usdz,
193 confidence: 0.9,
194 mime_type: "model/vnd.usdz+zip".to_string(),
195 extensions: vec![".usdz".to_string()],
196 },
197 DetectedFormat::Ply => FormatInfo {
198 format: DetectedFormat::Ply,
199 confidence: 1.0,
200 mime_type: "application/octet-stream".to_string(),
201 extensions: vec![".ply".to_string()],
202 },
203 DetectedFormat::Stl => FormatInfo {
204 format: DetectedFormat::Stl,
205 confidence: 0.9,
206 mime_type: "model/stl".to_string(),
207 extensions: vec![".stl".to_string()],
208 },
209 DetectedFormat::Collada => FormatInfo {
210 format: DetectedFormat::Collada,
211 confidence: 1.0,
212 mime_type: "model/vnd.collada+xml".to_string(),
213 extensions: vec![".dae".to_string()],
214 },
215 DetectedFormat::Png => FormatInfo {
216 format: DetectedFormat::Png,
217 confidence: 1.0,
218 mime_type: "image/png".to_string(),
219 extensions: vec![".png".to_string()],
220 },
221 DetectedFormat::Jpeg => FormatInfo {
222 format: DetectedFormat::Jpeg,
223 confidence: 1.0,
224 mime_type: "image/jpeg".to_string(),
225 extensions: vec![".jpg".to_string(), ".jpeg".to_string()],
226 },
227 DetectedFormat::Bmp => FormatInfo {
228 format: DetectedFormat::Bmp,
229 confidence: 1.0,
230 mime_type: "image/bmp".to_string(),
231 extensions: vec![".bmp".to_string()],
232 },
233 DetectedFormat::Tga => FormatInfo {
234 format: DetectedFormat::Tga,
235 confidence: 0.7,
236 mime_type: "image/x-tga".to_string(),
237 extensions: vec![".tga".to_string()],
238 },
239 DetectedFormat::Hdr => FormatInfo {
240 format: DetectedFormat::Hdr,
241 confidence: 0.9,
242 mime_type: "image/vnd.radiance".to_string(),
243 extensions: vec![".hdr".to_string()],
244 },
245 DetectedFormat::Exr => FormatInfo {
246 format: DetectedFormat::Exr,
247 confidence: 1.0,
248 mime_type: "image/x-exr".to_string(),
249 extensions: vec![".exr".to_string()],
250 },
251 DetectedFormat::Json => FormatInfo {
252 format: DetectedFormat::Json,
253 confidence: 0.8,
254 mime_type: "application/json".to_string(),
255 extensions: vec![".json".to_string()],
256 },
257 DetectedFormat::Xml => FormatInfo {
258 format: DetectedFormat::Xml,
259 confidence: 0.8,
260 mime_type: "application/xml".to_string(),
261 extensions: vec![".xml".to_string()],
262 },
263 DetectedFormat::Csv => FormatInfo {
264 format: DetectedFormat::Csv,
265 confidence: 0.7,
266 mime_type: "text/csv".to_string(),
267 extensions: vec![".csv".to_string()],
268 },
269 DetectedFormat::Binary => FormatInfo {
270 format: DetectedFormat::Binary,
271 confidence: 0.5,
272 mime_type: "application/octet-stream".to_string(),
273 extensions: vec![],
274 },
275 DetectedFormat::Unknown => FormatInfo {
276 format: DetectedFormat::Unknown,
277 confidence: 0.0,
278 mime_type: "application/octet-stream".to_string(),
279 extensions: vec![],
280 },
281 }
282}
283
284#[allow(dead_code)]
285pub fn is_3d_format(fmt: &DetectedFormat) -> bool {
286 matches!(
287 fmt,
288 DetectedFormat::Glb
289 | DetectedFormat::Gltf
290 | DetectedFormat::Obj
291 | DetectedFormat::Fbx
292 | DetectedFormat::Usdz
293 | DetectedFormat::Ply
294 | DetectedFormat::Stl
295 | DetectedFormat::Collada
296 )
297}
298
299#[allow(dead_code)]
300pub fn is_image_format(fmt: &DetectedFormat) -> bool {
301 matches!(
302 fmt,
303 DetectedFormat::Png
304 | DetectedFormat::Jpeg
305 | DetectedFormat::Bmp
306 | DetectedFormat::Tga
307 | DetectedFormat::Hdr
308 | DetectedFormat::Exr
309 )
310}
311
312#[allow(dead_code)]
313pub fn is_text_format(fmt: &DetectedFormat) -> bool {
314 matches!(
315 fmt,
316 DetectedFormat::Json
317 | DetectedFormat::Xml
318 | DetectedFormat::Csv
319 | DetectedFormat::Gltf
320 | DetectedFormat::Obj
321 | DetectedFormat::Collada
322 )
323}
324
325#[allow(dead_code)]
326pub fn mime_type(fmt: &DetectedFormat) -> &'static str {
327 match fmt {
328 DetectedFormat::Glb => "model/gltf-binary",
329 DetectedFormat::Gltf => "model/gltf+json",
330 DetectedFormat::Obj => "model/obj",
331 DetectedFormat::Fbx => "application/octet-stream",
332 DetectedFormat::Usdz => "model/vnd.usdz+zip",
333 DetectedFormat::Ply => "application/octet-stream",
334 DetectedFormat::Stl => "model/stl",
335 DetectedFormat::Collada => "model/vnd.collada+xml",
336 DetectedFormat::Png => "image/png",
337 DetectedFormat::Jpeg => "image/jpeg",
338 DetectedFormat::Bmp => "image/bmp",
339 DetectedFormat::Tga => "image/x-tga",
340 DetectedFormat::Hdr => "image/vnd.radiance",
341 DetectedFormat::Exr => "image/x-exr",
342 DetectedFormat::Json => "application/json",
343 DetectedFormat::Xml => "application/xml",
344 DetectedFormat::Csv => "text/csv",
345 DetectedFormat::Binary | DetectedFormat::Unknown => "application/octet-stream",
346 }
347}
348
349#[allow(dead_code)]
350pub fn format_name(fmt: &DetectedFormat) -> &'static str {
351 match fmt {
352 DetectedFormat::Glb => "GLB",
353 DetectedFormat::Gltf => "glTF",
354 DetectedFormat::Obj => "OBJ",
355 DetectedFormat::Fbx => "FBX",
356 DetectedFormat::Usdz => "USDZ",
357 DetectedFormat::Ply => "PLY",
358 DetectedFormat::Stl => "STL",
359 DetectedFormat::Collada => "Collada",
360 DetectedFormat::Png => "PNG",
361 DetectedFormat::Jpeg => "JPEG",
362 DetectedFormat::Bmp => "BMP",
363 DetectedFormat::Tga => "TGA",
364 DetectedFormat::Hdr => "HDR",
365 DetectedFormat::Exr => "EXR",
366 DetectedFormat::Json => "JSON",
367 DetectedFormat::Xml => "XML",
368 DetectedFormat::Csv => "CSV",
369 DetectedFormat::Binary => "Binary",
370 DetectedFormat::Unknown => "Unknown",
371 }
372}
373
374#[allow(dead_code)]
375pub fn all_3d_formats() -> Vec<DetectedFormat> {
376 vec![
377 DetectedFormat::Glb,
378 DetectedFormat::Gltf,
379 DetectedFormat::Obj,
380 DetectedFormat::Fbx,
381 DetectedFormat::Usdz,
382 DetectedFormat::Ply,
383 DetectedFormat::Stl,
384 DetectedFormat::Collada,
385 ]
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391
392 #[test]
393 fn test_detect_from_bytes_glb() {
394 let magic = glb_magic();
395 let data: Vec<u8> = magic
396 .iter()
397 .chain(b"\x00\x00\x00\x00".iter())
398 .cloned()
399 .collect();
400 assert_eq!(detect_from_bytes(&data), DetectedFormat::Glb);
401 }
402
403 #[test]
404 fn test_detect_from_bytes_png() {
405 let magic = png_magic();
406 let data: Vec<u8> = magic.iter().chain(b"\x00\x00".iter()).cloned().collect();
407 assert_eq!(detect_from_bytes(&data), DetectedFormat::Png);
408 }
409
410 #[test]
411 fn test_detect_from_bytes_jpeg() {
412 let data: Vec<u8> = vec![0xFF, 0xD8, 0xFF, 0xE0, 0x00];
413 assert_eq!(detect_from_bytes(&data), DetectedFormat::Jpeg);
414 }
415
416 #[test]
417 fn test_detect_from_bytes_ply() {
418 let data = b"ply\nformat ascii 1.0\n";
419 assert_eq!(detect_from_bytes(data), DetectedFormat::Ply);
420 }
421
422 #[test]
423 fn test_detect_from_bytes_json() {
424 let data = b"{\"key\": \"value\"}";
425 assert_eq!(detect_from_bytes(data), DetectedFormat::Json);
426 }
427
428 #[test]
429 fn test_detect_from_extension_obj() {
430 assert_eq!(detect_from_extension(".obj"), DetectedFormat::Obj);
431 }
432
433 #[test]
434 fn test_detect_from_extension_glb() {
435 assert_eq!(detect_from_extension(".glb"), DetectedFormat::Glb);
436 }
437
438 #[test]
439 fn test_detect_from_extension_unknown() {
440 assert_eq!(detect_from_extension(".xyz123"), DetectedFormat::Unknown);
441 }
442
443 #[test]
444 fn test_detect_from_path_ply() {
445 assert_eq!(detect_from_path("model.ply"), DetectedFormat::Ply);
446 }
447
448 #[test]
449 fn test_detect_from_path_unknown() {
450 assert_eq!(detect_from_path("file"), DetectedFormat::Unknown);
451 }
452
453 #[test]
454 fn test_is_3d_format_glb() {
455 assert!(is_3d_format(&DetectedFormat::Glb));
456 assert!(is_3d_format(&DetectedFormat::Obj));
457 assert!(!is_3d_format(&DetectedFormat::Png));
458 }
459
460 #[test]
461 fn test_is_image_format_png() {
462 assert!(is_image_format(&DetectedFormat::Png));
463 assert!(is_image_format(&DetectedFormat::Jpeg));
464 assert!(!is_image_format(&DetectedFormat::Glb));
465 }
466
467 #[test]
468 fn test_mime_type() {
469 assert_eq!(mime_type(&DetectedFormat::Png), "image/png");
470 assert_eq!(mime_type(&DetectedFormat::Glb), "model/gltf-binary");
471 assert_eq!(mime_type(&DetectedFormat::Json), "application/json");
472 }
473
474 #[test]
475 fn test_format_name() {
476 assert_eq!(format_name(&DetectedFormat::Glb), "GLB");
477 assert_eq!(format_name(&DetectedFormat::Png), "PNG");
478 assert_eq!(format_name(&DetectedFormat::Unknown), "Unknown");
479 }
480
481 #[test]
482 fn test_all_3d_formats_non_empty() {
483 let fmts = all_3d_formats();
484 assert!(!fmts.is_empty());
485 assert!(fmts.contains(&DetectedFormat::Glb));
486 assert!(fmts.contains(&DetectedFormat::Ply));
487 }
488
489 #[test]
490 fn test_glb_magic_length() {
491 let magic = glb_magic();
492 assert_eq!(magic.len(), 4);
493 assert_eq!(&magic, b"glTF");
494 }
495
496 #[test]
497 fn test_png_magic_length() {
498 let magic = png_magic();
499 assert_eq!(magic.len(), 8);
500 }
501
502 #[test]
503 fn test_is_text_format() {
504 assert!(is_text_format(&DetectedFormat::Json));
505 assert!(is_text_format(&DetectedFormat::Xml));
506 assert!(!is_text_format(&DetectedFormat::Glb));
507 }
508}