1#![allow(dead_code)]
2#[derive(Debug, Clone, PartialEq)]
9pub struct ContainerProbeResult {
10 pub video_present: bool,
12 pub audio_present: bool,
14 pub subtitle_present: bool,
16 pub confidence: f32,
18 pub format_label: String,
20}
21
22impl ContainerProbeResult {
23 #[must_use]
25 pub fn new(format_label: impl Into<String>) -> Self {
26 Self {
27 video_present: false,
28 audio_present: false,
29 subtitle_present: false,
30 confidence: 1.0,
31 format_label: format_label.into(),
32 }
33 }
34
35 #[must_use]
37 pub fn has_video(&self) -> bool {
38 self.video_present
39 }
40
41 #[must_use]
43 pub fn has_audio(&self) -> bool {
44 self.audio_present
45 }
46
47 #[must_use]
49 pub fn is_av(&self) -> bool {
50 self.video_present && self.audio_present
51 }
52
53 #[must_use]
55 pub fn is_confident(&self, threshold: f32) -> bool {
56 self.confidence >= threshold
57 }
58}
59
60#[derive(Debug, Clone)]
63pub struct ContainerInfo {
64 format_name: String,
66 total_tracks: usize,
68 video_count: usize,
70 audio_count: usize,
72 duration_ms: Option<u64>,
74 file_size: Option<u64>,
76}
77
78impl ContainerInfo {
79 #[must_use]
81 pub fn new(format_name: impl Into<String>) -> Self {
82 Self {
83 format_name: format_name.into(),
84 total_tracks: 0,
85 video_count: 0,
86 audio_count: 0,
87 duration_ms: None,
88 file_size: None,
89 }
90 }
91
92 #[must_use]
94 pub fn with_tracks(mut self, video: usize, audio: usize) -> Self {
95 self.video_count = video;
96 self.audio_count = audio;
97 self.total_tracks = video + audio;
98 self
99 }
100
101 #[must_use]
103 pub fn with_duration_ms(mut self, ms: u64) -> Self {
104 self.duration_ms = Some(ms);
105 self
106 }
107
108 #[must_use]
110 pub fn with_file_size(mut self, bytes: u64) -> Self {
111 self.file_size = Some(bytes);
112 self
113 }
114
115 #[must_use]
117 pub fn format_name(&self) -> &str {
118 &self.format_name
119 }
120
121 #[must_use]
123 pub fn track_count(&self) -> usize {
124 self.total_tracks
125 }
126
127 #[must_use]
129 pub fn video_count(&self) -> usize {
130 self.video_count
131 }
132
133 #[must_use]
135 pub fn audio_count(&self) -> usize {
136 self.audio_count
137 }
138
139 #[must_use]
141 pub fn duration_ms(&self) -> Option<u64> {
142 self.duration_ms
143 }
144
145 #[allow(clippy::cast_precision_loss)]
147 #[must_use]
148 pub fn estimated_bitrate_kbps(&self) -> Option<f64> {
149 match (self.file_size, self.duration_ms) {
150 (Some(bytes), Some(ms)) if ms > 0 => Some((bytes as f64 * 8.0) / (ms as f64)),
151 _ => None,
152 }
153 }
154}
155
156#[derive(Debug, Default)]
158pub struct ContainerProber {
159 probed_count: usize,
160}
161
162impl ContainerProber {
163 #[must_use]
165 pub fn new() -> Self {
166 Self::default()
167 }
168
169 #[must_use]
171 pub fn probed_count(&self) -> usize {
172 self.probed_count
173 }
174
175 pub fn probe_header(&mut self, header: &[u8]) -> ContainerProbeResult {
185 self.probed_count += 1;
186
187 if header.len() >= 4 && header[..4] == [0x1A, 0x45, 0xDF, 0xA3] {
188 let mut r = ContainerProbeResult::new("matroska");
189 r.video_present = true;
190 r.audio_present = true;
191 return r;
192 }
193 if header.len() >= 4 && &header[..4] == b"fLaC" {
194 let mut r = ContainerProbeResult::new("flac");
195 r.audio_present = true;
196 return r;
197 }
198 if header.len() >= 4 && &header[..4] == b"OggS" {
199 let mut r = ContainerProbeResult::new("ogg");
200 r.audio_present = true;
201 return r;
202 }
203 if header.len() >= 4 && &header[..4] == b"RIFF" {
204 let mut r = ContainerProbeResult::new("wav");
205 r.audio_present = true;
206 return r;
207 }
208 if header.len() >= 8 && &header[4..8] == b"ftyp" {
210 let mut r = ContainerProbeResult::new("mp4");
211 r.video_present = true;
212 r.audio_present = true;
213 return r;
214 }
215
216 let mut r = ContainerProbeResult::new("unknown");
217 r.confidence = 0.0;
218 r
219 }
220}
221
222#[derive(Debug, Clone, Default)]
226pub struct DetailedStreamInfo {
227 pub index: u32,
229 pub stream_type: String,
231 pub codec: String,
233 pub language: Option<String>,
235 pub duration_ms: Option<u64>,
237 pub bitrate_kbps: Option<u32>,
239 pub width: Option<u32>,
242 pub height: Option<u32>,
244 pub fps: Option<f32>,
246 pub pixel_format: Option<String>,
248 pub sample_rate: Option<u32>,
251 pub channels: Option<u8>,
253 pub sample_format: Option<String>,
255}
256
257#[derive(Debug, Clone, Default)]
259pub struct DetailedContainerInfo {
260 pub format: String,
263 pub duration_ms: Option<u64>,
265 pub bitrate_kbps: Option<u32>,
267 pub streams: Vec<DetailedStreamInfo>,
269 pub metadata: std::collections::HashMap<String, String>,
271 pub file_size_bytes: u64,
273}
274
275#[derive(Debug, Default)]
293pub struct MultiFormatProber;
294
295impl MultiFormatProber {
296 #[must_use]
298 pub fn new() -> Self {
299 Self
300 }
301
302 #[must_use]
304 pub fn probe(data: &[u8]) -> DetailedContainerInfo {
305 let mut info = DetailedContainerInfo {
306 file_size_bytes: data.len() as u64,
307 ..Default::default()
308 };
309
310 if data.len() < 8 {
311 info.format = "unknown".into();
312 return info;
313 }
314
315 if data[0] == 0x47 && (data.len() < 376 || data[188] == 0x47) {
317 Self::probe_mpegts(data, &mut info);
319 } else if data[..4] == [0x1A, 0x45, 0xDF, 0xA3] {
320 Self::probe_mkv(data, &mut info);
322 } else if data.len() >= 8 && &data[4..8] == b"ftyp" {
323 Self::probe_mp4(data, &mut info);
325 } else if &data[..4] == b"OggS" {
326 Self::probe_ogg(data, &mut info);
328 } else if &data[..4] == b"RIFF" {
329 Self::probe_wav(data, &mut info);
331 } else if &data[..4] == b"fLaC" {
332 Self::probe_flac(data, &mut info);
334 } else if data.len() >= 4 && &data[..4] == b"caff" {
335 Self::probe_caf(data, &mut info);
337 } else if data.len() >= 8 && data[0..2] == [0x49, 0x49] && data[2..4] == [0x2A, 0x00] {
338 Self::probe_dng_tiff(data, &mut info);
340 } else if data.len() >= 8 && data[0..2] == [0x4D, 0x4D] && data[2..4] == [0x00, 0x2A] {
341 Self::probe_dng_tiff(data, &mut info);
343 } else if data.len() >= 16
344 && data[0..4] == [0x06, 0x0E, 0x2B, 0x34]
345 && data[4..8] == [0x02, 0x05, 0x01, 0x01]
346 {
347 Self::probe_mxf(data, &mut info);
349 } else {
350 info.format = "unknown".into();
351 }
352
353 if let (Some(dur_ms), sz) = (info.duration_ms, info.file_size_bytes) {
355 if let Some(bitrate) = sz.saturating_mul(8).checked_div(dur_ms) {
357 info.bitrate_kbps = Some(bitrate as u32);
358 }
359 }
360
361 info
362 }
363
364 #[must_use]
366 pub fn probe_streams_only(data: &[u8]) -> Vec<DetailedStreamInfo> {
367 Self::probe(data).streams
368 }
369
370 fn probe_mpegts(data: &[u8], info: &mut DetailedContainerInfo) {
373 use crate::container_probe::mpegts_probe::*;
374 info.format = "mpeg-ts".into();
375
376 let (streams, duration_ms) = scan_mpegts(data);
377 info.streams = streams;
378 info.duration_ms = duration_ms;
379 }
380
381 fn probe_mp4(data: &[u8], info: &mut DetailedContainerInfo) {
384 info.format = "mp4".into();
385
386 let mut offset = 0usize;
388 while offset + 8 <= data.len() {
389 let box_size = u32::from_be_bytes([
390 data[offset],
391 data[offset + 1],
392 data[offset + 2],
393 data[offset + 3],
394 ]) as usize;
395 let fourcc = &data[offset + 4..offset + 8];
396
397 if box_size < 8 || offset + box_size > data.len() {
398 break;
399 }
400
401 if fourcc == b"moov" {
402 parse_moov(&data[offset + 8..offset + box_size], info);
403 break;
404 }
405
406 offset += box_size;
407 }
408 }
409
410 fn probe_mkv(data: &[u8], info: &mut DetailedContainerInfo) {
413 info.format = "mkv".into();
415 parse_ebml_for_info(data, info);
416 }
417
418 fn probe_ogg(data: &[u8], info: &mut DetailedContainerInfo) {
421 info.format = "ogg".into();
422 parse_ogg_bos(data, info);
423 }
424
425 fn probe_wav(data: &[u8], info: &mut DetailedContainerInfo) {
428 info.format = "wav".into();
429 if data.len() >= 12 && &data[8..12] == b"WAVE" {
430 parse_wav_chunks(data, info);
431 }
432 }
433
434 fn probe_flac(data: &[u8], info: &mut DetailedContainerInfo) {
437 info.format = "flac".into();
438 parse_flac_streaminfo(data, info);
439 }
440
441 fn probe_caf(data: &[u8], info: &mut DetailedContainerInfo) {
444 info.format = "caf".into();
445 if data.len() < 8 {
446 return;
447 }
448 let version = u16::from_be_bytes([data[4], data[5]]);
449 info.metadata
450 .insert("caf_version".into(), format!("{version}"));
451
452 let mut offset = 8usize;
453 while offset + 12 <= data.len() {
454 let chunk_type = &data[offset..offset + 4];
455 let chunk_size = read_u64_be(data, offset + 4);
456
457 if chunk_type == b"desc" && chunk_size >= 32 && offset + 44 <= data.len() {
458 let desc = &data[offset + 12..];
459 let sr = f64::from_be_bytes([
460 desc[0], desc[1], desc[2], desc[3], desc[4], desc[5], desc[6], desc[7],
461 ]);
462 let codec = String::from_utf8_lossy(&desc[8..12]).trim().to_string();
463 let ch = if desc.len() >= 28 {
464 read_u32_be(desc, 24)
465 } else {
466 0
467 };
468 let mut s = DetailedStreamInfo {
469 index: 0,
470 stream_type: "audio".into(),
471 codec,
472 ..Default::default()
473 };
474 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
475 {
476 s.sample_rate = Some(sr as u32);
477 if ch > 0 && ch < 256 {
478 s.channels = Some(ch as u8);
479 }
480 }
481 info.streams.push(s);
482 }
483
484 let advance = 12 + chunk_size as usize;
485 if advance == 0 {
486 break;
487 }
488 match offset.checked_add(advance) {
489 Some(new_offset) => offset = new_offset,
490 None => break,
491 }
492 }
493 }
494
495 fn probe_dng_tiff(data: &[u8], info: &mut DetailedContainerInfo) {
498 let is_le = data[0] == 0x49;
499 let ru16 = |off: usize| -> u16 {
500 if off + 2 > data.len() {
501 return 0;
502 }
503 if is_le {
504 u16::from_le_bytes([data[off], data[off + 1]])
505 } else {
506 u16::from_be_bytes([data[off], data[off + 1]])
507 }
508 };
509 let ru32 = |off: usize| -> u32 {
510 if off + 4 > data.len() {
511 return 0;
512 }
513 if is_le {
514 u32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
515 } else {
516 u32::from_be_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
517 }
518 };
519 let ifd_offset = ru32(4) as usize;
520 if ifd_offset + 2 > data.len() {
521 info.format = "tiff".into();
522 return;
523 }
524
525 let entry_count = ru16(ifd_offset) as usize;
526 let (mut found_dng, mut width, mut height) = (false, 0u32, 0u32);
527 for i in 0..entry_count {
528 let off = ifd_offset + 2 + i * 12;
529 if off + 12 > data.len() {
530 break;
531 }
532 match ru16(off) {
533 0xC612 => found_dng = true,
534 0x0100 => width = ru32(off + 8),
535 0x0101 => height = ru32(off + 8),
536 _ => {}
537 }
538 }
539 if found_dng {
540 info.format = "dng".into();
541 let mut s = DetailedStreamInfo {
542 index: 0,
543 stream_type: "video".into(),
544 codec: "raw".into(),
545 ..Default::default()
546 };
547 if width > 0 {
548 s.width = Some(width);
549 }
550 if height > 0 {
551 s.height = Some(height);
552 }
553 info.streams.push(s);
554 } else {
555 info.format = "tiff".into();
556 }
557 }
558
559 fn probe_mxf(data: &[u8], info: &mut DetailedContainerInfo) {
562 info.format = "mxf".into();
563 if data.len() >= 16 {
564 let pt = data[13];
565 let label = match pt {
566 0x02 => "header_partition",
567 0x03 => "body_partition",
568 0x04 => "footer_partition",
569 _ => "unknown_partition",
570 };
571 info.metadata
572 .insert("mxf_partition_type".into(), label.into());
573 }
574 if data.len() >= 12 && data[8] == 0x0D && data[9] == 0x01 {
575 info.metadata
576 .insert("mxf_registry".into(), "smpte_rdd".into());
577 }
578 if data.len() >= 64 {
579 info.streams.push(DetailedStreamInfo {
580 index: 0,
581 stream_type: "video".into(),
582 codec: "mxf_essence".into(),
583 ..Default::default()
584 });
585 }
586 }
587}
588
589#[derive(Debug, Clone, PartialEq)]
593pub struct IntegrityCheckResult {
594 pub valid: bool,
596 pub issues: Vec<String>,
598 pub score: f64,
600}
601
602impl IntegrityCheckResult {
603 #[must_use]
605 pub fn ok() -> Self {
606 Self {
607 valid: true,
608 issues: Vec::new(),
609 score: 1.0,
610 }
611 }
612
613 pub fn add_issue(&mut self, issue: impl Into<String>, severity: f64) {
615 self.issues.push(issue.into());
616 self.score = (self.score - severity).max(0.0);
617 if self.score < 0.5 {
618 self.valid = false;
619 }
620 }
621}
622
623#[must_use]
625pub fn check_container_integrity(data: &[u8]) -> IntegrityCheckResult {
626 let mut r = IntegrityCheckResult::ok();
627 if data.is_empty() {
628 r.add_issue("Container data is empty", 1.0);
629 return r;
630 }
631 if data.len() < 8 {
632 r.add_issue("Too short for any known format", 0.8);
633 return r;
634 }
635
636 if &data[4..8] == b"ftyp" {
637 validate_mp4_boxes(data, &mut r);
638 } else if &data[..4] == b"fLaC" {
639 validate_flac_structure(data, &mut r);
640 } else if &data[..4] == b"RIFF" {
641 validate_riff_structure(data, &mut r);
642 }
643 r
644}
645
646fn validate_mp4_boxes(data: &[u8], result: &mut IntegrityCheckResult) {
647 let (mut offset, mut box_count, mut found_moov) = (0usize, 0u32, false);
648 while offset + 8 <= data.len() {
649 let size = read_u32_be(data, offset) as usize;
650 if size < 8 {
651 result.add_issue(format!("Bad MP4 box size at {offset}"), 0.3);
652 break;
653 }
654 if offset + size > data.len() {
655 result.add_issue(format!("MP4 box exceeds data at {offset}"), 0.2);
656 break;
657 }
658 if &data[offset + 4..offset + 8] == b"moov" {
659 found_moov = true;
660 }
661 box_count += 1;
662 offset += size;
663 }
664 if box_count == 0 {
665 result.add_issue("No valid MP4 boxes", 0.5);
666 }
667 if !found_moov && data.len() > 1024 {
668 result.add_issue("MP4 missing moov", 0.3);
669 }
670}
671
672fn validate_flac_structure(data: &[u8], result: &mut IntegrityCheckResult) {
673 if data.len() < 42 {
674 result.add_issue("FLAC too short for STREAMINFO", 0.4);
675 return;
676 }
677 if data[4] & 0x7F != 0 {
678 result.add_issue("First FLAC block not STREAMINFO", 0.3);
679 }
680}
681
682fn validate_riff_structure(data: &[u8], result: &mut IntegrityCheckResult) {
683 if data.len() < 12 {
684 result.add_issue("RIFF too short", 0.5);
685 return;
686 }
687 if &data[8..12] != b"WAVE" && &data[8..12] != b"AVI " {
688 result.add_issue("RIFF form type not WAVE/AVI", 0.2);
689 }
690 let riff_size = u32::from_le_bytes([data[4], data[5], data[6], data[7]]) as u64;
691 if riff_size + 8 > data.len() as u64 {
692 result.add_issue(
693 format!("RIFF size mismatch ({} vs {})", riff_size + 8, data.len()),
694 0.15,
695 );
696 }
697}
698
699mod mpegts_probe {
703 use super::DetailedStreamInfo;
704 use crate::demux::mpegts_enhanced::TsDemuxer;
705
706 fn stream_type_to_codec(st: u8) -> Option<&'static str> {
708 match st {
709 0x85 => Some("av1"),
710 0x84 => Some("vp9"),
711 0x83 => Some("vp8"),
712 0x81 => Some("opus"),
713 0x82 => Some("flac"),
714 0x80 => Some("pcm"),
715 0x06 => Some("private"),
716 _ => None,
717 }
718 }
719
720 fn stream_type_to_kind(st: u8) -> &'static str {
721 match st {
722 0x85 | 0x84 | 0x83 | 0x1B | 0x24 => "video",
723 0x81 | 0x82 | 0x80 | 0x03 | 0x04 | 0x0F | 0x11 => "audio",
724 _ => "data",
725 }
726 }
727
728 pub fn scan_mpegts(data: &[u8]) -> (Vec<DetailedStreamInfo>, Option<u64>) {
730 let mut demux = TsDemuxer::new();
731 let scan_end = data.len().min(2 * 1024 * 1024);
733 demux.feed(&data[..scan_end]);
734
735 let si = demux.stream_info();
736 let duration_ms = demux.duration_ms();
737
738 let mut streams: Vec<DetailedStreamInfo> = Vec::new();
739 let mut idx = 0u32;
740
741 for pmt in si.pmts.values() {
743 for ps in &pmt.streams {
744 let codec = stream_type_to_codec(ps.stream_type)
745 .unwrap_or("unknown")
746 .to_string();
747 let kind = stream_type_to_kind(ps.stream_type).to_string();
748
749 let pid_info = si.pids.get(&ps.elementary_pid);
750 let mut s = DetailedStreamInfo {
751 index: idx,
752 stream_type: kind.clone(),
753 codec,
754 ..Default::default()
755 };
756
757 if let Some(pi) = pid_info {
758 if let (Some(f), Some(l)) = (pi.pts_first, pi.pts_last) {
759 if l > f {
760 s.duration_ms = Some((l - f) / 90);
761 }
762 }
763 if s.duration_ms.is_some() && pi.total_bytes > 0 {
764 let dur_s = s.duration_ms.unwrap_or(1) as u64;
765 if let Some(bitrate) = (pi.total_bytes * 8).checked_div(dur_s) {
766 s.bitrate_kbps = Some(bitrate as u32);
767 }
768 }
769 }
770
771 streams.push(s);
772 idx += 1;
773 }
774 }
775
776 (streams, duration_ms)
777 }
778}
779
780use crate::container_probe_parsers::{
782 parse_ebml_for_info, parse_flac_streaminfo, parse_moov, parse_ogg_bos, parse_wav_chunks,
783 read_u32_be, read_u64_be,
784};
785
786#[cfg(test)]
788mod tests {
789 use super::*;
790
791 #[test]
793 fn test_has_video_true() {
794 let mut r = ContainerProbeResult::new("mkv");
795 r.video_present = true;
796 assert!(r.has_video());
797 }
798
799 #[test]
801 fn test_has_video_false() {
802 let r = ContainerProbeResult::new("flac");
803 assert!(!r.has_video());
804 }
805
806 #[test]
808 fn test_has_audio_true() {
809 let mut r = ContainerProbeResult::new("ogg");
810 r.audio_present = true;
811 assert!(r.has_audio());
812 }
813
814 #[test]
816 fn test_is_av_both() {
817 let mut r = ContainerProbeResult::new("mp4");
818 r.video_present = true;
819 r.audio_present = true;
820 assert!(r.is_av());
821 }
822
823 #[test]
825 fn test_is_av_audio_only() {
826 let mut r = ContainerProbeResult::new("wav");
827 r.audio_present = true;
828 assert!(!r.is_av());
829 }
830
831 #[test]
833 fn test_is_confident() {
834 let r = ContainerProbeResult::new("matroska");
835 assert!(r.is_confident(0.9));
836 assert!(!r.is_confident(1.1));
837 }
838
839 #[test]
841 fn test_container_info_format_name() {
842 let info = ContainerInfo::new("matroska");
843 assert_eq!(info.format_name(), "matroska");
844 }
845
846 #[test]
848 fn test_container_info_track_count() {
849 let info = ContainerInfo::new("mp4").with_tracks(1, 2);
850 assert_eq!(info.track_count(), 3);
851 }
852
853 #[test]
855 fn test_container_info_video_count() {
856 let info = ContainerInfo::new("mkv").with_tracks(2, 4);
857 assert_eq!(info.video_count(), 2);
858 assert_eq!(info.audio_count(), 4);
859 }
860
861 #[test]
863 fn test_estimated_bitrate_kbps() {
864 let info = ContainerInfo::new("mp4")
865 .with_file_size(1_000_000)
866 .with_duration_ms(1000);
867 let kbps = info
869 .estimated_bitrate_kbps()
870 .expect("operation should succeed");
871 assert!((kbps - 8000.0).abs() < 1.0);
872 }
873
874 #[test]
876 fn test_estimated_bitrate_kbps_no_duration() {
877 let info = ContainerInfo::new("mkv").with_file_size(1_000_000);
878 assert!(info.estimated_bitrate_kbps().is_none());
879 }
880
881 #[test]
883 fn test_probe_matroska() {
884 let mut p = ContainerProber::new();
885 let magic = [0x1A, 0x45, 0xDF, 0xA3, 0x00, 0x00, 0x00, 0x00];
886 let r = p.probe_header(&magic);
887 assert_eq!(r.format_label, "matroska");
888 assert!(r.has_video());
889 assert!(r.has_audio());
890 }
891
892 #[test]
894 fn test_probe_flac() {
895 let mut p = ContainerProber::new();
896 let r = p.probe_header(b"fLaC\x00\x00\x00\x22");
897 assert_eq!(r.format_label, "flac");
898 assert!(!r.has_video());
899 assert!(r.has_audio());
900 }
901
902 #[test]
904 fn test_probe_mp4() {
905 let mut p = ContainerProber::new();
906 let header = b"\x00\x00\x00\x18ftyp\x69\x73\x6f\x6d";
908 let r = p.probe_header(header);
909 assert_eq!(r.format_label, "mp4");
910 assert!(r.has_video());
911 assert_eq!(p.probed_count(), 1);
912 }
913
914 #[test]
916 fn test_probe_unknown() {
917 let mut p = ContainerProber::new();
918 let r = p.probe_header(b"\xFF\xFF\xFF\xFF");
919 assert_eq!(r.format_label, "unknown");
920 assert_eq!(r.confidence, 0.0);
921 }
922
923 #[test]
927 fn test_multiformat_probe_empty() {
928 let info = MultiFormatProber::probe(&[]);
929 assert_eq!(info.format, "unknown");
930 assert!(info.streams.is_empty());
931 }
932
933 #[test]
935 fn test_multiformat_probe_random() {
936 let info = MultiFormatProber::probe(&[0xFF, 0xFE, 0xFD, 0xFC, 0x00, 0x00, 0x00, 0x00]);
937 assert_eq!(info.format, "unknown");
938 }
939
940 #[test]
942 fn test_multiformat_probe_flac_magic() {
943 let mut data = Vec::new();
945 data.extend_from_slice(b"fLaC");
946 data.push(0x00);
948 data.push(0x00);
949 data.push(0x00);
950 data.push(0x22); data.extend_from_slice(&[0u8; 10]);
953 data.push(0xAC); data.push(0x44); data.push(0x42);
967 data.push(0xF0);
968 data.extend_from_slice(&[0u8; 20]);
970
971 let info = MultiFormatProber::probe(&data);
972 assert_eq!(info.format, "flac");
973 assert!(!info.streams.is_empty());
974 assert_eq!(info.streams[0].codec, "flac");
975 assert_eq!(info.streams[0].stream_type, "audio");
976 }
977
978 #[test]
980 fn test_multiformat_probe_wav() {
981 let mut data = Vec::new();
982 data.extend_from_slice(b"RIFF");
983 let total_size: u32 = 36;
984 data.extend_from_slice(&total_size.to_le_bytes()); data.extend_from_slice(b"WAVE");
986 data.extend_from_slice(b"fmt ");
987 data.extend_from_slice(&16u32.to_le_bytes()); data.extend_from_slice(&1u16.to_le_bytes()); data.extend_from_slice(&2u16.to_le_bytes()); data.extend_from_slice(&44100u32.to_le_bytes()); data.extend_from_slice(&(44100 * 2 * 2u32).to_le_bytes()); data.extend_from_slice(&4u16.to_le_bytes()); data.extend_from_slice(&16u16.to_le_bytes()); let info = MultiFormatProber::probe(&data);
996 assert_eq!(info.format, "wav");
997 assert!(!info.streams.is_empty());
998 let s = &info.streams[0];
999 assert_eq!(s.codec, "pcm");
1000 assert_eq!(s.sample_rate, Some(44100));
1001 assert_eq!(s.channels, Some(2));
1002 }
1003
1004 #[test]
1006 fn test_multiformat_probe_ogg() {
1007 let mut data = vec![0u8; 300];
1009 data[0..4].copy_from_slice(b"OggS");
1011 data[4] = 0; data[5] = 0x02; data[6..14].fill(0);
1015 data[14..18].fill(0); data[18..22].fill(0); data[22..26].fill(0); data[26] = 1; data[27] = 19; data[28..36].copy_from_slice(b"OpusHead");
1023 data[36] = 1; data[37] = 2; data[38..40].fill(0); data[40..44].copy_from_slice(&48000u32.to_le_bytes()); data[44..46].fill(0); data[46] = 0; let info = MultiFormatProber::probe(&data);
1031 assert_eq!(info.format, "ogg");
1032 }
1033
1034 #[test]
1036 fn test_multiformat_probe_mp4_magic() {
1037 let mut data = Vec::new();
1038 data.extend_from_slice(&20u32.to_be_bytes());
1040 data.extend_from_slice(b"ftyp");
1041 data.extend_from_slice(b"iso5");
1042 data.extend_from_slice(&0u32.to_be_bytes());
1043 data.extend_from_slice(b"iso5");
1044 let info = MultiFormatProber::probe(&data);
1046 assert_eq!(info.format, "mp4");
1047 }
1048
1049 #[test]
1051 fn test_multiformat_probe_mkv_magic() {
1052 let data = [
1054 0x1A, 0x45, 0xDF, 0xA3, 0x84, 0x42, 0x82, 0x84, 0x77, 0x65, 0x62, 0x6D, 0x00,
1055 ];
1056 let info = MultiFormatProber::probe(&data);
1057 assert!(
1058 info.format == "mkv" || info.format == "webm",
1059 "got format: {}",
1060 info.format
1061 );
1062 }
1063
1064 #[test]
1066 fn test_probe_streams_only() {
1067 let mut data = Vec::new();
1068 data.extend_from_slice(b"RIFF");
1069 data.extend_from_slice(&36u32.to_le_bytes());
1070 data.extend_from_slice(b"WAVE");
1071 data.extend_from_slice(b"fmt ");
1072 data.extend_from_slice(&16u32.to_le_bytes());
1073 data.extend_from_slice(&1u16.to_le_bytes());
1074 data.extend_from_slice(&1u16.to_le_bytes()); data.extend_from_slice(&22050u32.to_le_bytes());
1076 data.extend_from_slice(&(22050u32 * 2).to_le_bytes());
1077 data.extend_from_slice(&2u16.to_le_bytes());
1078 data.extend_from_slice(&16u16.to_le_bytes());
1079
1080 let streams = MultiFormatProber::probe_streams_only(&data);
1081 assert!(!streams.is_empty());
1082 assert_eq!(streams[0].stream_type, "audio");
1083 }
1084
1085 #[test]
1087 fn test_multiformat_file_size() {
1088 let data = b"not a real container at all, just some bytes";
1089 let info = MultiFormatProber::probe(data);
1090 assert_eq!(info.file_size_bytes, data.len() as u64);
1091 }
1092
1093 #[test]
1095 fn test_multiformat_wav_duration() {
1096 let mut data = Vec::new();
1097 let pcm_bytes: u32 = 44100 * 2; let total: u32 = 36 + pcm_bytes;
1100 data.extend_from_slice(b"RIFF");
1101 data.extend_from_slice(&total.to_le_bytes());
1102 data.extend_from_slice(b"WAVE");
1103 data.extend_from_slice(b"fmt ");
1104 data.extend_from_slice(&16u32.to_le_bytes());
1105 data.extend_from_slice(&1u16.to_le_bytes()); data.extend_from_slice(&1u16.to_le_bytes()); data.extend_from_slice(&44100u32.to_le_bytes());
1108 data.extend_from_slice(&(44100u32 * 2).to_le_bytes());
1109 data.extend_from_slice(&2u16.to_le_bytes());
1110 data.extend_from_slice(&16u16.to_le_bytes());
1111 data.extend_from_slice(b"data");
1112 data.extend_from_slice(&pcm_bytes.to_le_bytes());
1113 data.extend(vec![0u8; pcm_bytes as usize]);
1114
1115 let info = MultiFormatProber::probe(&data);
1116 assert_eq!(info.format, "wav");
1117 assert_eq!(info.duration_ms, Some(1000));
1118 }
1119
1120 #[test]
1122 fn test_detailed_stream_info_default() {
1123 let s = DetailedStreamInfo::default();
1124 assert!(s.codec.is_empty());
1125 assert!(s.stream_type.is_empty());
1126 assert!(s.duration_ms.is_none());
1127 }
1128
1129 #[test]
1131 fn test_detailed_container_info_metadata() {
1132 let info = DetailedContainerInfo::default();
1133 assert!(info.metadata.is_empty());
1134 assert!(info.streams.is_empty());
1135 assert_eq!(info.file_size_bytes, 0);
1136 }
1137
1138 #[test]
1142 fn test_multiformat_probe_caf() {
1143 let mut data = Vec::new();
1144 data.extend_from_slice(b"caff");
1145 data.extend_from_slice(&1u16.to_be_bytes()); data.extend_from_slice(&0u16.to_be_bytes()); data.extend_from_slice(b"desc");
1149 data.extend_from_slice(&32u64.to_be_bytes()); data.extend_from_slice(&44100.0_f64.to_be_bytes()); data.extend_from_slice(b"lpcm"); data.extend_from_slice(&0u32.to_be_bytes()); data.extend_from_slice(&4u32.to_be_bytes()); data.extend_from_slice(&1u32.to_be_bytes()); data.extend_from_slice(&2u32.to_be_bytes()); data.extend_from_slice(&16u32.to_be_bytes()); let info = MultiFormatProber::probe(&data);
1160 assert_eq!(info.format, "caf");
1161 assert!(!info.streams.is_empty());
1162 assert_eq!(info.streams[0].stream_type, "audio");
1163 assert_eq!(info.streams[0].sample_rate, Some(44100));
1164 assert_eq!(info.streams[0].channels, Some(2));
1165 }
1166
1167 #[test]
1169 fn test_caf_short_data() {
1170 let mut data = Vec::new();
1171 data.extend_from_slice(b"caff");
1172 data.extend_from_slice(&1u16.to_be_bytes());
1173 data.extend_from_slice(&0u16.to_be_bytes());
1174 let info = MultiFormatProber::probe(&data);
1177 assert_eq!(info.format, "caf");
1178 assert!(info.streams.is_empty());
1179 }
1180
1181 #[test]
1185 fn test_probe_tiff_le() {
1186 let mut data = vec![0u8; 128];
1187 data[0] = 0x49; data[1] = 0x49; data[2] = 0x2A; data[3] = 0x00;
1191 data[4..8].copy_from_slice(&8u32.to_le_bytes());
1193 data[8..10].copy_from_slice(&0u16.to_le_bytes());
1195
1196 let info = MultiFormatProber::probe(&data);
1197 assert_eq!(info.format, "tiff");
1198 }
1199
1200 #[test]
1202 fn test_probe_dng() {
1203 let mut data = vec![0u8; 128];
1204 data[0] = 0x49; data[1] = 0x49; data[2] = 0x2A;
1207 data[3] = 0x00;
1208 data[4..8].copy_from_slice(&8u32.to_le_bytes());
1210 data[8..10].copy_from_slice(&2u16.to_le_bytes());
1212 data[10..12].copy_from_slice(&0x0100u16.to_le_bytes());
1214 data[12..14].copy_from_slice(&3u16.to_le_bytes()); data[14..18].copy_from_slice(&1u32.to_le_bytes()); data[18..22].copy_from_slice(&4000u32.to_le_bytes()); data[22..24].copy_from_slice(&0xC612u16.to_le_bytes());
1219 data[24..26].copy_from_slice(&1u16.to_le_bytes()); data[26..30].copy_from_slice(&4u32.to_le_bytes()); data[30..34].copy_from_slice(&1u32.to_le_bytes()); let info = MultiFormatProber::probe(&data);
1224 assert_eq!(info.format, "dng");
1225 assert!(!info.streams.is_empty());
1226 assert_eq!(info.streams[0].stream_type, "video");
1227 assert_eq!(info.streams[0].codec, "raw");
1228 assert_eq!(info.streams[0].width, Some(4000));
1229 }
1230
1231 #[test]
1233 fn test_probe_tiff_be() {
1234 let mut data = vec![0u8; 64];
1235 data[0] = 0x4D; data[1] = 0x4D; data[2] = 0x00;
1238 data[3] = 0x2A;
1239 data[4..8].copy_from_slice(&8u32.to_be_bytes());
1240 data[8..10].copy_from_slice(&0u16.to_be_bytes());
1241
1242 let info = MultiFormatProber::probe(&data);
1243 assert_eq!(info.format, "tiff");
1244 }
1245
1246 #[test]
1250 fn test_probe_mxf() {
1251 let mut data = vec![0u8; 128];
1252 data[0..4].copy_from_slice(&[0x06, 0x0E, 0x2B, 0x34]);
1254 data[4..8].copy_from_slice(&[0x02, 0x05, 0x01, 0x01]);
1255 data[8..12].copy_from_slice(&[0x0D, 0x01, 0x02, 0x01]);
1256 data[12..16].copy_from_slice(&[0x01, 0x02, 0x04, 0x00]); let info = MultiFormatProber::probe(&data);
1259 assert_eq!(info.format, "mxf");
1260 assert!(info.metadata.contains_key("mxf_partition_type"));
1261 assert_eq!(
1262 info.metadata.get("mxf_partition_type"),
1263 Some(&"header_partition".to_string())
1264 );
1265 }
1266
1267 #[test]
1269 fn test_probe_mxf_streams() {
1270 let mut data = vec![0u8; 128];
1271 data[0..4].copy_from_slice(&[0x06, 0x0E, 0x2B, 0x34]);
1272 data[4..8].copy_from_slice(&[0x02, 0x05, 0x01, 0x01]);
1273 data[8..12].copy_from_slice(&[0x0D, 0x01, 0x02, 0x01]);
1274 data[12..16].copy_from_slice(&[0x01, 0x03, 0x04, 0x00]); let info = MultiFormatProber::probe(&data);
1277 assert_eq!(info.format, "mxf");
1278 assert!(!info.streams.is_empty());
1279 assert_eq!(info.streams[0].codec, "mxf_essence");
1280 }
1281
1282 #[test]
1286 fn test_integrity_empty() {
1287 let result = check_container_integrity(&[]);
1288 assert!(!result.valid);
1289 assert!(!result.issues.is_empty());
1290 }
1291
1292 #[test]
1294 fn test_integrity_too_short() {
1295 let result = check_container_integrity(&[0x00, 0x01, 0x02]);
1296 assert!(!result.valid);
1297 }
1298
1299 #[test]
1301 fn test_integrity_valid_mp4() {
1302 let mut data = Vec::new();
1303 data.extend_from_slice(&20u32.to_be_bytes());
1305 data.extend_from_slice(b"ftyp");
1306 data.extend_from_slice(b"iso5");
1307 data.extend_from_slice(&0u32.to_be_bytes());
1308 data.extend_from_slice(b"iso5");
1309
1310 let result = check_container_integrity(&data);
1311 assert!(result.valid);
1312 assert!(result.score > 0.5);
1313 }
1314
1315 #[test]
1317 fn test_integrity_mp4_bad_box() {
1318 let mut data = Vec::new();
1319 data.extend_from_slice(&200u32.to_be_bytes());
1321 data.extend_from_slice(b"ftyp");
1322 data.extend_from_slice(&[0u8; 12]);
1323
1324 let result = check_container_integrity(&data);
1325 assert!(result.score < 1.0);
1326 }
1327
1328 #[test]
1330 fn test_integrity_valid_flac() {
1331 let mut data = vec![0u8; 50];
1332 data[0..4].copy_from_slice(b"fLaC");
1333 data[4] = 0x00; let result = check_container_integrity(&data);
1336 assert!(result.valid);
1337 }
1338
1339 #[test]
1341 fn test_integrity_flac_short() {
1342 let mut data = vec![0u8; 20];
1343 data[0..4].copy_from_slice(b"fLaC");
1344
1345 let result = check_container_integrity(&data);
1346 assert!(result.score < 1.0);
1347 }
1348
1349 #[test]
1351 fn test_integrity_valid_wav() {
1352 let data_size: u32 = 36;
1353 let mut data = Vec::new();
1354 data.extend_from_slice(b"RIFF");
1355 data.extend_from_slice(&data_size.to_le_bytes());
1356 data.extend_from_slice(b"WAVE");
1357 data.extend_from_slice(b"fmt ");
1358 data.extend_from_slice(&16u32.to_le_bytes());
1359 data.extend_from_slice(&[0u8; 16]); data.extend_from_slice(b"data");
1361 data.extend_from_slice(&0u32.to_le_bytes());
1362
1363 let result = check_container_integrity(&data);
1364 assert!(result.valid);
1365 }
1366
1367 #[test]
1369 fn test_integrity_riff_size_mismatch() {
1370 let mut data = Vec::new();
1371 data.extend_from_slice(b"RIFF");
1372 data.extend_from_slice(&100_000u32.to_le_bytes()); data.extend_from_slice(b"WAVE");
1374 data.extend_from_slice(&[0u8; 8]); let result = check_container_integrity(&data);
1377 assert!(result.score < 1.0);
1378 }
1379}