#![allow(dead_code)]
#[derive(Debug, Clone, PartialEq)]
pub struct ContainerProbeResult {
pub video_present: bool,
pub audio_present: bool,
pub subtitle_present: bool,
pub confidence: f32,
pub format_label: String,
}
impl ContainerProbeResult {
#[must_use]
pub fn new(format_label: impl Into<String>) -> Self {
Self {
video_present: false,
audio_present: false,
subtitle_present: false,
confidence: 1.0,
format_label: format_label.into(),
}
}
#[must_use]
pub fn has_video(&self) -> bool {
self.video_present
}
#[must_use]
pub fn has_audio(&self) -> bool {
self.audio_present
}
#[must_use]
pub fn is_av(&self) -> bool {
self.video_present && self.audio_present
}
#[must_use]
pub fn is_confident(&self, threshold: f32) -> bool {
self.confidence >= threshold
}
}
#[derive(Debug, Clone)]
pub struct ContainerInfo {
format_name: String,
total_tracks: usize,
video_count: usize,
audio_count: usize,
duration_ms: Option<u64>,
file_size: Option<u64>,
}
impl ContainerInfo {
#[must_use]
pub fn new(format_name: impl Into<String>) -> Self {
Self {
format_name: format_name.into(),
total_tracks: 0,
video_count: 0,
audio_count: 0,
duration_ms: None,
file_size: None,
}
}
#[must_use]
pub fn with_tracks(mut self, video: usize, audio: usize) -> Self {
self.video_count = video;
self.audio_count = audio;
self.total_tracks = video + audio;
self
}
#[must_use]
pub fn with_duration_ms(mut self, ms: u64) -> Self {
self.duration_ms = Some(ms);
self
}
#[must_use]
pub fn with_file_size(mut self, bytes: u64) -> Self {
self.file_size = Some(bytes);
self
}
#[must_use]
pub fn format_name(&self) -> &str {
&self.format_name
}
#[must_use]
pub fn track_count(&self) -> usize {
self.total_tracks
}
#[must_use]
pub fn video_count(&self) -> usize {
self.video_count
}
#[must_use]
pub fn audio_count(&self) -> usize {
self.audio_count
}
#[must_use]
pub fn duration_ms(&self) -> Option<u64> {
self.duration_ms
}
#[allow(clippy::cast_precision_loss)]
#[must_use]
pub fn estimated_bitrate_kbps(&self) -> Option<f64> {
match (self.file_size, self.duration_ms) {
(Some(bytes), Some(ms)) if ms > 0 => Some((bytes as f64 * 8.0) / (ms as f64)),
_ => None,
}
}
}
#[derive(Debug, Default)]
pub struct ContainerProber {
probed_count: usize,
}
impl ContainerProber {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn probed_count(&self) -> usize {
self.probed_count
}
pub fn probe_header(&mut self, header: &[u8]) -> ContainerProbeResult {
self.probed_count += 1;
if header.len() >= 4 && header[..4] == [0x1A, 0x45, 0xDF, 0xA3] {
let mut r = ContainerProbeResult::new("matroska");
r.video_present = true;
r.audio_present = true;
return r;
}
if header.len() >= 4 && &header[..4] == b"fLaC" {
let mut r = ContainerProbeResult::new("flac");
r.audio_present = true;
return r;
}
if header.len() >= 4 && &header[..4] == b"OggS" {
let mut r = ContainerProbeResult::new("ogg");
r.audio_present = true;
return r;
}
if header.len() >= 4 && &header[..4] == b"RIFF" {
let mut r = ContainerProbeResult::new("wav");
r.audio_present = true;
return r;
}
if header.len() >= 8 && &header[4..8] == b"ftyp" {
let mut r = ContainerProbeResult::new("mp4");
r.video_present = true;
r.audio_present = true;
return r;
}
let mut r = ContainerProbeResult::new("unknown");
r.confidence = 0.0;
r
}
}
#[derive(Debug, Clone, Default)]
pub struct DetailedStreamInfo {
pub index: u32,
pub stream_type: String,
pub codec: String,
pub language: Option<String>,
pub duration_ms: Option<u64>,
pub bitrate_kbps: Option<u32>,
pub width: Option<u32>,
pub height: Option<u32>,
pub fps: Option<f32>,
pub pixel_format: Option<String>,
pub sample_rate: Option<u32>,
pub channels: Option<u8>,
pub sample_format: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct DetailedContainerInfo {
pub format: String,
pub duration_ms: Option<u64>,
pub bitrate_kbps: Option<u32>,
pub streams: Vec<DetailedStreamInfo>,
pub metadata: std::collections::HashMap<String, String>,
pub file_size_bytes: u64,
}
#[derive(Debug, Default)]
pub struct MultiFormatProber;
impl MultiFormatProber {
#[must_use]
pub fn new() -> Self {
Self
}
#[must_use]
pub fn probe(data: &[u8]) -> DetailedContainerInfo {
let mut info = DetailedContainerInfo {
file_size_bytes: data.len() as u64,
..Default::default()
};
if data.len() < 8 {
info.format = "unknown".into();
return info;
}
if data[0] == 0x47 && (data.len() < 376 || data[188] == 0x47) {
Self::probe_mpegts(data, &mut info);
} else if data[..4] == [0x1A, 0x45, 0xDF, 0xA3] {
Self::probe_mkv(data, &mut info);
} else if data.len() >= 8 && &data[4..8] == b"ftyp" {
Self::probe_mp4(data, &mut info);
} else if &data[..4] == b"OggS" {
Self::probe_ogg(data, &mut info);
} else if &data[..4] == b"RIFF" {
Self::probe_wav(data, &mut info);
} else if &data[..4] == b"fLaC" {
Self::probe_flac(data, &mut info);
} else if data.len() >= 4 && &data[..4] == b"caff" {
Self::probe_caf(data, &mut info);
} else if data.len() >= 8 && data[0..2] == [0x49, 0x49] && data[2..4] == [0x2A, 0x00] {
Self::probe_dng_tiff(data, &mut info);
} else if data.len() >= 8 && data[0..2] == [0x4D, 0x4D] && data[2..4] == [0x00, 0x2A] {
Self::probe_dng_tiff(data, &mut info);
} else if data.len() >= 16
&& data[0..4] == [0x06, 0x0E, 0x2B, 0x34]
&& data[4..8] == [0x02, 0x05, 0x01, 0x01]
{
Self::probe_mxf(data, &mut info);
} else {
info.format = "unknown".into();
}
if let (Some(dur_ms), sz) = (info.duration_ms, info.file_size_bytes) {
if let Some(bitrate) = sz.saturating_mul(8).checked_div(dur_ms) {
info.bitrate_kbps = Some(bitrate as u32);
}
}
info
}
#[must_use]
pub fn probe_streams_only(data: &[u8]) -> Vec<DetailedStreamInfo> {
Self::probe(data).streams
}
fn probe_mpegts(data: &[u8], info: &mut DetailedContainerInfo) {
use crate::container_probe::mpegts_probe::*;
info.format = "mpeg-ts".into();
let (streams, duration_ms) = scan_mpegts(data);
info.streams = streams;
info.duration_ms = duration_ms;
}
fn probe_mp4(data: &[u8], info: &mut DetailedContainerInfo) {
info.format = "mp4".into();
let mut offset = 0usize;
while offset + 8 <= data.len() {
let box_size = u32::from_be_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
]) as usize;
let fourcc = &data[offset + 4..offset + 8];
if box_size < 8 || offset + box_size > data.len() {
break;
}
if fourcc == b"moov" {
parse_moov(&data[offset + 8..offset + box_size], info);
break;
}
offset += box_size;
}
}
fn probe_mkv(data: &[u8], info: &mut DetailedContainerInfo) {
info.format = "mkv".into();
parse_ebml_for_info(data, info);
}
fn probe_ogg(data: &[u8], info: &mut DetailedContainerInfo) {
info.format = "ogg".into();
parse_ogg_bos(data, info);
}
fn probe_wav(data: &[u8], info: &mut DetailedContainerInfo) {
info.format = "wav".into();
if data.len() >= 12 && &data[8..12] == b"WAVE" {
parse_wav_chunks(data, info);
}
}
fn probe_flac(data: &[u8], info: &mut DetailedContainerInfo) {
info.format = "flac".into();
parse_flac_streaminfo(data, info);
}
fn probe_caf(data: &[u8], info: &mut DetailedContainerInfo) {
info.format = "caf".into();
if data.len() < 8 {
return;
}
let version = u16::from_be_bytes([data[4], data[5]]);
info.metadata
.insert("caf_version".into(), format!("{version}"));
let mut offset = 8usize;
while offset + 12 <= data.len() {
let chunk_type = &data[offset..offset + 4];
let chunk_size = read_u64_be(data, offset + 4);
if chunk_type == b"desc" && chunk_size >= 32 && offset + 44 <= data.len() {
let desc = &data[offset + 12..];
let sr = f64::from_be_bytes([
desc[0], desc[1], desc[2], desc[3], desc[4], desc[5], desc[6], desc[7],
]);
let codec = String::from_utf8_lossy(&desc[8..12]).trim().to_string();
let ch = if desc.len() >= 28 {
read_u32_be(desc, 24)
} else {
0
};
let mut s = DetailedStreamInfo {
index: 0,
stream_type: "audio".into(),
codec,
..Default::default()
};
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
{
s.sample_rate = Some(sr as u32);
if ch > 0 && ch < 256 {
s.channels = Some(ch as u8);
}
}
info.streams.push(s);
}
let advance = 12 + chunk_size as usize;
if advance == 0 {
break;
}
match offset.checked_add(advance) {
Some(new_offset) => offset = new_offset,
None => break,
}
}
}
fn probe_dng_tiff(data: &[u8], info: &mut DetailedContainerInfo) {
let is_le = data[0] == 0x49;
let ru16 = |off: usize| -> u16 {
if off + 2 > data.len() {
return 0;
}
if is_le {
u16::from_le_bytes([data[off], data[off + 1]])
} else {
u16::from_be_bytes([data[off], data[off + 1]])
}
};
let ru32 = |off: usize| -> u32 {
if off + 4 > data.len() {
return 0;
}
if is_le {
u32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
} else {
u32::from_be_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
}
};
let ifd_offset = ru32(4) as usize;
if ifd_offset + 2 > data.len() {
info.format = "tiff".into();
return;
}
let entry_count = ru16(ifd_offset) as usize;
let (mut found_dng, mut width, mut height) = (false, 0u32, 0u32);
for i in 0..entry_count {
let off = ifd_offset + 2 + i * 12;
if off + 12 > data.len() {
break;
}
match ru16(off) {
0xC612 => found_dng = true,
0x0100 => width = ru32(off + 8),
0x0101 => height = ru32(off + 8),
_ => {}
}
}
if found_dng {
info.format = "dng".into();
let mut s = DetailedStreamInfo {
index: 0,
stream_type: "video".into(),
codec: "raw".into(),
..Default::default()
};
if width > 0 {
s.width = Some(width);
}
if height > 0 {
s.height = Some(height);
}
info.streams.push(s);
} else {
info.format = "tiff".into();
}
}
fn probe_mxf(data: &[u8], info: &mut DetailedContainerInfo) {
info.format = "mxf".into();
if data.len() >= 16 {
let pt = data[13];
let label = match pt {
0x02 => "header_partition",
0x03 => "body_partition",
0x04 => "footer_partition",
_ => "unknown_partition",
};
info.metadata
.insert("mxf_partition_type".into(), label.into());
}
if data.len() >= 12 && data[8] == 0x0D && data[9] == 0x01 {
info.metadata
.insert("mxf_registry".into(), "smpte_rdd".into());
}
if data.len() >= 64 {
info.streams.push(DetailedStreamInfo {
index: 0,
stream_type: "video".into(),
codec: "mxf_essence".into(),
..Default::default()
});
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct IntegrityCheckResult {
pub valid: bool,
pub issues: Vec<String>,
pub score: f64,
}
impl IntegrityCheckResult {
#[must_use]
pub fn ok() -> Self {
Self {
valid: true,
issues: Vec::new(),
score: 1.0,
}
}
pub fn add_issue(&mut self, issue: impl Into<String>, severity: f64) {
self.issues.push(issue.into());
self.score = (self.score - severity).max(0.0);
if self.score < 0.5 {
self.valid = false;
}
}
}
#[must_use]
pub fn check_container_integrity(data: &[u8]) -> IntegrityCheckResult {
let mut r = IntegrityCheckResult::ok();
if data.is_empty() {
r.add_issue("Container data is empty", 1.0);
return r;
}
if data.len() < 8 {
r.add_issue("Too short for any known format", 0.8);
return r;
}
if &data[4..8] == b"ftyp" {
validate_mp4_boxes(data, &mut r);
} else if &data[..4] == b"fLaC" {
validate_flac_structure(data, &mut r);
} else if &data[..4] == b"RIFF" {
validate_riff_structure(data, &mut r);
}
r
}
fn validate_mp4_boxes(data: &[u8], result: &mut IntegrityCheckResult) {
let (mut offset, mut box_count, mut found_moov) = (0usize, 0u32, false);
while offset + 8 <= data.len() {
let size = read_u32_be(data, offset) as usize;
if size < 8 {
result.add_issue(format!("Bad MP4 box size at {offset}"), 0.3);
break;
}
if offset + size > data.len() {
result.add_issue(format!("MP4 box exceeds data at {offset}"), 0.2);
break;
}
if &data[offset + 4..offset + 8] == b"moov" {
found_moov = true;
}
box_count += 1;
offset += size;
}
if box_count == 0 {
result.add_issue("No valid MP4 boxes", 0.5);
}
if !found_moov && data.len() > 1024 {
result.add_issue("MP4 missing moov", 0.3);
}
}
fn validate_flac_structure(data: &[u8], result: &mut IntegrityCheckResult) {
if data.len() < 42 {
result.add_issue("FLAC too short for STREAMINFO", 0.4);
return;
}
if data[4] & 0x7F != 0 {
result.add_issue("First FLAC block not STREAMINFO", 0.3);
}
}
fn validate_riff_structure(data: &[u8], result: &mut IntegrityCheckResult) {
if data.len() < 12 {
result.add_issue("RIFF too short", 0.5);
return;
}
if &data[8..12] != b"WAVE" && &data[8..12] != b"AVI " {
result.add_issue("RIFF form type not WAVE/AVI", 0.2);
}
let riff_size = u32::from_le_bytes([data[4], data[5], data[6], data[7]]) as u64;
if riff_size + 8 > data.len() as u64 {
result.add_issue(
format!("RIFF size mismatch ({} vs {})", riff_size + 8, data.len()),
0.15,
);
}
}
mod mpegts_probe {
use super::DetailedStreamInfo;
use crate::demux::mpegts_enhanced::TsDemuxer;
fn stream_type_to_codec(st: u8) -> Option<&'static str> {
match st {
0x85 => Some("av1"),
0x84 => Some("vp9"),
0x83 => Some("vp8"),
0x81 => Some("opus"),
0x82 => Some("flac"),
0x80 => Some("pcm"),
0x06 => Some("private"),
_ => None,
}
}
fn stream_type_to_kind(st: u8) -> &'static str {
match st {
0x85 | 0x84 | 0x83 | 0x1B | 0x24 => "video",
0x81 | 0x82 | 0x80 | 0x03 | 0x04 | 0x0F | 0x11 => "audio",
_ => "data",
}
}
pub fn scan_mpegts(data: &[u8]) -> (Vec<DetailedStreamInfo>, Option<u64>) {
let mut demux = TsDemuxer::new();
let scan_end = data.len().min(2 * 1024 * 1024);
demux.feed(&data[..scan_end]);
let si = demux.stream_info();
let duration_ms = demux.duration_ms();
let mut streams: Vec<DetailedStreamInfo> = Vec::new();
let mut idx = 0u32;
for pmt in si.pmts.values() {
for ps in &pmt.streams {
let codec = stream_type_to_codec(ps.stream_type)
.unwrap_or("unknown")
.to_string();
let kind = stream_type_to_kind(ps.stream_type).to_string();
let pid_info = si.pids.get(&ps.elementary_pid);
let mut s = DetailedStreamInfo {
index: idx,
stream_type: kind.clone(),
codec,
..Default::default()
};
if let Some(pi) = pid_info {
if let (Some(f), Some(l)) = (pi.pts_first, pi.pts_last) {
if l > f {
s.duration_ms = Some((l - f) / 90);
}
}
if s.duration_ms.is_some() && pi.total_bytes > 0 {
let dur_s = s.duration_ms.unwrap_or(1) as u64;
if let Some(bitrate) = (pi.total_bytes * 8).checked_div(dur_s) {
s.bitrate_kbps = Some(bitrate as u32);
}
}
}
streams.push(s);
idx += 1;
}
}
(streams, duration_ms)
}
}
use crate::container_probe_parsers::{
parse_ebml_for_info, parse_flac_streaminfo, parse_moov, parse_ogg_bos, parse_wav_chunks,
read_u32_be, read_u64_be,
};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_has_video_true() {
let mut r = ContainerProbeResult::new("mkv");
r.video_present = true;
assert!(r.has_video());
}
#[test]
fn test_has_video_false() {
let r = ContainerProbeResult::new("flac");
assert!(!r.has_video());
}
#[test]
fn test_has_audio_true() {
let mut r = ContainerProbeResult::new("ogg");
r.audio_present = true;
assert!(r.has_audio());
}
#[test]
fn test_is_av_both() {
let mut r = ContainerProbeResult::new("mp4");
r.video_present = true;
r.audio_present = true;
assert!(r.is_av());
}
#[test]
fn test_is_av_audio_only() {
let mut r = ContainerProbeResult::new("wav");
r.audio_present = true;
assert!(!r.is_av());
}
#[test]
fn test_is_confident() {
let r = ContainerProbeResult::new("matroska");
assert!(r.is_confident(0.9));
assert!(!r.is_confident(1.1));
}
#[test]
fn test_container_info_format_name() {
let info = ContainerInfo::new("matroska");
assert_eq!(info.format_name(), "matroska");
}
#[test]
fn test_container_info_track_count() {
let info = ContainerInfo::new("mp4").with_tracks(1, 2);
assert_eq!(info.track_count(), 3);
}
#[test]
fn test_container_info_video_count() {
let info = ContainerInfo::new("mkv").with_tracks(2, 4);
assert_eq!(info.video_count(), 2);
assert_eq!(info.audio_count(), 4);
}
#[test]
fn test_estimated_bitrate_kbps() {
let info = ContainerInfo::new("mp4")
.with_file_size(1_000_000)
.with_duration_ms(1000);
let kbps = info
.estimated_bitrate_kbps()
.expect("operation should succeed");
assert!((kbps - 8000.0).abs() < 1.0);
}
#[test]
fn test_estimated_bitrate_kbps_no_duration() {
let info = ContainerInfo::new("mkv").with_file_size(1_000_000);
assert!(info.estimated_bitrate_kbps().is_none());
}
#[test]
fn test_probe_matroska() {
let mut p = ContainerProber::new();
let magic = [0x1A, 0x45, 0xDF, 0xA3, 0x00, 0x00, 0x00, 0x00];
let r = p.probe_header(&magic);
assert_eq!(r.format_label, "matroska");
assert!(r.has_video());
assert!(r.has_audio());
}
#[test]
fn test_probe_flac() {
let mut p = ContainerProber::new();
let r = p.probe_header(b"fLaC\x00\x00\x00\x22");
assert_eq!(r.format_label, "flac");
assert!(!r.has_video());
assert!(r.has_audio());
}
#[test]
fn test_probe_mp4() {
let mut p = ContainerProber::new();
let header = b"\x00\x00\x00\x18ftyp\x69\x73\x6f\x6d";
let r = p.probe_header(header);
assert_eq!(r.format_label, "mp4");
assert!(r.has_video());
assert_eq!(p.probed_count(), 1);
}
#[test]
fn test_probe_unknown() {
let mut p = ContainerProber::new();
let r = p.probe_header(b"\xFF\xFF\xFF\xFF");
assert_eq!(r.format_label, "unknown");
assert_eq!(r.confidence, 0.0);
}
#[test]
fn test_multiformat_probe_empty() {
let info = MultiFormatProber::probe(&[]);
assert_eq!(info.format, "unknown");
assert!(info.streams.is_empty());
}
#[test]
fn test_multiformat_probe_random() {
let info = MultiFormatProber::probe(&[0xFF, 0xFE, 0xFD, 0xFC, 0x00, 0x00, 0x00, 0x00]);
assert_eq!(info.format, "unknown");
}
#[test]
fn test_multiformat_probe_flac_magic() {
let mut data = Vec::new();
data.extend_from_slice(b"fLaC");
data.push(0x00);
data.push(0x00);
data.push(0x00);
data.push(0x22); data.extend_from_slice(&[0u8; 10]);
data.push(0xAC); data.push(0x44); data.push(0x42);
data.push(0xF0);
data.extend_from_slice(&[0u8; 20]);
let info = MultiFormatProber::probe(&data);
assert_eq!(info.format, "flac");
assert!(!info.streams.is_empty());
assert_eq!(info.streams[0].codec, "flac");
assert_eq!(info.streams[0].stream_type, "audio");
}
#[test]
fn test_multiformat_probe_wav() {
let mut data = Vec::new();
data.extend_from_slice(b"RIFF");
let total_size: u32 = 36;
data.extend_from_slice(&total_size.to_le_bytes()); data.extend_from_slice(b"WAVE");
data.extend_from_slice(b"fmt ");
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);
assert_eq!(info.format, "wav");
assert!(!info.streams.is_empty());
let s = &info.streams[0];
assert_eq!(s.codec, "pcm");
assert_eq!(s.sample_rate, Some(44100));
assert_eq!(s.channels, Some(2));
}
#[test]
fn test_multiformat_probe_ogg() {
let mut data = vec![0u8; 300];
data[0..4].copy_from_slice(b"OggS");
data[4] = 0; data[5] = 0x02; data[6..14].fill(0);
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");
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);
assert_eq!(info.format, "ogg");
}
#[test]
fn test_multiformat_probe_mp4_magic() {
let mut data = Vec::new();
data.extend_from_slice(&20u32.to_be_bytes());
data.extend_from_slice(b"ftyp");
data.extend_from_slice(b"iso5");
data.extend_from_slice(&0u32.to_be_bytes());
data.extend_from_slice(b"iso5");
let info = MultiFormatProber::probe(&data);
assert_eq!(info.format, "mp4");
}
#[test]
fn test_multiformat_probe_mkv_magic() {
let data = [
0x1A, 0x45, 0xDF, 0xA3, 0x84, 0x42, 0x82, 0x84, 0x77, 0x65, 0x62, 0x6D, 0x00,
];
let info = MultiFormatProber::probe(&data);
assert!(
info.format == "mkv" || info.format == "webm",
"got format: {}",
info.format
);
}
#[test]
fn test_probe_streams_only() {
let mut data = Vec::new();
data.extend_from_slice(b"RIFF");
data.extend_from_slice(&36u32.to_le_bytes());
data.extend_from_slice(b"WAVE");
data.extend_from_slice(b"fmt ");
data.extend_from_slice(&16u32.to_le_bytes());
data.extend_from_slice(&1u16.to_le_bytes());
data.extend_from_slice(&1u16.to_le_bytes()); data.extend_from_slice(&22050u32.to_le_bytes());
data.extend_from_slice(&(22050u32 * 2).to_le_bytes());
data.extend_from_slice(&2u16.to_le_bytes());
data.extend_from_slice(&16u16.to_le_bytes());
let streams = MultiFormatProber::probe_streams_only(&data);
assert!(!streams.is_empty());
assert_eq!(streams[0].stream_type, "audio");
}
#[test]
fn test_multiformat_file_size() {
let data = b"not a real container at all, just some bytes";
let info = MultiFormatProber::probe(data);
assert_eq!(info.file_size_bytes, data.len() as u64);
}
#[test]
fn test_multiformat_wav_duration() {
let mut data = Vec::new();
let pcm_bytes: u32 = 44100 * 2; let total: u32 = 36 + pcm_bytes;
data.extend_from_slice(b"RIFF");
data.extend_from_slice(&total.to_le_bytes());
data.extend_from_slice(b"WAVE");
data.extend_from_slice(b"fmt ");
data.extend_from_slice(&16u32.to_le_bytes());
data.extend_from_slice(&1u16.to_le_bytes()); data.extend_from_slice(&1u16.to_le_bytes()); data.extend_from_slice(&44100u32.to_le_bytes());
data.extend_from_slice(&(44100u32 * 2).to_le_bytes());
data.extend_from_slice(&2u16.to_le_bytes());
data.extend_from_slice(&16u16.to_le_bytes());
data.extend_from_slice(b"data");
data.extend_from_slice(&pcm_bytes.to_le_bytes());
data.extend(vec![0u8; pcm_bytes as usize]);
let info = MultiFormatProber::probe(&data);
assert_eq!(info.format, "wav");
assert_eq!(info.duration_ms, Some(1000));
}
#[test]
fn test_detailed_stream_info_default() {
let s = DetailedStreamInfo::default();
assert!(s.codec.is_empty());
assert!(s.stream_type.is_empty());
assert!(s.duration_ms.is_none());
}
#[test]
fn test_detailed_container_info_metadata() {
let info = DetailedContainerInfo::default();
assert!(info.metadata.is_empty());
assert!(info.streams.is_empty());
assert_eq!(info.file_size_bytes, 0);
}
#[test]
fn test_multiformat_probe_caf() {
let mut data = Vec::new();
data.extend_from_slice(b"caff");
data.extend_from_slice(&1u16.to_be_bytes()); data.extend_from_slice(&0u16.to_be_bytes()); data.extend_from_slice(b"desc");
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);
assert_eq!(info.format, "caf");
assert!(!info.streams.is_empty());
assert_eq!(info.streams[0].stream_type, "audio");
assert_eq!(info.streams[0].sample_rate, Some(44100));
assert_eq!(info.streams[0].channels, Some(2));
}
#[test]
fn test_caf_short_data() {
let mut data = Vec::new();
data.extend_from_slice(b"caff");
data.extend_from_slice(&1u16.to_be_bytes());
data.extend_from_slice(&0u16.to_be_bytes());
let info = MultiFormatProber::probe(&data);
assert_eq!(info.format, "caf");
assert!(info.streams.is_empty());
}
#[test]
fn test_probe_tiff_le() {
let mut data = vec![0u8; 128];
data[0] = 0x49; data[1] = 0x49; data[2] = 0x2A; data[3] = 0x00;
data[4..8].copy_from_slice(&8u32.to_le_bytes());
data[8..10].copy_from_slice(&0u16.to_le_bytes());
let info = MultiFormatProber::probe(&data);
assert_eq!(info.format, "tiff");
}
#[test]
fn test_probe_dng() {
let mut data = vec![0u8; 128];
data[0] = 0x49; data[1] = 0x49; data[2] = 0x2A;
data[3] = 0x00;
data[4..8].copy_from_slice(&8u32.to_le_bytes());
data[8..10].copy_from_slice(&2u16.to_le_bytes());
data[10..12].copy_from_slice(&0x0100u16.to_le_bytes());
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());
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);
assert_eq!(info.format, "dng");
assert!(!info.streams.is_empty());
assert_eq!(info.streams[0].stream_type, "video");
assert_eq!(info.streams[0].codec, "raw");
assert_eq!(info.streams[0].width, Some(4000));
}
#[test]
fn test_probe_tiff_be() {
let mut data = vec![0u8; 64];
data[0] = 0x4D; data[1] = 0x4D; data[2] = 0x00;
data[3] = 0x2A;
data[4..8].copy_from_slice(&8u32.to_be_bytes());
data[8..10].copy_from_slice(&0u16.to_be_bytes());
let info = MultiFormatProber::probe(&data);
assert_eq!(info.format, "tiff");
}
#[test]
fn test_probe_mxf() {
let mut data = vec![0u8; 128];
data[0..4].copy_from_slice(&[0x06, 0x0E, 0x2B, 0x34]);
data[4..8].copy_from_slice(&[0x02, 0x05, 0x01, 0x01]);
data[8..12].copy_from_slice(&[0x0D, 0x01, 0x02, 0x01]);
data[12..16].copy_from_slice(&[0x01, 0x02, 0x04, 0x00]);
let info = MultiFormatProber::probe(&data);
assert_eq!(info.format, "mxf");
assert!(info.metadata.contains_key("mxf_partition_type"));
assert_eq!(
info.metadata.get("mxf_partition_type"),
Some(&"header_partition".to_string())
);
}
#[test]
fn test_probe_mxf_streams() {
let mut data = vec![0u8; 128];
data[0..4].copy_from_slice(&[0x06, 0x0E, 0x2B, 0x34]);
data[4..8].copy_from_slice(&[0x02, 0x05, 0x01, 0x01]);
data[8..12].copy_from_slice(&[0x0D, 0x01, 0x02, 0x01]);
data[12..16].copy_from_slice(&[0x01, 0x03, 0x04, 0x00]);
let info = MultiFormatProber::probe(&data);
assert_eq!(info.format, "mxf");
assert!(!info.streams.is_empty());
assert_eq!(info.streams[0].codec, "mxf_essence");
}
#[test]
fn test_integrity_empty() {
let result = check_container_integrity(&[]);
assert!(!result.valid);
assert!(!result.issues.is_empty());
}
#[test]
fn test_integrity_too_short() {
let result = check_container_integrity(&[0x00, 0x01, 0x02]);
assert!(!result.valid);
}
#[test]
fn test_integrity_valid_mp4() {
let mut data = Vec::new();
data.extend_from_slice(&20u32.to_be_bytes());
data.extend_from_slice(b"ftyp");
data.extend_from_slice(b"iso5");
data.extend_from_slice(&0u32.to_be_bytes());
data.extend_from_slice(b"iso5");
let result = check_container_integrity(&data);
assert!(result.valid);
assert!(result.score > 0.5);
}
#[test]
fn test_integrity_mp4_bad_box() {
let mut data = Vec::new();
data.extend_from_slice(&200u32.to_be_bytes());
data.extend_from_slice(b"ftyp");
data.extend_from_slice(&[0u8; 12]);
let result = check_container_integrity(&data);
assert!(result.score < 1.0);
}
#[test]
fn test_integrity_valid_flac() {
let mut data = vec![0u8; 50];
data[0..4].copy_from_slice(b"fLaC");
data[4] = 0x00;
let result = check_container_integrity(&data);
assert!(result.valid);
}
#[test]
fn test_integrity_flac_short() {
let mut data = vec![0u8; 20];
data[0..4].copy_from_slice(b"fLaC");
let result = check_container_integrity(&data);
assert!(result.score < 1.0);
}
#[test]
fn test_integrity_valid_wav() {
let data_size: u32 = 36;
let mut data = Vec::new();
data.extend_from_slice(b"RIFF");
data.extend_from_slice(&data_size.to_le_bytes());
data.extend_from_slice(b"WAVE");
data.extend_from_slice(b"fmt ");
data.extend_from_slice(&16u32.to_le_bytes());
data.extend_from_slice(&[0u8; 16]); data.extend_from_slice(b"data");
data.extend_from_slice(&0u32.to_le_bytes());
let result = check_container_integrity(&data);
assert!(result.valid);
}
#[test]
fn test_integrity_riff_size_mismatch() {
let mut data = Vec::new();
data.extend_from_slice(b"RIFF");
data.extend_from_slice(&100_000u32.to_le_bytes()); data.extend_from_slice(b"WAVE");
data.extend_from_slice(&[0u8; 8]);
let result = check_container_integrity(&data);
assert!(result.score < 1.0);
}
}