Merge pull request #331 from grovebit/corrupt-audio

Refuse corrupt audio files instead of crashing
This commit is contained in:
Brandon Thomas
2026-10-13 04:43:37 -04:00
committed by GitHub
8 changed files with 543 additions and 26 deletions
+87
View File
@@ -0,0 +1,87 @@
//! Size checks on CAF chunks before symphonia reads them: it allocates whatever a magic cookie,
//! a packet table or a packet claims, and a failed allocation aborts the process.
use crate::pcm::{be_u16, be_u32, be_u64};
use crate::{AudioError, Result};
/// Refuse the CAF stream at `at` if its magic cookie, packet table or packets claim more bytes
/// than the file holds.
pub(crate) fn check(b: &[u8], at: usize) -> Result<()> {
if be_u16(b, at + 4) == Some(1) { chunks(b, at + 8) } else { Ok(()) }
}
fn malformed(what: &str) -> AudioError {
AudioError::Malformed(format!("CAF {what} is larger than the file"))
}
/// Walk the chunks the way symphonia 0.5 reads them: it advances by the bytes it parses, not by
/// the declared size, and after a `data` chunk of unknown size it reads on into the audio.
fn chunks(b: &[u8], mut pos: usize) -> Result<()> {
// Whether the packet table lists each packet's size and frame count (variable per packet).
let (mut lists_sizes, mut lists_frames, mut described) = (false, false, false);
while let (Some(kind), Some(size)) = (b.get(pos..pos + 4), be_u64(b, pos + 4)) {
let (body, size) = (pos + 12, size as i64);
let used = match kind {
b"desc" if size == 32 => {
lists_sizes = be_u32(b, body + 16) == Some(0);
lists_frames = be_u32(b, body + 20) == Some(0);
described = true;
Some(32)
}
b"data" if size == -1 => Some(4),
b"chan" if size >= 12 => be_u32(b, body + 8).map(|n| 12 + u64::from(n) * 20),
b"pakt" if size >= 24 && described => packet_table(b, body, lists_sizes, lists_frames)?,
b"kuki" if size >= 0 && size as u64 > b.len().saturating_sub(body) as u64 => return Err(malformed("magic cookie")),
b"desc" | b"pakt" | b"chan" => None,
_ => u64::try_from(size).ok(),
};
match used.filter(|_| described).and_then(|n| usize::try_from(n).ok()).and_then(|n| body.checked_add(n)) {
Some(next) if next < b.len() => pos = next,
_ => return Ok(()),
}
}
Ok(())
}
/// Check a packet table's count and packet sizes; returns the bytes symphonia reads for it, or
/// None where symphonia stops. It reserves room for every packet before reading any, so a table
/// it would refuse after that, or that runs past the file, is refused first.
fn packet_table(b: &[u8], body: usize, lists_sizes: bool, lists_frames: bool) -> Result<Option<u64>> {
let valid = |at: usize| be_u64(b, at).filter(|&n| n as i64 >= 0);
let (Some(packets), Some(_)) = (valid(body), valid(body + 8)) else { return Ok(None) };
let mut at = body + 24;
if packets > b.len().saturating_sub(at) as u64 {
return Err(malformed("packet table"));
}
if !lists_sizes && !lists_frames {
return if packets == 0 { Ok(Some(24)) } else { Err(AudioError::Malformed("a constant bit rate CAF lists packets".into())) };
}
for _ in 0..packets {
if lists_sizes {
let Some((size, len)) = varint(b, at) else { return Err(malformed("packet table")) };
if size > b.len() as u64 {
return Err(malformed("packet"));
}
at += len;
}
if lists_frames {
let Some((_, len)) = varint(b, at) else { return Err(malformed("packet table")) };
at += len;
}
}
Ok(Some((at - body) as u64))
}
/// A CAF variable-length integer at `at` and its length in bytes.
fn varint(b: &[u8], at: usize) -> Option<(u64, usize)> {
let mut v = 0u64;
for i in 0..9 {
let x = *b.get(at + i)?;
v |= u64::from(x & 0x7f);
if x & 0x80 == 0 {
return Some((v, i + 1));
}
v <<= 7;
}
None
}
+50 -2
View File
@@ -1,8 +1,8 @@
//! A small pure-Rust FLAC encoder (RFC 9639): fixed blocksize, independent channels, and per
//! subframe the cheapest of CONSTANT, VERBATIM or FIXED (orders 0–4) prediction with
//! partitioned Rice-coded residuals.
//! partitioned Rice-coded residuals. Decoding is symphonia's, after [`check_metadata`].
use crate::pcm::{Quantizer, validate_buffer};
use crate::pcm::{Quantizer, be_u32, le_u32, validate_buffer};
use crate::{AudioBuffer, AudioError, BitDepth, Result};
const BLOCK_SIZE: usize = 4096;
@@ -354,6 +354,54 @@ pub(crate) fn encode(buf: &AudioBuffer, depth: BitDepth, dither: bool) -> Result
Ok(out)
}
/// Refuse the FLAC stream at `at` if a comment or picture field claims more bytes than its
/// metadata block holds: symphonia allocates each field's length before reading it.
pub(crate) fn check_metadata(b: &[u8], at: usize) -> Result<()> {
let mut pos = at + 4;
while let Some(head) = be_u32(b, pos) {
let (body, len) = (pos + 4, (head & 0x00ff_ffff) as usize);
let end = (body + len).min(b.len());
let overruns = match (head >> 24) & 0x7f {
4 => comments_overrun(b, body, end),
// Picture: type, MIME type, description, four size fields, data.
6 => overruns(b, body + 4, end, be_u32, [0, 0, 16]),
_ => false,
};
if overruns {
return Err(AudioError::Malformed("a FLAC metadata field is larger than its block".into()));
}
if head >> 31 == 1 {
break;
}
pos = body + len;
}
Ok(())
}
/// Whether a comment in a (little-endian) Vorbis comment block claims more than the block holds.
/// The vendor string is skipped unread.
fn comments_overrun(b: &[u8], body: usize, end: usize) -> bool {
let Some(vendor) = le_u32(b, body) else { return false };
let at = (body + 4).saturating_add(vendor as usize);
let Some(count) = le_u32(b, at).filter(|_| at.saturating_add(4) <= end) else { return false };
overruns(b, at + 4, end, le_u32, std::iter::repeat_n(0, count as usize))
}
/// Whether one of the length-prefixed fields from `at` claims more than `end` leaves; `gaps` are
/// the bytes skipped before each length, which `read` reads.
fn overruns(b: &[u8], mut at: usize, end: usize, read: fn(&[u8], usize) -> Option<u32>, gaps: impl IntoIterator<Item = usize>) -> bool {
for gap in gaps {
at = at.saturating_add(gap);
let Some(n) = read(b, at).filter(|_| at.saturating_add(4) <= end) else { return false };
at += 4;
if n as usize > end - at {
return true;
}
at += n as usize;
}
false
}
#[cfg(test)]
mod tests {
use super::*;
+8 -6
View File
@@ -14,7 +14,9 @@
mod aiff;
mod buffer;
mod caf;
mod flac;
mod mp4;
mod pcm;
pub mod peaks;
mod symph;
@@ -205,16 +207,16 @@ pub fn probe(bytes: &[u8], ext_hint: Option<&str>) -> Result<AudioInfo> {
FileFormat::Wav if wav::is_wav(bytes) => match wav::parse(bytes)? {
wav::WavParse::Native(l) => Ok(l.info),
wav::WavParse::Foreign { bwf } => {
let mut info = symph::probe(bytes, Some("wav"), format)?;
let mut info = symph::probe(bytes, format)?;
info.bwf = bwf;
Ok(info)
}
},
FileFormat::Aiff if aiff::is_aiff(bytes) => match aiff::parse(bytes)? {
aiff::AiffParse::Native(l) => Ok(l.info),
aiff::AiffParse::Foreign => symph::probe(bytes, Some("aiff"), format),
aiff::AiffParse::Foreign => symph::probe(bytes, format),
},
_ => symph::probe(bytes, ext_hint, format),
_ => symph::probe(bytes, format),
}
}
@@ -230,7 +232,7 @@ pub fn decode(bytes: &[u8], ext_hint: Option<&str>) -> Result<(AudioInfo, AudioB
Ok((info, buf))
}
wav::WavParse::Foreign { bwf } => {
let (mut info, buf) = symph::decode(bytes, Some("wav"), format)?;
let (mut info, buf) = symph::decode(bytes, format)?;
info.bwf = bwf;
Ok((info, buf))
}
@@ -242,9 +244,9 @@ pub fn decode(bytes: &[u8], ext_hint: Option<&str>) -> Result<(AudioInfo, AudioB
info.frames = buf.frames() as u64;
Ok((info, buf))
}
aiff::AiffParse::Foreign => symph::decode(bytes, Some("aiff"), format),
aiff::AiffParse::Foreign => symph::decode(bytes, format),
},
_ => symph::decode(bytes, ext_hint, format),
_ => symph::decode(bytes, format),
}
}
+106
View File
@@ -0,0 +1,106 @@
//! Size checks on MP4 atoms before symphonia reads them: it allocates whatever an atom, a sample
//! table or a sample claims, and a failed allocation aborts the process.
use crate::pcm::{be_u32, be_u64};
use crate::{AudioError, Result};
#[derive(Clone, Copy)]
enum Level {
Top,
Moov,
Trak,
Mdia,
Minf,
Stbl,
Udta,
Meta,
Ilst,
Tag,
}
/// Refuse the MP4 file whose top-level atoms start at `at` if an atom symphonia reads whole, a
/// sample table or a sample claims more bytes than the file holds.
pub(crate) fn check(b: &[u8], at: usize) -> Result<()> {
atoms(b, at, b.len(), Level::Top).map(|_| ())
}
fn malformed(what: &str) -> AudioError {
AudioError::Malformed(format!("MP4 {what} is larger than the file"))
}
/// Walk the atoms in `pos..end` that symphonia descends into, as it reads them. False where
/// symphonia stops reading: at a header it refuses, or after an atom that runs past `end`.
fn atoms(b: &[u8], mut pos: usize, end: usize, level: Level) -> Result<bool> {
// A header that starts inside the container is read whole, even past its end.
while pos < end {
let (Some(size), Some(kind)) = (be_u32(b, pos), b.get(pos + 4..pos + 8)) else { return Ok(false) };
let head = if size == 1 { 16 } else { 8 };
// Size 0: no data, and the next atom starts at the container's end.
let len = match size {
0 => head,
1 => be_u64(b, pos + 8).unwrap_or(0),
n => u64::from(n),
};
if len < head {
return Ok(false);
}
let body = pos + head as usize;
let data_end = usize::try_from(len).ok().and_then(|n| pos.checked_add(n));
let next = if size == 0 { Some(end) } else { data_end };
let inner = data_end.map_or(b.len(), |n| n.min(b.len()));
let more = match (level, kind) {
(Level::Top, b"moov") => atoms(b, body, inner, Level::Moov)?,
(Level::Top | Level::Udta, b"meta") => atoms(b, body + 4, inner, Level::Meta)?,
(Level::Moov, b"trak") => atoms(b, body, inner, Level::Trak)?,
(Level::Moov, b"udta") => atoms(b, body, inner, Level::Udta)?,
(Level::Trak, b"mdia") => atoms(b, body, inner, Level::Mdia)?,
(Level::Mdia, b"minf") => atoms(b, body, inner, Level::Minf)?,
(Level::Minf, b"stbl") => atoms(b, body, inner, Level::Stbl)?,
(Level::Meta, b"ilst") => atoms(b, body, inner, Level::Ilst)?,
(Level::Ilst, _) => atoms(b, body, inner, Level::Tag)?,
(Level::Stbl, b"stts" | b"co64") => table(b, body + 4, 8, inner)?,
(Level::Stbl, b"stsc") => table(b, body + 4, 12, inner)?,
(Level::Stbl, b"stco") => table(b, body + 4, 4, inner)?,
(Level::Stbl, b"stsz") => sample_sizes(b, body, inner)?,
(Level::Mdia, b"hdlr") | (Level::Tag, b"data" | b"name" | b"mean") if len > (b.len() - pos) as u64 => {
return Err(malformed(&format!("`{}` atom", String::from_utf8_lossy(kind))));
}
_ => true,
};
match next {
Some(n) if more && n <= end => pos = n,
_ => return Ok(false),
}
}
Ok(true)
}
/// A sample table's entries, counted at `count`, must fit in the file: symphonia reserves room for
/// all of them first. False if they run past the table's atom (`end`), where symphonia stops.
fn table(b: &[u8], count: usize, entry: u64, end: usize) -> Result<bool> {
let Some(n) = be_u32(b, count) else { return Ok(false) };
let (first, bytes) = (count + 4, u64::from(n) * entry);
if bytes > b.len().saturating_sub(first) as u64 {
return Err(malformed("sample table"));
}
Ok(first as u64 + bytes <= end as u64)
}
/// Every sample's size must fit in the file: symphonia reads each one into a buffer of that size.
/// Sizes past the atom are never used, as symphonia stops before reading a packet.
fn sample_sizes(b: &[u8], body: usize, end: usize) -> Result<bool> {
let (Some(size), Some(count)) = (be_u32(b, body + 4), be_u32(b, body + 8)) else { return Ok(false) };
let file = b.len() as u64;
if u64::from(size) > file {
return Err(malformed("sample"));
}
if size != 0 {
return Ok(true);
}
let fits = table(b, body + 8, 4, end)?;
let (sizes, _) = b.get(body + 12..end).unwrap_or_default().as_chunks::<4>();
if sizes.iter().take(count as usize).any(|s| u64::from(u32::from_be_bytes(*s)) > file) {
return Err(malformed("sample"));
}
Ok(fits)
}
+6
View File
@@ -36,6 +36,12 @@ pub(crate) fn be_u32(b: &[u8], pos: usize) -> Option<u32> {
Some(u32::from_be_bytes(s))
}
/// Big-endian `u64` at `pos`.
pub(crate) fn be_u64(b: &[u8], pos: usize) -> Option<u64> {
let s: [u8; 8] = b.get(pos..pos.checked_add(8)?)?.try_into().ok()?;
Some(u64::from_be_bytes(s))
}
/// Four-byte tag at `pos`.
pub(crate) fn tag(b: &[u8], pos: usize) -> Option<[u8; 4]> {
b.get(pos..pos.checked_add(4)?)?.try_into().ok()
+47 -18
View File
@@ -12,13 +12,17 @@ use symphonia::core::codecs::{
};
use symphonia::core::errors::Error as SymError;
use symphonia::core::formats::{FormatOptions, FormatReader};
use symphonia::core::io::{MediaSourceStream, MediaSourceStreamOptions};
use symphonia::core::io::{MediaSourceStream, MediaSourceStreamOptions, ReadBytes};
use symphonia::core::meta::MetadataOptions;
use symphonia::core::probe::Hint;
use symphonia::core::probe::Instantiate;
use crate::pcm::MAX_DECODE_SAMPLES;
use crate::pcm::{MAX_DECODE_SAMPLES, be_u32};
use crate::{AudioBuffer, AudioError, AudioInfo, FileFormat, Result, SampleFormat};
/// The most frames a packet may declare: decoders allocate a packet's frames up front, and ALAC
/// takes its frame length from the file (encoders use 4096).
const MAX_PACKET_FRAMES: u64 = 1 << 20;
fn map_err(e: SymError) -> AudioError {
match e {
SymError::Unsupported(m) => AudioError::Unsupported(m.to_string()),
@@ -32,15 +36,36 @@ fn guarded<T>(f: impl FnOnce() -> Result<T>) -> Result<T> {
catch_unwind(AssertUnwindSafe(f)).unwrap_or_else(|_| Err(AudioError::Malformed("decoder failed on malformed input".into())))
}
fn open(bytes: &[u8], ext_hint: Option<&str>) -> Result<Box<dyn FormatReader>> {
let mss = MediaSourceStream::new(Box::new(Cursor::new(bytes.to_vec())), MediaSourceStreamOptions::default());
let mut hint = Hint::new();
if let Some(ext) = ext_hint {
hint.with_extension(ext.trim_start_matches('.'));
}
/// Symphonia's probe loop (`Probe::format`, whose hint 0.5 ignores), checking the container it
/// finds before its reader allocates whatever the file claims.
fn open(bytes: &[u8]) -> Result<Box<dyn FormatReader>> {
let mut mss = MediaSourceStream::new(Box::new(Cursor::new(bytes.to_vec())), MediaSourceStreamOptions::default());
let fmt_opts = FormatOptions { enable_gapless: true, ..FormatOptions::default() };
let probed = symphonia::default::get_probe().format(&hint, mss, &fmt_opts, &MetadataOptions::default()).map_err(map_err)?;
Ok(probed.format)
loop {
let found = symphonia::default::get_probe().next(&mut mss).map_err(map_err)?;
let at = usize::try_from(mss.pos()).unwrap_or(usize::MAX);
let head = bytes.get(at..).unwrap_or_default();
match (found, head.get(..10)) {
(Instantiate::Format(reader), _) => {
match head.get(..4) {
Some(b"caff") => crate::caf::check(bytes, at)?,
Some(b"ftyp") => crate::mp4::check(bytes, at.saturating_sub(4))?,
Some(b"fLaC") => crate::flac::check_metadata(bytes, at)?,
Some(b"RIFF") => crate::wav::check_info(bytes, at)?,
_ => {}
}
return reader(mss, &fmt_opts).map_err(map_err);
}
// Tags aren't used, and symphonia allocates whatever an ID3v2 frame claims: skip it.
(Instantiate::Metadata(_), Some([b'I', b'D', b'3', _, _, _, size @ ..])) => {
let size = size.iter().fold(0u64, |v, &x| (v << 7) | u64::from(x & 0x7f));
mss.ignore_bytes(10 + size).map_err(|e| AudioError::Malformed(e.to_string()))?;
}
(Instantiate::Metadata(tag), _) => {
tag(&MetadataOptions::default()).read_all(&mut mss).map_err(map_err)?;
}
}
}
}
fn format_from_codec(codec: CodecType, detected: FileFormat) -> FileFormat {
@@ -115,8 +140,8 @@ fn info_of(reader: &dyn FormatReader, detected: FileFormat) -> Result<(u32, Audi
}
/// Decode the whole first audio track. When `count_only`, samples are counted but not stored.
fn run(bytes: &[u8], ext_hint: Option<&str>, detected: FileFormat, count_only: bool) -> Result<(AudioInfo, AudioBuffer)> {
let mut reader = open(bytes, ext_hint)?;
fn run(bytes: &[u8], detected: FileFormat, count_only: bool) -> Result<(AudioInfo, AudioBuffer)> {
let mut reader = open(bytes)?;
let (track_id, mut info) = info_of(reader.as_ref(), detected)?;
if let Some(n) = reader.tracks().iter().find(|t| t.id == track_id).and_then(|t| t.codec_params.n_frames) {
let total = n.saturating_mul(u64::from(info.channels.max(1)));
@@ -130,6 +155,10 @@ fn run(bytes: &[u8], ext_hint: Option<&str>, detected: FileFormat, count_only: b
.find(|t| t.id == track_id)
.map(|t| t.codec_params.clone())
.ok_or_else(|| AudioError::Malformed("track vanished".into()))?;
let alac = params.extra_data.as_deref().filter(|_| params.codec == CODEC_TYPE_ALAC).and_then(|d| be_u32(d, 0));
if params.max_frames_per_packet.into_iter().chain(alac.map(u64::from)).any(|n| n > MAX_PACKET_FRAMES) {
return Err(AudioError::Malformed("the file declares packets far longer than any codec uses".into()));
}
let mut decoder = symphonia::default::get_codecs().make(&params, &DecoderOptions::default()).map_err(map_err)?;
let mut planar: Vec<Vec<f32>> = Vec::new();
@@ -206,18 +235,18 @@ fn run(bytes: &[u8], ext_hint: Option<&str>, detected: FileFormat, count_only: b
Ok((info, AudioBuffer { sample_rate, channels: planar }))
}
pub(crate) fn probe(bytes: &[u8], ext_hint: Option<&str>, detected: FileFormat) -> Result<AudioInfo> {
pub(crate) fn probe(bytes: &[u8], detected: FileFormat) -> Result<AudioInfo> {
guarded(|| {
let reader = open(bytes, ext_hint)?;
let reader = open(bytes)?;
let (_, info) = info_of(reader.as_ref(), detected)?;
if info.frames > 0 && info.channels > 0 {
return Ok(info);
}
// Length not in the header (e.g. MP3 without a Xing frame): count by decoding.
run(bytes, ext_hint, detected, true).map(|(i, _)| i)
run(bytes, detected, true).map(|(i, _)| i)
})
}
pub(crate) fn decode(bytes: &[u8], ext_hint: Option<&str>, detected: FileFormat) -> Result<(AudioInfo, AudioBuffer)> {
guarded(|| run(bytes, ext_hint, detected, false))
pub(crate) fn decode(bytes: &[u8], detected: FileFormat) -> Result<(AudioInfo, AudioBuffer)> {
guarded(|| run(bytes, detected, false))
}
+51
View File
@@ -83,6 +83,57 @@ fn parse_bext(c: &[u8]) -> Option<BwfInfo> {
})
}
/// Refuse the WAVE file at `at`, which symphonia reads (A-law, ADPCM, ...), if an INFO entry
/// claims more bytes than the file holds: symphonia allocates each entry's length. Chunks are
/// walked where symphonia reads them: after the bytes a chunk's parser used, not its length.
pub(crate) fn check_info(b: &[u8], at: usize) -> Result<()> {
if tag(b, at + 8) != Some(*b"WAVE") {
return Ok(());
}
let mut pos = at + 12;
while let Some(id) = tag(b, pos) {
let Some(len) = le_u32(b, pos + 4).map(|n| n as usize) else { break };
let (body, pad) = (pos + 8, len & 1);
let used = match &id {
b"data" => break,
b"fmt " => fmt_used(b, body, len),
b"LIST" if len < 4 => break,
b"LIST" => {
let entries = if tag(b, body) == Some(*b"INFO") { info_entries(b, body + 4, len - 4)? } else { 0 };
// The list pads itself, then the chunk is padded again.
4 + entries.max(len - 4) + pad
}
_ => len,
};
pos = body.saturating_add(used).saturating_add(pad);
}
Ok(())
}
/// The bytes symphonia reads of a `fmt ` chunk: some formats' parsers go by their own extra size.
fn fmt_used(b: &[u8], body: usize, len: usize) -> usize {
match le_u16(b, body) {
Some(2 | 6 | 7 | 0x11) => le_u16(b, body + 16).map_or(len, |extra| 18 + usize::from(extra)),
Some(0xfffe) => 40,
_ => len,
}
}
/// Check the INFO entries in the `len` bytes from `start`; returns the bytes symphonia reads of
/// them, with the pad it reads after an odd entry.
fn info_entries(b: &[u8], start: usize, len: usize) -> Result<usize> {
let mut used = 0;
while used + 8 <= len {
let Some(n) = le_u32(b, start + used + 4).map(|n| n as usize) else { break };
if n > b.len().saturating_sub(start + used + 8) {
return Err(AudioError::Malformed("a WAV INFO entry is larger than the file".into()));
}
used += 8 + n;
used += used & 1;
}
Ok(used)
}
/// True when the bytes start like a RIFF/RF64/BW64 WAVE file.
pub(crate) fn is_wav(b: &[u8]) -> bool {
matches!(tag(b, 0), Some(t) if &t == b"RIFF" || &t == b"RF64" || &t == b"BW64") && tag(b, 8).is_some_and(|t| &t == b"WAVE")
+188
View File
@@ -153,6 +153,194 @@ fn absurd_chunk_sizes() {
assert_eq!(decode(&aiff, None).unwrap().1.frames(), 0);
}
fn caf(chunks: &[(&[u8; 4], i64, Vec<u8>)]) -> Vec<u8> {
let mut out = b"caff\0\x01\0\0".to_vec();
for (kind, size, body) in chunks {
out.extend_from_slice(*kind);
out.extend_from_slice(&size.to_be_bytes());
out.extend_from_slice(body);
}
out
}
fn caf_desc(format: &[u8; 4], bytes_per_packet: u32, frames_per_packet: u32, channels: u32, bits: u32) -> (&'static [u8; 4], i64, Vec<u8>) {
let mut d = 48_000f64.to_be_bytes().to_vec();
d.extend_from_slice(format);
for v in [0, bytes_per_packet, frames_per_packet, channels, bits] {
d.extend_from_slice(&v.to_be_bytes());
}
(b"desc", 32, d)
}
fn packet_table(packets: i64, frames: i64, entries: &[u8]) -> Vec<u8> {
[&packets.to_be_bytes()[..], &frames.to_be_bytes(), &[0; 8], entries].concat()
}
#[test]
fn caf_sizes_beyond_the_file_are_errors_not_aborts() {
let aac = || caf_desc(b"aac ", 0, 1024, 2, 0);
let huge = 1i64 << 50;
let cookie = caf(&[aac(), (b"kuki", huge, vec![0; 8])]);
let files = [
cookie.clone(),
[vec![0; 100], cookie].concat(),
caf(&[aac(), (b"data", -1, [&[0; 4][..], b"kuki", &huge.to_be_bytes()].concat())]),
caf(&[aac(), (b"pakt", 24, packet_table(huge, 1024, &[])), (b"data", -1, vec![0; 16])]),
caf(&[aac(), (b"pakt", 32, packet_table(1, 1024, &[0x82, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0])), (b"data", -1, vec![0; 64])]),
caf(&[aac(), (b"pakt", 25, packet_table(1, 1024, &[0x80]))]),
];
for f in files {
assert!(matches!(probe(&f, None), Err(AudioError::Malformed(m)) if m.contains("larger than the file")));
assert!(matches!(decode(&f, None), Err(AudioError::Malformed(m)) if m.contains("larger than the file")));
}
let constant = caf(&[caf_desc(b"lpcm", 2, 1, 1, 16), (b"pakt", 25, packet_table(1, 1, &[1])), (b"data", -1, vec![0; 16])]);
assert!(matches!(decode(&constant, None), Err(AudioError::Malformed(m)) if m.contains("lists packets")));
}
#[test]
fn well_formed_caf_still_reads() {
let pcm: Vec<u8> = (0..480i16).flat_map(|i| (i * 50).to_be_bytes()).collect();
let chan = [((100u32 << 16) | 1).to_be_bytes(), [0; 4], [0; 4]].concat();
let data = [vec![0; 4], pcm].concat();
let lpcm = caf(&[caf_desc(b"lpcm", 2, 1, 1, 16), (b"chan", 12, chan), (b"free", 8, vec![0; 8]), (b"data", data.len() as i64, data)]);
let (info, buf) = decode(&lpcm, None).unwrap();
assert_eq!((info.format, buf.frames()), (FileFormat::Caf, 480));
let table = packet_table(3, 3072, &[10, 0x81, 0x48, 5]);
let aac = caf(&[caf_desc(b"aac ", 0, 1024, 2, 0), (b"pakt", table.len() as i64, table), (b"data", 219, vec![0; 219])]);
assert_eq!(probe(&aac, None).unwrap().frames, 3072);
}
fn atom(kind: &[u8; 4], body: &[u8]) -> Vec<u8> {
[&(8 + body.len() as u32).to_be_bytes()[..], kind, body].concat()
}
/// A mono 16-bit, 48 kHz audio sample entry of `version`, its version fields and codec atoms.
fn sample_entry(kind: &[u8; 4], version: u8, fields: &[u8], codec: &[u8]) -> Vec<u8> {
let base = [0, 0, 0, 0, 0, 0, 0, 1, 0, version, 0, 0, 0, 0, 0, 0, 0, 1, 0, 16, 0, 0, 0, 0];
atom(kind, &[&base[..], &(48_000u32 << 16).to_be_bytes(), fields, codec].concat())
}
/// An MP4 of `frames` 2-byte samples described by `entry`; `swap` may replace any version-0 atom.
fn mp4(frames: u32, entry: &[u8], swap: impl Fn(&[u8; 4], Vec<u8>) -> Vec<u8>) -> Vec<u8> {
let full = |kind: &[u8; 4], fields: &[u32]| {
let body: Vec<u8> = [0].iter().chain(fields).flat_map(|v: &u32| v.to_be_bytes()).collect();
swap(kind, atom(kind, &body))
};
let moov = |offset: u32| {
let stsd = atom(b"stsd", &[&[0, 0, 0, 0, 0, 0, 0, 1][..], entry].concat());
let stbl =
[stsd, full(b"stts", &[1, frames, 1]), full(b"stsc", &[1, 1, frames, 1]), full(b"stsz", &[2, frames]), full(b"stco", &[1, offset])];
let minf = [full(b"smhd", &[0]), atom(b"stbl", &stbl.concat())].concat();
let mdia = [full(b"mdhd", &[0, 0, 48_000, frames, 0]), full(b"hdlr", &[0, u32::from_be_bytes(*b"soun"), 0, 0, 0, 0]), atom(b"minf", &minf)];
let trak = [full(b"tkhd", &[0; 20]), atom(b"mdia", &mdia.concat())].concat();
atom(b"moov", &[full(b"mvhd", &[[0, 0, 48_000, frames, 1 << 16].as_slice(), &[0; 18], &[2]].concat()), atom(b"trak", &trak)].concat())
};
let ftyp = atom(b"ftyp", b"M4A \0\0\0\0M4A isom");
let offset = (ftyp.len() + moov(0).len() + 8) as u32;
let pcm: Vec<u8> = (0..frames as i16).flat_map(|i| (i * 40).to_le_bytes()).collect();
[ftyp, moov(offset), atom(b"mdat", &pcm)].concat()
}
#[test]
fn mp4_sizes_beyond_the_file_are_errors_not_aborts() {
let pcm = sample_entry(b"sowt", 0, &[], &[]);
let (_, buf) = decode(&mp4(480, &pcm, |_, a| a), None).unwrap();
assert_eq!(buf.frames(), 480);
let huge = |kind: &[u8; 4], a: Vec<u8>| [&1u32.to_be_bytes()[..], kind, &(1u64 << 60).to_be_bytes(), &a[8..]].concat();
// The first field after version and flags: stts's entry count, stsz's sample size.
let first_field_max = |mut a: Vec<u8>| {
a[12..16].copy_from_slice(&u32::MAX.to_be_bytes());
a
};
let sizes: Vec<u8> = (0..480u32).flat_map(|i| if i == 479 { u32::MAX } else { 2 }.to_be_bytes()).collect();
let per_sample = atom(b"stsz", &[&[0; 8][..], &480u32.to_be_bytes(), &sizes].concat());
// A header that starts inside its container is read whole, past the container's end.
let mdia = atom(b"mdia", &1u32.to_be_bytes());
let straddling = [&b"hdlr"[..], &(1u64 << 60).to_be_bytes(), &[0; 24]].concat();
let files = [
mp4(480, &pcm, |k, a| if k == b"hdlr" { huge(k, a) } else { a }),
mp4(480, &pcm, |k, a| if k == b"stts" { first_field_max(a) } else { a }),
mp4(480, &pcm, |k, a| if k == b"stsz" { first_field_max(a) } else { a }),
mp4(480, &pcm, |k, a| if k == b"stsz" { per_sample.clone() } else { a }),
[atom(b"ftyp", b"M4A \0\0\0\0M4A isom"), atom(b"moov", &atom(b"trak", &mdia)), straddling].concat(),
];
for f in files {
assert!(matches!(decode(&f, None), Err(AudioError::Malformed(m)) if m.contains("larger than the file")));
}
}
#[test]
fn atoms_running_past_their_parent_are_walked_once() {
// Each moov holds a trak that runs to the end of the file; symphonia stops after the first.
let unit = atom(b"moov", &[&u32::MAX.to_be_bytes()[..], b"trak"].concat());
let f = [atom(b"ftyp", b"M4A \0\0\0\0M4A isom"), unit.repeat(1 << 18)].concat();
assert!(decode(&f, None).is_err());
}
#[test]
fn packets_longer_than_any_codec_uses_are_refused() {
let be = |v: &[u32]| v.iter().flat_map(|x| x.to_be_bytes()).collect::<Vec<u8>>();
let v2 = |frames: u32| [&be(&[72])[..], &48_000f64.to_be_bytes(), &be(&[1, 0x7f00_0000, 16, 4, 2, frames])].concat();
let lpcm = |frames: u32| sample_entry(b"lpcm", 2, &v2(frames), &[]);
let config = |frames: u32| [&be(&[0, frames])[..], &[0, 16, 40, 10, 14, 1, 0, 255], &be(&[0, 0, 48_000])].concat();
let alac = |frames: u32| sample_entry(b"alac", 0, &[], &atom(b"alac", &config(frames)));
let (_, buf) = decode(&mp4(480, &lpcm(1), |_, a| a), None).unwrap();
assert_eq!(buf.frames(), 480);
for entry in [lpcm(1 << 21), alac(1 << 21)] {
assert!(matches!(decode(&mp4(480, &entry, |_, a| a), None), Err(AudioError::Malformed(m)) if m.contains("declares packets")));
}
}
#[test]
fn flac_metadata_fields_beyond_their_block_are_refused() {
let with_block = |kind: u8, body: &[u8]| {
let f = small(FileFormat::Flac, BitDepth::Int16, 1);
let head = ((0x80 | u32::from(kind)) << 24 | body.len() as u32).to_be_bytes();
[&f[..4], &[f[4] & 0x7f], &f[5..42], &head, body, &f[42..]].concat()
};
let picture = |data: u32| [&[0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0][..], &[0; 16], &data.to_be_bytes(), &[1, 2, 3]].concat();
assert_eq!(decode(&with_block(6, &picture(3)), None).unwrap().1.frames(), 300);
let comment = [&0u32.to_le_bytes()[..], &1u32.to_le_bytes(), &u32::MAX.to_le_bytes()].concat();
for f in [with_block(6, &picture(0x7fff_fff0)), with_block(4, &comment)] {
assert!(matches!(decode(&f, None), Err(AudioError::Malformed(m)) if m.contains("larger than its block")));
}
}
#[test]
fn an_id3_tag_before_the_audio_is_skipped_unread() {
let frame = [&b"TIT2"[..], &0x7fff_ffffu32.to_be_bytes(), &[0, 0, 3, b'h', b'i', 0]].concat();
let tag = [&b"ID3\x03\0\0\0\0\0"[..], &[frame.len() as u8], &frame].concat();
let flac = small(FileFormat::Flac, BitDepth::Int16, 1);
assert_eq!(decode(&[tag, flac].concat(), None).unwrap().1.frames(), 300);
}
#[test]
fn wav_info_entries_beyond_the_file_are_refused() {
let alaw = |len: u32| {
let info = [&b"INFO"[..], b"INAM", &len.to_le_bytes(), b"hi"].concat();
riff(&[(b"fmt ", [fmt(6, 1, 8_000, 8), vec![0, 0]].concat()), (b"LIST", info), (b"data", vec![0xD5; 800])])
};
assert_eq!(decode(&alaw(2), None).unwrap().1.frames(), 800);
let mut f = alaw(0x7fff_fff0);
f[4..8].copy_from_slice(&u32::MAX.to_le_bytes());
let list = f.windows(4).position(|w| w == b"LIST").unwrap();
f[list + 4..list + 8].copy_from_slice(&0x7fff_fffcu32.to_le_bytes());
assert!(matches!(decode(&f, None), Err(AudioError::Malformed(m)) if m.contains("larger than the file")));
}
#[test]
fn wav_info_entries_are_found_where_symphonia_reads_them() {
let list = [&b"LIST"[..], &0x7fff_fffcu32.to_le_bytes(), b"INFO", b"INAM", &0x7fff_fff0u32.to_le_bytes(), b"hi"].concat();
let alaw = |extra: u16| (b"fmt ", [fmt(6, 1, 8_000, 8), extra.to_le_bytes().to_vec()].concat());
// A-law's extra bytes are read past its chunk; an odd list is padded twice.
let files =
[riff(&[alaw(12), (b"JUNK", [&[0; 4][..], &list].concat())]), [riff(&[alaw(0), (b"LIST", b"adtl\0".to_vec())]), vec![0], list].concat()];
for mut f in files {
f[4..8].copy_from_slice(&u32::MAX.to_le_bytes());
assert!(matches!(decode(&f, None), Err(AudioError::Malformed(m)) if m.contains("larger than the file")));
}
}
#[test]
fn many_tiny_chunks_terminate() {
let mut chunks: Vec<(&[u8; 4], Vec<u8>)> = vec![(b"fmt ", fmt(1, 1, 8_000, 16))];