Skip to main content

oximedia_container/
container_probe.rs

1#![allow(dead_code)]
2//! Higher-level container probing beyond magic-byte detection.
3//!
4//! Provides `ContainerProbeResult`, `ContainerInfo`, and `ContainerProber`
5//! for interrogating container structure without a full demux pass.
6
7/// Summary flags produced by probing a container's header region.
8#[derive(Debug, Clone, PartialEq)]
9pub struct ContainerProbeResult {
10    /// Whether at least one video track was detected.
11    pub video_present: bool,
12    /// Whether at least one audio track was detected.
13    pub audio_present: bool,
14    /// Whether at least one subtitle track was detected.
15    pub subtitle_present: bool,
16    /// Confidence of the format detection in the range `[0.0, 1.0]`.
17    pub confidence: f32,
18    /// Raw format name string as reported by the container layer.
19    pub format_label: String,
20}
21
22impl ContainerProbeResult {
23    /// Creates a new probe result with default confidence of 1.0.
24    #[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    /// Returns `true` when at least one video track was detected.
36    #[must_use]
37    pub fn has_video(&self) -> bool {
38        self.video_present
39    }
40
41    /// Returns `true` when at least one audio track was detected.
42    #[must_use]
43    pub fn has_audio(&self) -> bool {
44        self.audio_present
45    }
46
47    /// Returns `true` for multimedia containers that have both video and audio.
48    #[must_use]
49    pub fn is_av(&self) -> bool {
50        self.video_present && self.audio_present
51    }
52
53    /// Returns `true` when confidence is at or above `threshold`.
54    #[must_use]
55    pub fn is_confident(&self, threshold: f32) -> bool {
56        self.confidence >= threshold
57    }
58}
59
60/// Detailed structural information about a container, produced after a
61/// more thorough header scan than a simple magic-byte probe.
62#[derive(Debug, Clone)]
63pub struct ContainerInfo {
64    /// Short format name (e.g. `"matroska"`, `"mp4"`, `"ogg"`).
65    format_name: String,
66    /// Total number of tracks (all types).
67    total_tracks: usize,
68    /// Number of video tracks.
69    video_count: usize,
70    /// Number of audio tracks.
71    audio_count: usize,
72    /// Total container duration in milliseconds, if signalled.
73    duration_ms: Option<u64>,
74    /// Container file size in bytes, if known.
75    file_size: Option<u64>,
76}
77
78impl ContainerInfo {
79    /// Creates a new `ContainerInfo`.
80    #[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    /// Sets video and audio track counts, automatically deriving `total_tracks`.
93    #[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    /// Sets the duration.
102    #[must_use]
103    pub fn with_duration_ms(mut self, ms: u64) -> Self {
104        self.duration_ms = Some(ms);
105        self
106    }
107
108    /// Sets the file size.
109    #[must_use]
110    pub fn with_file_size(mut self, bytes: u64) -> Self {
111        self.file_size = Some(bytes);
112        self
113    }
114
115    /// Returns the short format name.
116    #[must_use]
117    pub fn format_name(&self) -> &str {
118        &self.format_name
119    }
120
121    /// Returns the total track count (all types).
122    #[must_use]
123    pub fn track_count(&self) -> usize {
124        self.total_tracks
125    }
126
127    /// Returns the number of video tracks.
128    #[must_use]
129    pub fn video_count(&self) -> usize {
130        self.video_count
131    }
132
133    /// Returns the number of audio tracks.
134    #[must_use]
135    pub fn audio_count(&self) -> usize {
136        self.audio_count
137    }
138
139    /// Returns the duration in milliseconds, if known.
140    #[must_use]
141    pub fn duration_ms(&self) -> Option<u64> {
142        self.duration_ms
143    }
144
145    /// Estimates the average bit rate in kbps from file size and duration.
146    #[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/// A thin prober that inspects raw bytes and fills a `ContainerInfo`.
157#[derive(Debug, Default)]
158pub struct ContainerProber {
159    probed_count: usize,
160}
161
162impl ContainerProber {
163    /// Creates a new `ContainerProber`.
164    #[must_use]
165    pub fn new() -> Self {
166        Self::default()
167    }
168
169    /// Returns the number of containers probed so far.
170    #[must_use]
171    pub fn probed_count(&self) -> usize {
172        self.probed_count
173    }
174
175    /// Inspects the first bytes of a container and returns a
176    /// `ContainerProbeResult`.
177    ///
178    /// Detection is based on well-known magic sequences:
179    /// - `[0x1A, 0x45, 0xDF, 0xA3]` → Matroska / `WebM`
180    /// - `[0x66, 0x4C, 0x61, 0x43]` (`fLaC`) → FLAC
181    /// - `[0x4F, 0x67, 0x67, 0x53]` (`OggS`) → Ogg
182    /// - `[0x52, 0x49, 0x46, 0x46]` (`RIFF`) → WAV
183    /// - `[0x00, 0x00, 0x00, _, 0x66, 0x74, 0x79, 0x70]` → MP4/ftyp
184    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        // MP4: check bytes 4-7 for "ftyp"
209        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// ─── Enhanced multi-format container probing ──────────────────────────────────
223
224/// Detailed information about one media stream found inside a container.
225#[derive(Debug, Clone, Default)]
226pub struct DetailedStreamInfo {
227    /// Zero-based stream index.
228    pub index: u32,
229    /// Stream type: `"video"`, `"audio"`, `"subtitle"`, or `"data"`.
230    pub stream_type: String,
231    /// Short codec name (e.g. `"av1"`, `"opus"`, `"flac"`).
232    pub codec: String,
233    /// ISO 639-2 language tag, if present.
234    pub language: Option<String>,
235    /// Stream duration in milliseconds, if known.
236    pub duration_ms: Option<u64>,
237    /// Average bitrate in kbps, if estimable.
238    pub bitrate_kbps: Option<u32>,
239    // Video fields
240    /// Frame width in pixels.
241    pub width: Option<u32>,
242    /// Frame height in pixels.
243    pub height: Option<u32>,
244    /// Frames per second.
245    pub fps: Option<f32>,
246    /// Pixel format string (e.g. `"yuv420p"`).
247    pub pixel_format: Option<String>,
248    // Audio fields
249    /// Audio sample rate in Hz.
250    pub sample_rate: Option<u32>,
251    /// Number of audio channels.
252    pub channels: Option<u8>,
253    /// Sample format string (e.g. `"s16"`).
254    pub sample_format: Option<String>,
255}
256
257/// Rich container information returned by [`MultiFormatProber`].
258#[derive(Debug, Clone, Default)]
259pub struct DetailedContainerInfo {
260    /// Short format name (`"mp4"`, `"mkv"`, `"mpeg-ts"`, `"webm"`, `"ogg"`,
261    /// `"wav"`, `"flac"`, `"unknown"`).
262    pub format: String,
263    /// Total duration in milliseconds, if signalled.
264    pub duration_ms: Option<u64>,
265    /// Overall bitrate in kbps, if estimable from file_size_bytes + duration_ms.
266    pub bitrate_kbps: Option<u32>,
267    /// Discovered streams.
268    pub streams: Vec<DetailedStreamInfo>,
269    /// Key/value metadata extracted from the container header.
270    pub metadata: std::collections::HashMap<String, String>,
271    /// Byte length of the input slice.
272    pub file_size_bytes: u64,
273}
274
275/// A stateless multi-format container prober that inspects raw byte slices.
276///
277/// Compared to [`ContainerProber`] (magic-byte only), `MultiFormatProber`
278/// performs a shallow parse of the container structure to discover stream
279/// count, codec, dimensions, duration, and basic metadata — all without
280/// decoding any compressed data.
281///
282/// # Supported formats
283///
284/// | Format | Detection | Duration | Streams |
285/// |--------|-----------|----------|---------|
286/// | MPEG-TS | ✓ | from PTS | from PMT |
287/// | MP4/MOV | ✓ | mvhd | trak/hdlr |
288/// | MKV/WebM | ✓ | EBML Segment/Info | TrackEntry |
289/// | Ogg | ✓ | BOS codec | codec header |
290/// | WAV | ✓ | fmt chunk | PCM params |
291/// | FLAC | ✓ | STREAMINFO | sample params |
292#[derive(Debug, Default)]
293pub struct MultiFormatProber;
294
295impl MultiFormatProber {
296    /// Creates a new `MultiFormatProber`.
297    #[must_use]
298    pub fn new() -> Self {
299        Self
300    }
301
302    /// Probes `data` and returns all available container information.
303    #[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        // Detect by magic bytes
316        if data[0] == 0x47 && (data.len() < 376 || data[188] == 0x47) {
317            // MPEG-TS: sync byte 0x47 at offset 0 (and 188 if data is long enough)
318            Self::probe_mpegts(data, &mut info);
319        } else if data[..4] == [0x1A, 0x45, 0xDF, 0xA3] {
320            // Matroska / WebM
321            Self::probe_mkv(data, &mut info);
322        } else if data.len() >= 8 && &data[4..8] == b"ftyp" {
323            // MP4 / MOV / CMAF
324            Self::probe_mp4(data, &mut info);
325        } else if &data[..4] == b"OggS" {
326            // Ogg container
327            Self::probe_ogg(data, &mut info);
328        } else if &data[..4] == b"RIFF" {
329            // WAV / RIFF
330            Self::probe_wav(data, &mut info);
331        } else if &data[..4] == b"fLaC" {
332            // Native FLAC
333            Self::probe_flac(data, &mut info);
334        } else if data.len() >= 4 && &data[..4] == b"caff" {
335            // CAF (Core Audio Format)
336            Self::probe_caf(data, &mut info);
337        } else if data.len() >= 8 && data[0..2] == [0x49, 0x49] && data[2..4] == [0x2A, 0x00] {
338            // TIFF little-endian (DNG is a subset of TIFF)
339            Self::probe_dng_tiff(data, &mut info);
340        } else if data.len() >= 8 && data[0..2] == [0x4D, 0x4D] && data[2..4] == [0x00, 0x2A] {
341            // TIFF big-endian
342            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            // MXF (Material Exchange Format) - KLV key prefix
348            Self::probe_mxf(data, &mut info);
349        } else {
350            info.format = "unknown".into();
351        }
352
353        // Estimate overall bitrate
354        if let (Some(dur_ms), sz) = (info.duration_ms, info.file_size_bytes) {
355            // bitrate kbps = bytes * 8 / ms
356            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    /// Returns only the stream list from `data`.
365    #[must_use]
366    pub fn probe_streams_only(data: &[u8]) -> Vec<DetailedStreamInfo> {
367        Self::probe(data).streams
368    }
369
370    // ─── MPEG-TS ──────────────────────────────────────────────────────────
371
372    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    // ─── MP4 / MOV ────────────────────────────────────────────────────────
382
383    fn probe_mp4(data: &[u8], info: &mut DetailedContainerInfo) {
384        info.format = "mp4".into();
385
386        // Walk top-level boxes looking for moov
387        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    // ─── MKV / WebM ───────────────────────────────────────────────────────
411
412    fn probe_mkv(data: &[u8], info: &mut DetailedContainerInfo) {
413        // Check if this is WebM (subset of Matroska)
414        info.format = "mkv".into();
415        parse_ebml_for_info(data, info);
416    }
417
418    // ─── Ogg ──────────────────────────────────────────────────────────────
419
420    fn probe_ogg(data: &[u8], info: &mut DetailedContainerInfo) {
421        info.format = "ogg".into();
422        parse_ogg_bos(data, info);
423    }
424
425    // ─── WAV / RIFF ───────────────────────────────────────────────────────
426
427    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    // ─── FLAC ─────────────────────────────────────────────────────────────
435
436    fn probe_flac(data: &[u8], info: &mut DetailedContainerInfo) {
437        info.format = "flac".into();
438        parse_flac_streaminfo(data, info);
439    }
440
441    // ─── CAF (Core Audio Format) ─────────────────────────────────────────
442
443    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    // ─── DNG / TIFF ──────────────────────────────────────────────────────
496
497    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    // ─── MXF (Material Exchange Format) ──────────────────────────────────
560
561    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// ─── Container corruption detection ───────────────────────────────────────────
590
591/// Result of a container integrity check.
592#[derive(Debug, Clone, PartialEq)]
593pub struct IntegrityCheckResult {
594    /// Whether the container passes structural validation.
595    pub valid: bool,
596    /// List of issues found during validation.
597    pub issues: Vec<String>,
598    /// Overall integrity score (0.0 = completely corrupted, 1.0 = perfect).
599    pub score: f64,
600}
601
602impl IntegrityCheckResult {
603    /// Creates a new passing result.
604    #[must_use]
605    pub fn ok() -> Self {
606        Self {
607            valid: true,
608            issues: Vec::new(),
609            score: 1.0,
610        }
611    }
612
613    /// Adds an issue and adjusts the score.
614    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/// Checks the structural integrity of a container's byte stream.
624#[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
699// ─── Format-specific parsers (private) ───────────────────────────────────────
700
701/// MPEG-TS scanning helper (kept in a sub-module to avoid name collisions).
702mod mpegts_probe {
703    use super::DetailedStreamInfo;
704    use crate::demux::mpegts_enhanced::TsDemuxer;
705
706    /// Stream type byte → codec name mapping (patent-free only).
707    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    /// Scans `data` for MPEG-TS packets, returning (streams, duration_ms).
729    pub fn scan_mpegts(data: &[u8]) -> (Vec<DetailedStreamInfo>, Option<u64>) {
730        let mut demux = TsDemuxer::new();
731        // Only scan the first 2 MB to keep probe fast
732        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        // Walk PMT streams
742        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
780// Format-specific parsers — extracted to `container_probe_parsers` module.
781use 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// ─── Unit tests ───────────────────────────────────────────────────────────────
787#[cfg(test)]
788mod tests {
789    use super::*;
790
791    // 1. has_video – true
792    #[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    // 2. has_video – false
800    #[test]
801    fn test_has_video_false() {
802        let r = ContainerProbeResult::new("flac");
803        assert!(!r.has_video());
804    }
805
806    // 3. has_audio – true
807    #[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    // 4. is_av – both present
815    #[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    // 5. is_av – audio only
824    #[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    // 6. is_confident threshold
832    #[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    // 7. ContainerInfo format_name
840    #[test]
841    fn test_container_info_format_name() {
842        let info = ContainerInfo::new("matroska");
843        assert_eq!(info.format_name(), "matroska");
844    }
845
846    // 8. ContainerInfo track_count
847    #[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    // 9. ContainerInfo video_count
854    #[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    // 10. estimated_bitrate_kbps – computes correctly
862    #[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        // 1 MB in 1 s = 8 Mbps = 8000 kbps
868        let kbps = info
869            .estimated_bitrate_kbps()
870            .expect("operation should succeed");
871        assert!((kbps - 8000.0).abs() < 1.0);
872    }
873
874    // 11. estimated_bitrate_kbps – None when no duration
875    #[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    // 12. ContainerProber detects Matroska
882    #[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    // 13. ContainerProber detects FLAC
893    #[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    // 14. ContainerProber detects MP4 via ftyp box
903    #[test]
904    fn test_probe_mp4() {
905        let mut p = ContainerProber::new();
906        // 4-byte box size + "ftyp"
907        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    // 15. ContainerProber unknown returns confidence 0
915    #[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    // ─── MultiFormatProber tests ─────────────────────────────────────────
924
925    // 16. Probe empty data → unknown format
926    #[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    // 17. Probe short random bytes → unknown
934    #[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    // 18. Probe FLAC magic → format = "flac"
941    #[test]
942    fn test_multiformat_probe_flac_magic() {
943        // fLaC + STREAMINFO block header (block_type=0, length=34) + 34 bytes of STREAMINFO
944        let mut data = Vec::new();
945        data.extend_from_slice(b"fLaC");
946        // Block header: last=0, type=0, length=34
947        data.push(0x00);
948        data.push(0x00);
949        data.push(0x00);
950        data.push(0x22); // 34
951                         // Minimal STREAMINFO (34 bytes): min_block=4096, max_block=4096, all zeros
952        data.extend_from_slice(&[0u8; 10]);
953        // sample_rate=44100 (0xAC44), channels=2, bits=16, total_samples=441000
954        // Packed: sample_rate(20) | channels(3) | bits(5) → bytes 10-12
955        // 44100 = 0xAC44 = 1010 1100 0100 0100
956        // bits 19..0 of sample_rate in si[10..12] plus channel and bps
957        // si[10] = 0b10101100 = 0xAC  (sample_rate bits 19..12)
958        // si[11] = 0b01000100 = 0x44  (sample_rate bits 11..4)
959        // si[12] = 0b0100_001_01111 → high nibble = sample_rate bits 3..0 (0100),
960        //          then channels-1 (2-1=1 → 001), then bps-1 (16-1=15 → 01111) split over [12][13]
961        // It is complex — use known-good bytes instead
962        data.push(0xAC); // si[0]  sample_rate high
963        data.push(0x44); // si[1]
964                         // si[2]: high nibble = sample_rate low 4 bits (0x4 = 0100), then ch-1(1)=001, bps-1(15)=01111
965                         // 0100 001 0 | 1111 xxxx  → si[2] = 0x42, si[3] = 0xF0
966        data.push(0x42);
967        data.push(0xF0);
968        // total_samples and MD5 — just zeros
969        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    // 19. Probe RIFF/WAVE → format = "wav"
979    #[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()); // file size - 8
985        data.extend_from_slice(b"WAVE");
986        data.extend_from_slice(b"fmt ");
987        data.extend_from_slice(&16u32.to_le_bytes()); // chunk size
988        data.extend_from_slice(&1u16.to_le_bytes()); // PCM format
989        data.extend_from_slice(&2u16.to_le_bytes()); // channels
990        data.extend_from_slice(&44100u32.to_le_bytes()); // sample rate
991        data.extend_from_slice(&(44100 * 2 * 2u32).to_le_bytes()); // byte rate
992        data.extend_from_slice(&4u16.to_le_bytes()); // block align
993        data.extend_from_slice(&16u16.to_le_bytes()); // bits per sample
994
995        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    // 20. Probe Ogg magic → format = "ogg"
1005    #[test]
1006    fn test_multiformat_probe_ogg() {
1007        // Minimal OggS page with OpusHead BOS
1008        let mut data = vec![0u8; 300];
1009        // OggS capture
1010        data[0..4].copy_from_slice(b"OggS");
1011        data[4] = 0; // version
1012        data[5] = 0x02; // header_type: BOS
1013                        // granule position (8 bytes)
1014        data[6..14].fill(0);
1015        // serial, sequence, checksum, n_segs
1016        data[14..18].fill(0); // serial
1017        data[18..22].fill(0); // sequence
1018        data[22..26].fill(0); // checksum
1019        data[26] = 1; // n_segs = 1
1020        data[27] = 19; // segment size = 19 (OpusHead)
1021                       // OpusHead payload
1022        data[28..36].copy_from_slice(b"OpusHead");
1023        data[36] = 1; // version
1024        data[37] = 2; // channels
1025        data[38..40].fill(0); // pre-skip
1026        data[40..44].copy_from_slice(&48000u32.to_le_bytes()); // sample rate
1027        data[44..46].fill(0); // output gain
1028        data[46] = 0; // channel mapping family
1029
1030        let info = MultiFormatProber::probe(&data);
1031        assert_eq!(info.format, "ogg");
1032    }
1033
1034    // 21. Probe MP4 ftyp magic → format = "mp4"
1035    #[test]
1036    fn test_multiformat_probe_mp4_magic() {
1037        let mut data = Vec::new();
1038        // ftyp box: size=20, "ftyp", "iso5", minor=0, compatible="iso5"
1039        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        // No moov — still detects as mp4
1045        let info = MultiFormatProber::probe(&data);
1046        assert_eq!(info.format, "mp4");
1047    }
1048
1049    // 22. Probe Matroska magic → format starts with "mkv" or "webm"
1050    #[test]
1051    fn test_multiformat_probe_mkv_magic() {
1052        // EBML header magic
1053        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    // 23. probe_streams_only delegates to probe correctly
1065    #[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()); // mono
1075        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    // 24. file_size_bytes is correctly reported
1086    #[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    // 25. WAV with data chunk gives duration
1094    #[test]
1095    fn test_multiformat_wav_duration() {
1096        let mut data = Vec::new();
1097        // 44100 Hz, mono, 16-bit, 44100 samples = 1000 ms
1098        let pcm_bytes: u32 = 44100 * 2; // samples * 2 bytes/sample
1099        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()); // PCM
1106        data.extend_from_slice(&1u16.to_le_bytes()); // mono
1107        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    // 26. DetailedStreamInfo default is empty
1121    #[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    // 27. DetailedContainerInfo metadata map is empty by default
1130    #[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    // ── CAF detection tests ──────────────────────────────────────────────────
1139
1140    // 28. Probe CAF magic
1141    #[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()); // version
1146        data.extend_from_slice(&0u16.to_be_bytes()); // flags
1147                                                     // desc chunk
1148        data.extend_from_slice(b"desc");
1149        data.extend_from_slice(&32u64.to_be_bytes()); // chunk size
1150                                                      // CAFAudioDescription
1151        data.extend_from_slice(&44100.0_f64.to_be_bytes()); // sample rate
1152        data.extend_from_slice(b"lpcm"); // format ID
1153        data.extend_from_slice(&0u32.to_be_bytes()); // format flags
1154        data.extend_from_slice(&4u32.to_be_bytes()); // bytes per packet
1155        data.extend_from_slice(&1u32.to_be_bytes()); // frames per packet
1156        data.extend_from_slice(&2u32.to_be_bytes()); // channels per frame
1157        data.extend_from_slice(&16u32.to_be_bytes()); // bits per channel
1158
1159        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    // 29. CAF with short data
1168    #[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        // No desc chunk
1175
1176        let info = MultiFormatProber::probe(&data);
1177        assert_eq!(info.format, "caf");
1178        assert!(info.streams.is_empty());
1179    }
1180
1181    // ── DNG / TIFF detection tests ───────────────────────────────────────────
1182
1183    // 30. TIFF LE detection without DNG tag
1184    #[test]
1185    fn test_probe_tiff_le() {
1186        let mut data = vec![0u8; 128];
1187        data[0] = 0x49; // 'I'
1188        data[1] = 0x49; // 'I'
1189        data[2] = 0x2A; // TIFF magic
1190        data[3] = 0x00;
1191        // IFD offset at byte 8
1192        data[4..8].copy_from_slice(&8u32.to_le_bytes());
1193        // IFD: entry count = 0
1194        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    // 31. DNG detection with DNGVersion tag
1201    #[test]
1202    fn test_probe_dng() {
1203        let mut data = vec![0u8; 128];
1204        data[0] = 0x49; // 'I'
1205        data[1] = 0x49; // 'I'
1206        data[2] = 0x2A;
1207        data[3] = 0x00;
1208        // IFD offset at byte 8
1209        data[4..8].copy_from_slice(&8u32.to_le_bytes());
1210        // IFD: 2 entries
1211        data[8..10].copy_from_slice(&2u16.to_le_bytes());
1212        // Entry 1: ImageWidth (0x0100) = 4000
1213        data[10..12].copy_from_slice(&0x0100u16.to_le_bytes());
1214        data[12..14].copy_from_slice(&3u16.to_le_bytes()); // type: SHORT
1215        data[14..18].copy_from_slice(&1u32.to_le_bytes()); // count
1216        data[18..22].copy_from_slice(&4000u32.to_le_bytes()); // value
1217                                                              // Entry 2: DNGVersion (0xC612) = 1
1218        data[22..24].copy_from_slice(&0xC612u16.to_le_bytes());
1219        data[24..26].copy_from_slice(&1u16.to_le_bytes()); // type: BYTE
1220        data[26..30].copy_from_slice(&4u32.to_le_bytes()); // count
1221        data[30..34].copy_from_slice(&1u32.to_le_bytes()); // value
1222
1223        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    // 32. TIFF BE detection
1232    #[test]
1233    fn test_probe_tiff_be() {
1234        let mut data = vec![0u8; 64];
1235        data[0] = 0x4D; // 'M'
1236        data[1] = 0x4D; // 'M'
1237        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    // ── MXF detection tests ──────────────────────────────────────────────────
1247
1248    // 33. MXF KLV header detection
1249    #[test]
1250    fn test_probe_mxf() {
1251        let mut data = vec![0u8; 128];
1252        // MXF header partition pack key
1253        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]); // header partition
1257
1258        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    // 34. MXF with video stream
1268    #[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]); // body partition
1275
1276        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    // ── Container integrity tests ────────────────────────────────────────────
1283
1284    // 35. Empty data
1285    #[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    // 36. Too short data
1293    #[test]
1294    fn test_integrity_too_short() {
1295        let result = check_container_integrity(&[0x00, 0x01, 0x02]);
1296        assert!(!result.valid);
1297    }
1298
1299    // 37. Valid MP4 structure
1300    #[test]
1301    fn test_integrity_valid_mp4() {
1302        let mut data = Vec::new();
1303        // ftyp box
1304        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    // 38. MP4 with bad box size
1316    #[test]
1317    fn test_integrity_mp4_bad_box() {
1318        let mut data = Vec::new();
1319        // ftyp box with size larger than data
1320        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    // 39. Valid FLAC structure
1329    #[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; // STREAMINFO block type
1334
1335        let result = check_container_integrity(&data);
1336        assert!(result.valid);
1337    }
1338
1339    // 40. FLAC too short
1340    #[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    // 41. Valid RIFF/WAV
1350    #[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]); // fmt data
1360        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    // 42. RIFF with size mismatch
1368    #[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()); // claims 100KB
1373        data.extend_from_slice(b"WAVE");
1374        data.extend_from_slice(&[0u8; 8]); // only 20 bytes total
1375
1376        let result = check_container_integrity(&data);
1377        assert!(result.score < 1.0);
1378    }
1379}