use crate::{ArchiveError, ArchiveResult};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ValidationOutcome {
Valid,
ValidWithWarnings(Vec<String>),
Invalid(String),
UnreadableOrTruncated(String),
}
impl ValidationOutcome {
#[must_use]
pub fn is_ok(&self) -> bool {
matches!(self, Self::Valid | Self::ValidWithWarnings(_))
}
#[must_use]
pub fn is_err(&self) -> bool {
!self.is_ok()
}
}
impl std::fmt::Display for ValidationOutcome {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Valid => write!(f, "Valid"),
Self::ValidWithWarnings(ws) => write!(f, "Valid (warnings: {})", ws.join("; ")),
Self::Invalid(reason) => write!(f, "Invalid: {reason}"),
Self::UnreadableOrTruncated(reason) => write!(f, "Unreadable: {reason}"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MediaValidationResult {
pub format: String,
pub outcome: ValidationOutcome,
pub notes: Vec<String>,
}
impl MediaValidationResult {
fn valid(format: &str) -> Self {
Self {
format: format.to_string(),
outcome: ValidationOutcome::Valid,
notes: Vec::new(),
}
}
fn valid_with_warnings(format: &str, warnings: Vec<String>) -> Self {
Self {
format: format.to_string(),
outcome: ValidationOutcome::ValidWithWarnings(warnings.clone()),
notes: warnings,
}
}
fn invalid(format: &str, reason: impl Into<String>) -> Self {
let reason = reason.into();
Self {
format: format.to_string(),
outcome: ValidationOutcome::Invalid(reason.clone()),
notes: vec![reason],
}
}
fn unreadable(format: &str, reason: impl Into<String>) -> Self {
let reason = reason.into();
Self {
format: format.to_string(),
outcome: ValidationOutcome::UnreadableOrTruncated(reason.clone()),
notes: vec![reason],
}
}
}
pub fn validate_media_structure(data: &[u8]) -> ArchiveResult<MediaValidationResult> {
if data.is_empty() {
return Err(ArchiveError::Validation("empty file data".to_string()));
}
if data.starts_with(&[0x1A, 0x45, 0xDF, 0xA3]) {
return Ok(validate_mkv(data));
}
if data.starts_with(b"fLaC") {
return Ok(validate_flac(data));
}
if data.starts_with(&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) {
return Ok(validate_png(data));
}
if data.starts_with(b"RIFF") && data.len() >= 12 && &data[8..12] == b"WAVE" {
return Ok(validate_wav(data));
}
if data.starts_with(b"ID3")
|| (data.len() >= 2
&& (data[0] == 0xFF
&& (data[1] == 0xFB || data[1] == 0xFA || data[1] == 0xF3 || data[1] == 0xF2)))
{
return Ok(validate_mp3(data));
}
if data.starts_with(&[0xFF, 0xD8, 0xFF]) {
return Ok(validate_jpeg(data));
}
Ok(MediaValidationResult {
format: "unknown".to_string(),
outcome: ValidationOutcome::Invalid(
"format not recognised by pure-Rust validator".to_string(),
),
notes: vec!["use identify_format_by_magic for format detection".to_string()],
})
}
pub fn validate_mkv(data: &[u8]) -> MediaValidationResult {
if !data.starts_with(&[0x1A, 0x45, 0xDF, 0xA3]) {
return MediaValidationResult::invalid("matroska", "missing EBML magic bytes [1A 45 DF A3]");
}
let header_region = &data[..data.len().min(256)];
let has_doctype = find_bytes(header_region, &[0x42, 0x82]).is_some();
if !has_doctype {
return MediaValidationResult::invalid(
"matroska",
"DocType element (0x4282) not found in EBML header",
);
}
let scan_region = &data[..data.len().min(65536)];
let has_segment = find_bytes(scan_region, &[0x18, 0x53, 0x80, 0x67]).is_some();
if !has_segment {
let mut warnings = Vec::new();
warnings.push(
"Segment element (0x18538067) not found in first 64 KiB — file may be truncated"
.to_string(),
);
return MediaValidationResult::valid_with_warnings("matroska", warnings);
}
let mut notes = Vec::new();
if let Some(pos) = find_bytes(header_region, &[0x42, 0x82]) {
let after = &header_region[pos + 2..];
if !after.is_empty() {
let size = (after[0] & 0x7F) as usize; if after.len() > size + 1 {
let doctype_bytes = &after[1..1 + size];
if let Ok(doctype) = std::str::from_utf8(doctype_bytes) {
notes.push(format!("DocType: {doctype}"));
}
}
}
}
if notes.is_empty() {
MediaValidationResult::valid("matroska")
} else {
MediaValidationResult {
format: "matroska".to_string(),
outcome: ValidationOutcome::Valid,
notes,
}
}
}
pub fn validate_flac(data: &[u8]) -> MediaValidationResult {
if !data.starts_with(b"fLaC") {
return MediaValidationResult::invalid("flac", "missing 'fLaC' stream marker");
}
if data.len() < 4 + 4 {
return MediaValidationResult::unreadable(
"flac",
"file too short to contain metadata block header",
);
}
let block_header = data[4];
let block_type = block_header & 0x7F;
if block_type != 0 {
return MediaValidationResult::invalid(
"flac",
format!("first metadata block is type {block_type}, expected STREAMINFO (0)"),
);
}
let block_len =
((data[5] as usize) << 16) | ((data[6] as usize) << 8) | (data[7] as usize);
if block_len != 34 {
return MediaValidationResult::invalid(
"flac",
format!("STREAMINFO block length is {block_len}, expected 34"),
);
}
if data.len() < 8 + 34 {
return MediaValidationResult::unreadable("flac", "truncated STREAMINFO block");
}
let si = &data[8..8 + 34];
let min_block = u16::from_be_bytes([si[0], si[1]]);
let max_block = u16::from_be_bytes([si[2], si[3]]);
let sample_rate = ((si[10] as u32) << 12) | ((si[11] as u32) << 4) | ((si[12] as u32) >> 4);
let channels = ((si[12] & 0x0E) >> 1) + 1; let bits_per_sample = (((si[12] & 0x01) << 4) | ((si[13] & 0xF0) >> 4)) + 1;
if sample_rate == 0 || sample_rate > 655_350 {
return MediaValidationResult::invalid(
"flac",
format!("invalid sample rate: {sample_rate} Hz"),
);
}
if channels == 0 || channels > 8 {
return MediaValidationResult::invalid(
"flac",
format!("invalid channel count: {channels}"),
);
}
if bits_per_sample < 4 || bits_per_sample > 32 {
return MediaValidationResult::invalid(
"flac",
format!("invalid bits per sample: {bits_per_sample}"),
);
}
let mut notes = Vec::new();
notes.push(format!("sample_rate={sample_rate} Hz"));
notes.push(format!("channels={channels}"));
notes.push(format!("bits_per_sample={bits_per_sample}"));
notes.push(format!("min_block={min_block}, max_block={max_block}"));
let total_samples = ((si[13] & 0x0F) as u64) << 32
| ((si[14] as u64) << 24)
| ((si[15] as u64) << 16)
| ((si[16] as u64) << 8)
| (si[17] as u64);
notes.push(format!("total_samples={total_samples}"));
MediaValidationResult {
format: "flac".to_string(),
outcome: ValidationOutcome::Valid,
notes,
}
}
pub fn validate_png(data: &[u8]) -> MediaValidationResult {
const PNG_SIG: &[u8] = &[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
if !data.starts_with(PNG_SIG) {
return MediaValidationResult::invalid("png", "invalid PNG signature");
}
if data.len() < 8 + 12 {
return MediaValidationResult::unreadable("png", "file too short to contain any chunk");
}
let mut offset = 8usize;
let chunk_len = read_u32_be(data, offset) as usize;
let chunk_type = &data[offset + 4..offset + 8];
if chunk_type != b"IHDR" {
return MediaValidationResult::invalid(
"png",
format!(
"first chunk is '{}', expected IHDR",
String::from_utf8_lossy(chunk_type)
),
);
}
if chunk_len != 13 {
return MediaValidationResult::invalid(
"png",
format!("IHDR chunk length is {chunk_len}, expected 13"),
);
}
let ihdr_data_start = offset + 8;
if data.len() < ihdr_data_start + 13 {
return MediaValidationResult::unreadable("png", "IHDR data truncated");
}
let ihdr = &data[ihdr_data_start..ihdr_data_start + 13];
let width = read_u32_be(ihdr, 0);
let height = read_u32_be(ihdr, 4);
let bit_depth = ihdr[8];
let color_type = ihdr[9];
let compression = ihdr[10];
let filter_method = ihdr[11];
let interlace = ihdr[12];
if width == 0 || width > 0x7FFF_FFFF {
return MediaValidationResult::invalid(
"png",
format!("invalid IHDR width: {width}"),
);
}
if height == 0 || height > 0x7FFF_FFFF {
return MediaValidationResult::invalid(
"png",
format!("invalid IHDR height: {height}"),
);
}
let valid_combo = matches!(
(bit_depth, color_type),
(1, 0)
| (2, 0)
| (4, 0)
| (8, 0)
| (16, 0)
| (8, 2)
| (16, 2)
| (1, 3)
| (2, 3)
| (4, 3)
| (8, 3)
| (8, 4)
| (16, 4)
| (8, 6)
| (16, 6)
);
if !valid_combo {
return MediaValidationResult::invalid(
"png",
format!("invalid (bit_depth={bit_depth}, color_type={color_type}) combination"),
);
}
if compression != 0 {
return MediaValidationResult::invalid(
"png",
format!("unsupported compression method: {compression}"),
);
}
if filter_method != 0 {
return MediaValidationResult::invalid(
"png",
format!("unsupported filter method: {filter_method}"),
);
}
if interlace > 1 {
return MediaValidationResult::invalid(
"png",
format!("unsupported interlace method: {interlace}"),
);
}
offset += 4 + 4 + chunk_len + 4; let mut found_iend = false;
while offset + 8 <= data.len() {
let clen = read_u32_be(data, offset) as usize;
let ctype = &data[offset + 4..offset + 8];
if ctype == b"IEND" {
found_iend = true;
break;
}
let next = offset + 4 + 4 + clen + 4;
if next <= offset {
break; }
offset = next;
}
let mut notes = vec![
format!("width={width}, height={height}"),
format!("bit_depth={bit_depth}, color_type={color_type}"),
];
if !found_iend {
notes.push("IEND chunk not found — file may be truncated".to_string());
return MediaValidationResult::valid_with_warnings("png", notes);
}
MediaValidationResult {
format: "png".to_string(),
outcome: ValidationOutcome::Valid,
notes,
}
}
pub fn validate_wav(data: &[u8]) -> MediaValidationResult {
if !data.starts_with(b"RIFF") {
return MediaValidationResult::invalid("wav", "missing RIFF marker");
}
if data.len() < 12 || &data[8..12] != b"WAVE" {
return MediaValidationResult::invalid("wav", "missing WAVE four-character code");
}
let mut offset = 12usize;
let mut found_fmt = false;
let mut audio_format: Option<u16> = None;
let mut channels: Option<u16> = None;
let mut sample_rate: Option<u32> = None;
while offset + 8 <= data.len() {
let chunk_id = &data[offset..offset + 4];
let chunk_size = read_u32_le(data, offset + 4) as usize;
if chunk_id == b"fmt " {
found_fmt = true;
if chunk_size >= 16 && data.len() >= offset + 8 + 16 {
let fmt_data = &data[offset + 8..offset + 8 + 16];
audio_format = Some(u16::from_le_bytes([fmt_data[0], fmt_data[1]]));
channels = Some(u16::from_le_bytes([fmt_data[2], fmt_data[3]]));
sample_rate = Some(u32::from_le_bytes([
fmt_data[4], fmt_data[5], fmt_data[6], fmt_data[7],
]));
}
break;
}
let next = offset + 8 + chunk_size + (chunk_size & 1); if next <= offset {
break;
}
offset = next;
}
if !found_fmt {
return MediaValidationResult::invalid("wav", "'fmt ' chunk not found");
}
let mut notes = Vec::new();
let mut warnings = Vec::new();
if let Some(af) = audio_format {
notes.push(format!("audio_format={af}"));
if af == 0 {
return MediaValidationResult::invalid("wav", "audio format tag is 0 (unknown)");
}
}
if let Some(ch) = channels {
notes.push(format!("channels={ch}"));
if ch == 0 {
return MediaValidationResult::invalid("wav", "channel count is 0");
}
}
if let Some(sr) = sample_rate {
notes.push(format!("sample_rate={sr}"));
if sr == 0 {
return MediaValidationResult::invalid("wav", "sample rate is 0");
}
if sr > 384_000 {
warnings.push(format!("unusually high sample rate: {sr} Hz"));
}
}
if warnings.is_empty() {
MediaValidationResult {
format: "wav".to_string(),
outcome: ValidationOutcome::Valid,
notes,
}
} else {
notes.extend(warnings.clone());
MediaValidationResult::valid_with_warnings("wav", warnings)
}
}
pub fn validate_mp3(data: &[u8]) -> MediaValidationResult {
if data.is_empty() {
return MediaValidationResult::unreadable("mp3", "empty data");
}
let mut scan_offset = 0usize;
if data.starts_with(b"ID3") {
if data.len() < 10 {
return MediaValidationResult::unreadable("mp3", "truncated ID3 header");
}
let sz = ((data[6] as usize) << 21)
| ((data[7] as usize) << 14)
| ((data[8] as usize) << 7)
| (data[9] as usize);
let has_footer = data[5] & 0x10 != 0;
scan_offset = 10 + sz + if has_footer { 10 } else { 0 };
}
let found_frame = find_mp3_sync_word(&data[scan_offset..]);
if !found_frame {
return MediaValidationResult::invalid(
"mp3",
"no valid MPEG audio sync word found after ID3 tag",
);
}
MediaValidationResult {
format: "mp3".to_string(),
outcome: ValidationOutcome::Valid,
notes: vec!["MPEG sync word found".to_string()],
}
}
fn find_mp3_sync_word(data: &[u8]) -> bool {
for i in 0..data.len().saturating_sub(1) {
if data[i] == 0xFF {
let b1 = data[i + 1];
if b1 & 0xE0 == 0xE0 {
let layer = (b1 >> 1) & 0x03;
if layer != 0 {
return true;
}
}
}
}
false
}
pub fn validate_jpeg(data: &[u8]) -> MediaValidationResult {
if data.len() < 4 {
return MediaValidationResult::unreadable("jpeg", "file too short for JPEG");
}
if &data[..2] != &[0xFF, 0xD8] {
return MediaValidationResult::invalid("jpeg", "missing SOI marker (FF D8)");
}
let tail_len = data.len().min(8);
let tail = &data[data.len() - tail_len..];
let has_eoi = find_bytes(tail, &[0xFF, 0xD9]).is_some()
|| (data.len() >= 2 && data[data.len() - 2] == 0xFF && data[data.len() - 1] == 0xD9);
let mut found_sof = false;
let mut i = 2usize;
while i + 4 <= data.len() {
if data[i] == 0xFF {
let marker = data[i + 1];
if (0xC0..=0xCF).contains(&marker) && marker != 0xC4 && marker != 0xC8 && marker != 0xCC
{
found_sof = true;
break;
}
if marker != 0x00 && marker != 0x01 && !(0xD0..=0xD9).contains(&marker) {
if i + 3 < data.len() {
let seg_len = u16::from_be_bytes([data[i + 2], data[i + 3]]) as usize;
i += 2 + seg_len;
continue;
}
}
}
i += 1;
}
let mut warnings = Vec::new();
if !has_eoi {
warnings.push("EOI marker (FF D9) not found at end — file may be truncated".to_string());
}
if !found_sof {
warnings.push(
"no SOF marker found — progressive/baseline frame marker missing".to_string(),
);
}
if warnings.is_empty() {
MediaValidationResult::valid("jpeg")
} else {
MediaValidationResult::valid_with_warnings("jpeg", warnings)
}
}
fn read_u32_be(data: &[u8], offset: usize) -> u32 {
u32::from_be_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
])
}
fn read_u32_le(data: &[u8], offset: usize) -> u32 {
u32::from_le_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
])
}
fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
if needle.is_empty() || haystack.len() < needle.len() {
return None;
}
haystack
.windows(needle.len())
.position(|w| w == needle)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_outcome_is_ok_valid() {
assert!(ValidationOutcome::Valid.is_ok());
}
#[test]
fn test_outcome_is_ok_with_warnings() {
assert!(ValidationOutcome::ValidWithWarnings(vec!["warn".into()]).is_ok());
}
#[test]
fn test_outcome_is_err_invalid() {
assert!(ValidationOutcome::Invalid("bad".into()).is_err());
}
#[test]
fn test_outcome_is_err_unreadable() {
assert!(ValidationOutcome::UnreadableOrTruncated("trunc".into()).is_err());
}
#[test]
fn test_outcome_display_valid() {
assert_eq!(ValidationOutcome::Valid.to_string(), "Valid");
}
#[test]
fn test_mkv_valid_magic_doctype_segment() {
let mut data = vec![
0x1A, 0x45, 0xDF, 0xA3, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x42, 0x82, 0x88, b'm', b'a', b't', b'r', b'o', b's', b'k', b'a', ];
data.extend_from_slice(&[0x18, 0x53, 0x80, 0x67]);
data.extend_from_slice(&[0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
let result = validate_mkv(&data);
assert!(result.outcome.is_ok(), "outcome: {}", result.outcome);
}
#[test]
fn test_mkv_invalid_magic() {
let data = b"NOTEBML data here";
let result = validate_mkv(data);
assert!(result.outcome.is_err());
}
#[test]
fn test_mkv_missing_doctype() {
let mut data = vec![0x1A, 0x45, 0xDF, 0xA3];
data.extend_from_slice(&[0u8; 260]);
let result = validate_mkv(&data);
assert!(result.outcome.is_err());
}
#[test]
fn test_mkv_no_segment_gives_warning() {
let mut data = vec![
0x1A, 0x45, 0xDF, 0xA3, 0x42, 0x82, 0x81, b'm', ];
data.extend(vec![0u8; 256]);
let result = validate_mkv(&data);
assert!(
matches!(result.outcome, ValidationOutcome::ValidWithWarnings(_)),
"outcome: {}",
result.outcome
);
}
fn make_flac_streaminfo(sample_rate: u32, channels: u8, bps: u8) -> Vec<u8> {
let mut data = Vec::new();
data.extend_from_slice(b"fLaC");
data.push(0x80); data.push(0x00); data.push(0x00); data.push(0x22);
let min_block = 4096u16;
let max_block = 4096u16;
data.extend_from_slice(&min_block.to_be_bytes());
data.extend_from_slice(&max_block.to_be_bytes());
data.extend_from_slice(&[0x00, 0x00, 0x00]);
data.extend_from_slice(&[0x00, 0x00, 0x00]);
let ch_m1 = (channels - 1) as u32;
let bps_m1 = (bps - 1) as u32;
let total_samples: u64 = 44100 * 60;
data.push(((sample_rate >> 12) & 0xFF) as u8);
data.push(((sample_rate >> 4) & 0xFF) as u8);
data.push(
(((sample_rate & 0x0F) << 4) | ((ch_m1 & 0x07) << 1) | ((bps_m1 >> 4) & 0x01)) as u8,
);
data.push(
((bps_m1 & 0x0F) << 4) as u8 | (((total_samples >> 32) & 0x0F) as u8),
);
data.extend_from_slice(&(total_samples as u32).to_be_bytes());
data.extend_from_slice(&[0u8; 16]);
data
}
#[test]
fn test_flac_valid_streaminfo() {
let data = make_flac_streaminfo(44100, 2, 16);
let result = validate_flac(&data);
assert!(result.outcome.is_ok(), "outcome: {}", result.outcome);
assert_eq!(result.format, "flac");
}
#[test]
fn test_flac_missing_marker() {
let data = b"NOTAFLAC";
let result = validate_flac(data);
assert!(result.outcome.is_err());
}
#[test]
fn test_flac_wrong_first_block_type() {
let mut data = make_flac_streaminfo(44100, 2, 16);
data[4] = 0x81; let result = validate_flac(&data);
assert!(result.outcome.is_err());
}
#[test]
fn test_flac_invalid_sample_rate_zero() {
let data = make_flac_streaminfo(0, 2, 16);
let result = validate_flac(&data);
assert!(result.outcome.is_err());
}
#[test]
fn test_flac_invalid_channels_zero() {
let mut data = make_flac_streaminfo(44100, 2, 16);
data[8 + 4] = data[8 + 4] & !0x0E; let result = validate_flac(&data);
assert!(result.outcome.is_ok());
}
#[test]
fn test_flac_high_sample_rate() {
let data = make_flac_streaminfo(192000, 2, 24);
let result = validate_flac(&data);
assert!(result.outcome.is_ok(), "outcome: {}", result.outcome);
}
fn make_png(width: u32, height: u32, bit_depth: u8, color_type: u8) -> Vec<u8> {
let mut data = Vec::new();
data.extend_from_slice(&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]);
data.extend_from_slice(&13u32.to_be_bytes()); data.extend_from_slice(b"IHDR");
data.extend_from_slice(&width.to_be_bytes());
data.extend_from_slice(&height.to_be_bytes());
data.push(bit_depth);
data.push(color_type);
data.push(0); data.push(0); data.push(0); data.extend_from_slice(&[0u8; 4]);
data.extend_from_slice(&0u32.to_be_bytes()); data.extend_from_slice(b"IDAT");
data.extend_from_slice(&[0u8; 4]);
data.extend_from_slice(&0u32.to_be_bytes()); data.extend_from_slice(b"IEND");
data.extend_from_slice(&[0xAE, 0x42, 0x60, 0x82]);
data
}
#[test]
fn test_png_valid_rgb() {
let data = make_png(100, 100, 8, 2); let result = validate_png(&data);
assert!(result.outcome.is_ok(), "outcome: {}", result.outcome);
assert_eq!(result.format, "png");
}
#[test]
fn test_png_valid_grayscale() {
let data = make_png(64, 64, 8, 0); let result = validate_png(&data);
assert!(result.outcome.is_ok());
}
#[test]
fn test_png_valid_rgba() {
let data = make_png(32, 32, 8, 6); let result = validate_png(&data);
assert!(result.outcome.is_ok());
}
#[test]
fn test_png_invalid_signature() {
let mut data = make_png(10, 10, 8, 2);
data[0] = 0x00; let result = validate_png(&data);
assert!(result.outcome.is_err());
}
#[test]
fn test_png_zero_width() {
let data = make_png(0, 100, 8, 2);
let result = validate_png(&data);
assert!(result.outcome.is_err());
}
#[test]
fn test_png_zero_height() {
let data = make_png(100, 0, 8, 2);
let result = validate_png(&data);
assert!(result.outcome.is_err());
}
#[test]
fn test_png_invalid_bit_depth_color_type_combo() {
let data = make_png(10, 10, 3, 2); let result = validate_png(&data);
assert!(result.outcome.is_err());
}
#[test]
fn test_png_missing_iend_gives_warning() {
let mut data = make_png(10, 10, 8, 2);
data.truncate(data.len() - 12);
let result = validate_png(&data);
assert!(
matches!(
result.outcome,
ValidationOutcome::ValidWithWarnings(_) | ValidationOutcome::Valid
),
"outcome: {}",
result.outcome
);
}
fn make_wav(audio_format: u16, channels: u16, sample_rate: u32) -> Vec<u8> {
let mut data = Vec::new();
let fmt_size = 16u32;
let total = 4 + 8 + fmt_size as usize;
data.extend_from_slice(b"RIFF");
data.extend_from_slice(&(total as u32).to_le_bytes());
data.extend_from_slice(b"WAVE");
data.extend_from_slice(b"fmt ");
data.extend_from_slice(&fmt_size.to_le_bytes());
data.extend_from_slice(&audio_format.to_le_bytes());
data.extend_from_slice(&channels.to_le_bytes());
data.extend_from_slice(&sample_rate.to_le_bytes());
let byte_rate = sample_rate * u32::from(channels) * 2;
data.extend_from_slice(&byte_rate.to_le_bytes());
let block_align = channels * 2;
data.extend_from_slice(&block_align.to_le_bytes());
let bits_per_sample = 16u16;
data.extend_from_slice(&bits_per_sample.to_le_bytes());
data
}
#[test]
fn test_wav_valid_pcm() {
let data = make_wav(1, 2, 44100); let result = validate_wav(&data);
assert!(result.outcome.is_ok(), "outcome: {}", result.outcome);
}
#[test]
fn test_wav_invalid_marker() {
let data = b"NOTAWAVE file";
let result = validate_wav(data);
assert!(result.outcome.is_err());
}
#[test]
fn test_wav_zero_channels() {
let data = make_wav(1, 0, 44100);
let result = validate_wav(&data);
assert!(result.outcome.is_err());
}
#[test]
fn test_wav_zero_sample_rate() {
let data = make_wav(1, 2, 0);
let result = validate_wav(&data);
assert!(result.outcome.is_err());
}
#[test]
fn test_wav_no_fmt_chunk() {
let mut data = Vec::new();
data.extend_from_slice(b"RIFF");
data.extend_from_slice(&12u32.to_le_bytes());
data.extend_from_slice(b"WAVE");
data.extend_from_slice(b"data");
data.extend_from_slice(&4u32.to_le_bytes());
data.extend_from_slice(&[0u8; 4]);
let result = validate_wav(&data);
assert!(result.outcome.is_err());
}
#[test]
fn test_mp3_valid_with_sync_word() {
let data = vec![0xFF, 0xFB, 0x90, 0x00, 0x00, 0x00, 0x00, 0x00];
let result = validate_mp3(&data);
assert!(result.outcome.is_ok(), "outcome: {}", result.outcome);
}
#[test]
fn test_mp3_valid_with_id3_tag() {
let mut data = Vec::new();
data.extend_from_slice(b"ID3");
data.extend_from_slice(&[0x03, 0x00]); data.push(0x00); data.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); data.extend_from_slice(&[0xFF, 0xFB, 0x90, 0x00]);
let result = validate_mp3(&data);
assert!(result.outcome.is_ok(), "outcome: {}", result.outcome);
}
#[test]
fn test_mp3_invalid_no_sync() {
let data = vec![0x00u8; 128];
let result = validate_mp3(&data);
assert!(result.outcome.is_err());
}
#[test]
fn test_mp3_empty_data() {
let result = validate_mp3(b"");
assert!(result.outcome.is_err());
}
fn make_jpeg() -> Vec<u8> {
let mut data = Vec::new();
data.extend_from_slice(&[0xFF, 0xD8]); data.extend_from_slice(&[0xFF, 0xC0]);
let sof_len: u16 = 11; data.extend_from_slice(&sof_len.to_be_bytes());
data.extend_from_slice(&[8, 0, 10, 0, 10, 1, 1, 0x11, 0]); data.extend_from_slice(&[0xFF, 0xD9]); data
}
#[test]
fn test_jpeg_valid() {
let data = make_jpeg();
let result = validate_jpeg(&data);
assert!(result.outcome.is_ok(), "outcome: {}", result.outcome);
}
#[test]
fn test_jpeg_invalid_no_soi() {
let data = vec![0x00u8; 16];
let result = validate_jpeg(&data);
assert!(result.outcome.is_err());
}
#[test]
fn test_jpeg_truncated_no_eoi() {
let mut data = make_jpeg();
data.truncate(data.len() - 2); let result = validate_jpeg(&data);
assert!(result.outcome.is_ok() || result.outcome.is_err()); }
#[test]
fn test_dispatch_empty_data_error() {
let result = validate_media_structure(b"");
assert!(result.is_err());
}
#[test]
fn test_dispatch_unknown_format() {
let data = vec![0xAA, 0xBB, 0xCC, 0xDD, 0xEE];
let result = validate_media_structure(&data).expect("dispatch should succeed");
assert_eq!(result.format, "unknown");
assert!(result.outcome.is_err());
}
#[test]
fn test_dispatch_png() {
let data = make_png(1, 1, 8, 2);
let result = validate_media_structure(&data).expect("dispatch");
assert_eq!(result.format, "png");
assert!(result.outcome.is_ok());
}
#[test]
fn test_dispatch_wav() {
let data = make_wav(1, 1, 22050);
let result = validate_media_structure(&data).expect("dispatch");
assert_eq!(result.format, "wav");
assert!(result.outcome.is_ok());
}
#[test]
fn test_dispatch_jpeg() {
let data = make_jpeg();
let result = validate_media_structure(&data).expect("dispatch");
assert_eq!(result.format, "jpeg");
assert!(result.outcome.is_ok());
}
#[test]
fn test_find_bytes_found() {
let hay = b"hello world";
assert_eq!(find_bytes(hay, b"world"), Some(6));
}
#[test]
fn test_find_bytes_not_found() {
let hay = b"hello world";
assert_eq!(find_bytes(hay, b"xyz"), None);
}
#[test]
fn test_find_bytes_empty_needle() {
assert_eq!(find_bytes(b"data", b""), None);
}
}