1use crate::error::{IoError, Result};
49use std::path::Path;
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
53pub enum DataFormat {
54 Hdf5,
56 NetCdf,
58 ArrowIpc,
60 Matlab,
62 Wav,
64 Npy,
66 Npz,
68 Png,
70 Jpeg,
72 Bmp,
74 Tiff,
76 Fits,
78 Csv,
80 Json,
82 Parquet,
84 Fasta,
86 MatrixMarket,
88 Arff,
90 MessagePack,
92 Zip,
94 FortranUnformatted,
96 Scirs2,
98 Unknown,
100}
101
102impl DataFormat {
103 pub fn name(&self) -> &'static str {
105 match self {
106 Self::Hdf5 => "HDF5",
107 Self::NetCdf => "NetCDF",
108 Self::ArrowIpc => "Arrow IPC",
109 Self::Matlab => "MATLAB v5",
110 Self::Wav => "WAV",
111 Self::Npy => "NumPy NPY",
112 Self::Npz => "NumPy NPZ",
113 Self::Png => "PNG",
114 Self::Jpeg => "JPEG",
115 Self::Bmp => "BMP",
116 Self::Tiff => "TIFF",
117 Self::Fits => "FITS",
118 Self::Csv => "CSV",
119 Self::Json => "JSON",
120 Self::Parquet => "Parquet",
121 Self::Fasta => "FASTA",
122 Self::MatrixMarket => "Matrix Market",
123 Self::Arff => "ARFF",
124 Self::MessagePack => "MessagePack",
125 Self::Zip => "ZIP",
126 Self::FortranUnformatted => "Fortran Unformatted",
127 Self::Scirs2 => "SciRS2",
128 Self::Unknown => "Unknown",
129 }
130 }
131
132 pub fn extensions(&self) -> &'static [&'static str] {
134 match self {
135 Self::Hdf5 => &["h5", "hdf5", "he5"],
136 Self::NetCdf => &["nc", "cdf", "nc3"],
137 Self::ArrowIpc => &["arrow", "feather", "ipc"],
138 Self::Matlab => &["mat"],
139 Self::Wav => &["wav"],
140 Self::Npy => &["npy"],
141 Self::Npz => &["npz"],
142 Self::Png => &["png"],
143 Self::Jpeg => &["jpg", "jpeg"],
144 Self::Bmp => &["bmp"],
145 Self::Tiff => &["tif", "tiff"],
146 Self::Fits => &["fits", "fit", "fts"],
147 Self::Csv => &["csv", "tsv", "txt"],
148 Self::Json => &["json", "geojson"],
149 Self::Parquet => &["parquet"],
150 Self::Fasta => &["fasta", "fa", "fna", "faa"],
151 Self::MatrixMarket => &["mtx"],
152 Self::Arff => &["arff"],
153 Self::MessagePack => &["msgpack", "mpk"],
154 Self::Zip => &["zip"],
155 Self::FortranUnformatted => &["unf"],
156 Self::Scirs2 => &["scirs2"],
157 Self::Unknown => &[],
158 }
159 }
160
161 pub fn is_text(&self) -> bool {
163 matches!(
164 self,
165 Self::Csv | Self::Json | Self::Fasta | Self::MatrixMarket | Self::Arff
166 )
167 }
168
169 pub fn supports_streaming(&self) -> bool {
171 matches!(
172 self,
173 Self::Csv | Self::Json | Self::ArrowIpc | Self::Fasta | Self::Wav | Self::MatrixMarket
174 )
175 }
176}
177
178impl std::fmt::Display for DataFormat {
179 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
180 write!(f, "{}", self.name())
181 }
182}
183
184#[derive(Debug, Clone)]
186pub struct FormatDetection {
187 pub format: DataFormat,
189 pub confidence: f64,
191 pub method: DetectionMethod,
193}
194
195#[derive(Debug, Clone, Copy, PartialEq, Eq)]
197pub enum DetectionMethod {
198 MagicBytes,
200 ContentSniffing,
202 Extension,
204 Combined,
206}
207
208pub fn detect_format<P: AsRef<Path>>(path: P) -> Result<DataFormat> {
213 let path = path.as_ref();
214
215 if !path.exists() {
216 return Err(IoError::FileNotFound(path.display().to_string()));
217 }
218
219 let file = std::fs::File::open(path)
221 .map_err(|e| IoError::FileError(format!("Cannot open '{}': {e}", path.display())))?;
222 let mut reader = std::io::BufReader::new(file);
223 let mut header = vec![0u8; 256];
224 let bytes_read = std::io::Read::read(&mut reader, &mut header)
225 .map_err(|e| IoError::FileError(format!("Cannot read '{}': {e}", path.display())))?;
226 header.truncate(bytes_read);
227
228 let ext = path
229 .extension()
230 .and_then(|e| e.to_str())
231 .map(|e| e.to_lowercase());
232
233 Ok(detect_format_from_bytes(&header, ext.as_deref()))
234}
235
236pub fn detect_format_detailed<P: AsRef<Path>>(path: P) -> Result<FormatDetection> {
238 let path = path.as_ref();
239
240 if !path.exists() {
241 return Err(IoError::FileNotFound(path.display().to_string()));
242 }
243
244 let file = std::fs::File::open(path)
245 .map_err(|e| IoError::FileError(format!("Cannot open '{}': {e}", path.display())))?;
246 let mut reader = std::io::BufReader::new(file);
247 let mut header = vec![0u8; 256];
248 let bytes_read = std::io::Read::read(&mut reader, &mut header)
249 .map_err(|e| IoError::FileError(format!("Cannot read '{}': {e}", path.display())))?;
250 header.truncate(bytes_read);
251
252 let ext = path
253 .extension()
254 .and_then(|e| e.to_str())
255 .map(|e| e.to_lowercase());
256
257 Ok(detect_format_detailed_from_bytes(&header, ext.as_deref()))
258}
259
260pub fn detect_format_from_bytes(data: &[u8], extension: Option<&str>) -> DataFormat {
265 if let Some(fmt) = detect_magic_bytes(data) {
267 return fmt;
268 }
269
270 if let Some(fmt) = detect_content(data) {
272 return fmt;
273 }
274
275 if let Some(ext) = extension {
277 if let Some(fmt) = detect_extension(ext) {
278 return fmt;
279 }
280 }
281
282 DataFormat::Unknown
283}
284
285pub fn detect_format_detailed_from_bytes(data: &[u8], extension: Option<&str>) -> FormatDetection {
287 if let Some(fmt) = detect_magic_bytes(data) {
289 let ext_agrees = extension
290 .and_then(|ext| detect_extension(ext))
291 .map_or(false, |ef| ef == fmt);
292
293 return FormatDetection {
294 format: fmt,
295 confidence: if ext_agrees { 1.0 } else { 0.95 },
296 method: if ext_agrees {
297 DetectionMethod::Combined
298 } else {
299 DetectionMethod::MagicBytes
300 },
301 };
302 }
303
304 if let Some(fmt) = detect_content(data) {
306 let ext_agrees = extension
307 .and_then(|ext| detect_extension(ext))
308 .map_or(false, |ef| ef == fmt);
309
310 return FormatDetection {
311 format: fmt,
312 confidence: if ext_agrees { 0.85 } else { 0.6 },
313 method: if ext_agrees {
314 DetectionMethod::Combined
315 } else {
316 DetectionMethod::ContentSniffing
317 },
318 };
319 }
320
321 if let Some(ext) = extension {
323 if let Some(fmt) = detect_extension(ext) {
324 return FormatDetection {
325 format: fmt,
326 confidence: 0.4,
327 method: DetectionMethod::Extension,
328 };
329 }
330 }
331
332 FormatDetection {
333 format: DataFormat::Unknown,
334 confidence: 0.0,
335 method: DetectionMethod::Extension,
336 }
337}
338
339fn detect_magic_bytes(data: &[u8]) -> Option<DataFormat> {
345 if data.len() < 4 {
346 return None;
347 }
348
349 if data.len() >= 8
351 && data[0] == 0x89
352 && data[1] == 0x48
353 && data[2] == 0x44
354 && data[3] == 0x46
355 && data[4] == 0x0d
356 && data[5] == 0x0a
357 && data[6] == 0x1a
358 && data[7] == 0x0a
359 {
360 return Some(DataFormat::Hdf5);
361 }
362
363 if data.len() >= 4
365 && data[0] == b'C'
366 && data[1] == b'D'
367 && data[2] == b'F'
368 && (data[3] == 0x01 || data[3] == 0x02)
369 {
370 return Some(DataFormat::NetCdf);
371 }
372
373 if data.len() >= 6 && &data[..6] == b"ARROW1" {
375 return Some(DataFormat::ArrowIpc);
376 }
377
378 if data.len() >= 10 && &data[..10] == b"MATLAB 5.0" {
380 return Some(DataFormat::Matlab);
381 }
382
383 if data.len() >= 12 && &data[..4] == b"RIFF" && &data[8..12] == b"WAVE" {
385 return Some(DataFormat::Wav);
386 }
387
388 if data.len() >= 6 && data[0] == 0x93 && &data[1..6] == b"NUMPY" {
390 return Some(DataFormat::Npy);
391 }
392
393 if data.len() >= 4 && &data[..4] == b"PAR1" {
395 return Some(DataFormat::Parquet);
396 }
397
398 if data.len() >= 8 && data[0] == 0x89 && data[1] == b'P' && data[2] == b'N' && data[3] == b'G' {
400 return Some(DataFormat::Png);
401 }
402
403 if data.len() >= 3 && data[0] == 0xFF && data[1] == 0xD8 && data[2] == 0xFF {
405 return Some(DataFormat::Jpeg);
406 }
407
408 if data.len() >= 2 && data[0] == b'B' && data[1] == b'M' {
410 return Some(DataFormat::Bmp);
411 }
412
413 if data.len() >= 4
415 && ((data[0] == b'I' && data[1] == b'I' && data[2] == 0x2A && data[3] == 0x00)
416 || (data[0] == b'M' && data[1] == b'M' && data[2] == 0x00 && data[3] == 0x2A))
417 {
418 return Some(DataFormat::Tiff);
419 }
420
421 if data.len() >= 9 && &data[..6] == b"SIMPLE" {
423 return Some(DataFormat::Fits);
424 }
425
426 if data.len() >= 4 && data[0] == b'P' && data[1] == b'K' && data[2] == 0x03 && data[3] == 0x04 {
428 return Some(DataFormat::Zip);
430 }
431
432 if data.len() >= 8 && &data[..6] == b"SCIRS2" {
434 return Some(DataFormat::Scirs2);
435 }
436
437 None
438}
439
440fn detect_content(data: &[u8]) -> Option<DataFormat> {
442 let text_data = if data.len() >= 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF {
444 &data[3..]
445 } else {
446 data
447 };
448
449 let text = std::str::from_utf8(text_data).ok()?;
451 let trimmed = text.trim();
452
453 if trimmed.is_empty() {
454 return None;
455 }
456
457 if trimmed.starts_with("%%MatrixMarket") {
459 return Some(DataFormat::MatrixMarket);
460 }
461
462 let upper = trimmed.to_uppercase();
464 if upper.starts_with("@RELATION") || upper.starts_with("% ") && upper.contains("@RELATION") {
465 return Some(DataFormat::Arff);
466 }
467
468 if trimmed.starts_with('{') || trimmed.starts_with('[') {
470 return Some(DataFormat::Json);
471 }
472
473 if trimmed.starts_with('>') {
475 let lines: Vec<&str> = trimmed.lines().take(5).collect();
476 if lines.len() >= 2 {
477 let second_line = lines[1].trim();
478 if second_line
479 .chars()
480 .all(|c| "ACGTUNRYWSMKHBVD.-*acgtunrywsmkhbvd".contains(c))
481 {
482 return Some(DataFormat::Fasta);
483 }
484 }
485 }
486
487 if is_likely_csv(trimmed) {
489 return Some(DataFormat::Csv);
490 }
491
492 None
493}
494
495fn is_likely_csv(text: &str) -> bool {
497 let lines: Vec<&str> = text.lines().take(10).collect();
498 if lines.len() < 2 {
499 return false;
500 }
501
502 for delim in [',', '\t', ';', '|'] {
504 let counts: Vec<usize> = lines
505 .iter()
506 .map(|line| line.matches(delim).count())
507 .collect();
508
509 if counts.is_empty() {
510 continue;
511 }
512
513 let first = counts[0];
514 if first == 0 {
515 continue;
516 }
517
518 let matching = counts.iter().filter(|&&c| c == first).count();
520 if matching >= counts.len() * 3 / 4 {
521 return true;
522 }
523 }
524
525 false
526}
527
528fn detect_extension(ext: &str) -> Option<DataFormat> {
530 let lower = ext.to_lowercase();
531 let ext_clean = lower.trim_start_matches('.');
533
534 match ext_clean {
535 "h5" | "hdf5" | "he5" | "hdf" => Some(DataFormat::Hdf5),
536 "nc" | "cdf" | "nc3" | "nc4" => Some(DataFormat::NetCdf),
537 "arrow" | "feather" | "ipc" => Some(DataFormat::ArrowIpc),
538 "mat" => Some(DataFormat::Matlab),
539 "wav" | "wave" => Some(DataFormat::Wav),
540 "npy" => Some(DataFormat::Npy),
541 "npz" => Some(DataFormat::Npz),
542 "png" => Some(DataFormat::Png),
543 "jpg" | "jpeg" => Some(DataFormat::Jpeg),
544 "bmp" => Some(DataFormat::Bmp),
545 "tif" | "tiff" => Some(DataFormat::Tiff),
546 "fits" | "fit" | "fts" => Some(DataFormat::Fits),
547 "csv" | "tsv" => Some(DataFormat::Csv),
548 "json" | "geojson" => Some(DataFormat::Json),
549 "parquet" => Some(DataFormat::Parquet),
550 "fasta" | "fa" | "fna" | "faa" => Some(DataFormat::Fasta),
551 "mtx" => Some(DataFormat::MatrixMarket),
552 "arff" => Some(DataFormat::Arff),
553 "msgpack" | "mpk" => Some(DataFormat::MessagePack),
554 "zip" => Some(DataFormat::Zip),
555 "scirs2" => Some(DataFormat::Scirs2),
556 _ => None,
557 }
558}
559
560#[cfg(test)]
565mod tests {
566 use super::*;
567
568 #[test]
569 fn test_hdf5_magic() {
570 let magic = [0x89, 0x48, 0x44, 0x46, 0x0d, 0x0a, 0x1a, 0x0a];
571 assert_eq!(detect_format_from_bytes(&magic, None), DataFormat::Hdf5);
572 }
573
574 #[test]
575 fn test_netcdf_magic() {
576 let magic = [b'C', b'D', b'F', 0x01];
577 assert_eq!(detect_format_from_bytes(&magic, None), DataFormat::NetCdf);
578
579 let magic2 = [b'C', b'D', b'F', 0x02];
580 assert_eq!(detect_format_from_bytes(&magic2, None), DataFormat::NetCdf);
581 }
582
583 #[test]
584 fn test_arrow_magic() {
585 let magic = b"ARROW1\x00\x00";
586 assert_eq!(detect_format_from_bytes(magic, None), DataFormat::ArrowIpc);
587 }
588
589 #[test]
590 fn test_matlab_magic() {
591 let magic = b"MATLAB 5.0 MAT-file, Platform: WIN64";
592 assert_eq!(detect_format_from_bytes(magic, None), DataFormat::Matlab);
593 }
594
595 #[test]
596 fn test_wav_magic() {
597 let mut magic = Vec::new();
598 magic.extend_from_slice(b"RIFF");
599 magic.extend_from_slice(&1000u32.to_le_bytes());
600 magic.extend_from_slice(b"WAVE");
601 assert_eq!(detect_format_from_bytes(&magic, None), DataFormat::Wav);
602 }
603
604 #[test]
605 fn test_npy_magic() {
606 let magic = [0x93, b'N', b'U', b'M', b'P', b'Y'];
607 assert_eq!(detect_format_from_bytes(&magic, None), DataFormat::Npy);
608 }
609
610 #[test]
611 fn test_png_magic() {
612 let magic = [0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a];
613 assert_eq!(detect_format_from_bytes(&magic, None), DataFormat::Png);
614 }
615
616 #[test]
617 fn test_jpeg_magic() {
618 let magic = [0xFF, 0xD8, 0xFF, 0xE0];
619 assert_eq!(detect_format_from_bytes(&magic, None), DataFormat::Jpeg);
620 }
621
622 #[test]
623 fn test_bmp_magic() {
624 let magic = [b'B', b'M', 0x00, 0x00];
625 assert_eq!(detect_format_from_bytes(&magic, None), DataFormat::Bmp);
626 }
627
628 #[test]
629 fn test_tiff_magic_le() {
630 let magic = [b'I', b'I', 0x2A, 0x00];
631 assert_eq!(detect_format_from_bytes(&magic, None), DataFormat::Tiff);
632 }
633
634 #[test]
635 fn test_tiff_magic_be() {
636 let magic = [b'M', b'M', 0x00, 0x2A];
637 assert_eq!(detect_format_from_bytes(&magic, None), DataFormat::Tiff);
638 }
639
640 #[test]
641 fn test_parquet_magic() {
642 let magic = b"PAR1";
643 assert_eq!(detect_format_from_bytes(magic, None), DataFormat::Parquet);
644 }
645
646 #[test]
647 fn test_zip_magic() {
648 let magic = [b'P', b'K', 0x03, 0x04];
649 assert_eq!(detect_format_from_bytes(&magic, None), DataFormat::Zip);
650 }
651
652 #[test]
653 fn test_fits_magic() {
654 let magic = b"SIMPLE = T";
655 assert_eq!(detect_format_from_bytes(magic, None), DataFormat::Fits);
656 }
657
658 #[test]
659 fn test_json_content() {
660 let data = b"{\"key\": \"value\"}";
661 assert_eq!(detect_format_from_bytes(data, None), DataFormat::Json);
662
663 let data2 = b"[1, 2, 3]";
664 assert_eq!(detect_format_from_bytes(data2, None), DataFormat::Json);
665 }
666
667 #[test]
668 fn test_matrix_market_content() {
669 let data = b"%%MatrixMarket matrix coordinate real general\n3 3 4\n1 1 1.0\n";
670 assert_eq!(
671 detect_format_from_bytes(data, None),
672 DataFormat::MatrixMarket
673 );
674 }
675
676 #[test]
677 fn test_arff_content() {
678 let data = b"@RELATION test\n@ATTRIBUTE x NUMERIC\n@DATA\n1.0\n";
679 assert_eq!(detect_format_from_bytes(data, None), DataFormat::Arff);
680 }
681
682 #[test]
683 fn test_csv_content() {
684 let data = b"a,b,c\n1,2,3\n4,5,6\n7,8,9\n";
685 assert_eq!(detect_format_from_bytes(data, None), DataFormat::Csv);
686 }
687
688 #[test]
689 fn test_tsv_content() {
690 let data = b"a\tb\tc\n1\t2\t3\n4\t5\t6\n";
691 assert_eq!(detect_format_from_bytes(data, None), DataFormat::Csv);
692 }
693
694 #[test]
695 fn test_fasta_content() {
696 let data = b">seq1\nACGTACGT\n>seq2\nGGGCCCAAA\n";
697 assert_eq!(detect_format_from_bytes(data, None), DataFormat::Fasta);
698 }
699
700 #[test]
701 fn test_extension_fallback() {
702 let empty: &[u8] = &[];
703 assert_eq!(
704 detect_format_from_bytes(empty, Some("h5")),
705 DataFormat::Hdf5
706 );
707 assert_eq!(
708 detect_format_from_bytes(empty, Some("csv")),
709 DataFormat::Csv
710 );
711 assert_eq!(
712 detect_format_from_bytes(empty, Some("json")),
713 DataFormat::Json
714 );
715 assert_eq!(
716 detect_format_from_bytes(empty, Some("mat")),
717 DataFormat::Matlab
718 );
719 assert_eq!(
720 detect_format_from_bytes(empty, Some("parquet")),
721 DataFormat::Parquet
722 );
723 }
724
725 #[test]
726 fn test_unknown_format() {
727 let data = [0x00, 0x01, 0x02, 0x03];
728 assert_eq!(detect_format_from_bytes(&data, None), DataFormat::Unknown);
729 }
730
731 #[test]
732 fn test_format_name() {
733 assert_eq!(DataFormat::Hdf5.name(), "HDF5");
734 assert_eq!(DataFormat::Csv.name(), "CSV");
735 assert_eq!(DataFormat::Unknown.name(), "Unknown");
736 }
737
738 #[test]
739 fn test_format_extensions() {
740 assert!(DataFormat::Hdf5.extensions().contains(&"h5"));
741 assert!(DataFormat::Csv.extensions().contains(&"csv"));
742 assert!(DataFormat::Unknown.extensions().is_empty());
743 }
744
745 #[test]
746 fn test_is_text() {
747 assert!(DataFormat::Csv.is_text());
748 assert!(DataFormat::Json.is_text());
749 assert!(!DataFormat::Hdf5.is_text());
750 assert!(!DataFormat::Png.is_text());
751 }
752
753 #[test]
754 fn test_supports_streaming() {
755 assert!(DataFormat::Csv.supports_streaming());
756 assert!(DataFormat::ArrowIpc.supports_streaming());
757 assert!(!DataFormat::Hdf5.supports_streaming());
758 }
759
760 #[test]
761 fn test_detailed_detection_magic() {
762 let magic = [0x89, 0x48, 0x44, 0x46, 0x0d, 0x0a, 0x1a, 0x0a];
763 let det = detect_format_detailed_from_bytes(&magic, Some("h5"));
764 assert_eq!(det.format, DataFormat::Hdf5);
765 assert!(det.confidence > 0.9);
766 assert_eq!(det.method, DetectionMethod::Combined);
767 }
768
769 #[test]
770 fn test_detailed_detection_content_only() {
771 let data = b"{\"key\": 42}";
772 let det = detect_format_detailed_from_bytes(data, None);
773 assert_eq!(det.format, DataFormat::Json);
774 assert_eq!(det.method, DetectionMethod::ContentSniffing);
775 assert!(det.confidence > 0.5);
776 }
777
778 #[test]
779 fn test_detailed_detection_extension_only() {
780 let data: &[u8] = &[];
781 let det = detect_format_detailed_from_bytes(data, Some("parquet"));
782 assert_eq!(det.format, DataFormat::Parquet);
783 assert_eq!(det.method, DetectionMethod::Extension);
784 assert!(det.confidence < 0.5);
785 }
786
787 #[test]
788 fn test_format_display() {
789 assert_eq!(format!("{}", DataFormat::Hdf5), "HDF5");
790 assert_eq!(format!("{}", DataFormat::ArrowIpc), "Arrow IPC");
791 }
792
793 #[test]
794 fn test_empty_data_with_no_ext() {
795 let data: &[u8] = &[];
796 assert_eq!(detect_format_from_bytes(data, None), DataFormat::Unknown);
797 }
798
799 #[test]
800 fn test_too_short_data() {
801 let data = [0x89];
802 assert_eq!(detect_format_from_bytes(&data, None), DataFormat::Unknown);
803 }
804
805 #[test]
806 fn test_extension_with_dot() {
807 assert_eq!(detect_extension(".h5"), Some(DataFormat::Hdf5));
808 assert_eq!(detect_extension("h5"), Some(DataFormat::Hdf5));
809 }
810
811 #[test]
812 fn test_extension_case_insensitive() {
813 assert_eq!(detect_extension("H5"), Some(DataFormat::Hdf5));
814 assert_eq!(detect_extension("CSV"), Some(DataFormat::Csv));
815 assert_eq!(detect_extension("Json"), Some(DataFormat::Json));
816 }
817
818 #[test]
819 fn test_file_not_found() {
820 let result = detect_format("/definitely/nonexistent/path.h5");
821 assert!(result.is_err());
822 }
823
824 #[test]
825 fn test_scirs2_magic() {
826 let mut magic = Vec::new();
827 magic.extend_from_slice(b"SCIRS2");
828 magic.push(0x00);
829 magic.push(0x01);
830 assert_eq!(detect_format_from_bytes(&magic, None), DataFormat::Scirs2);
831 }
832}