Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 96 additions & 14 deletions mp4parse/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1155,6 +1155,11 @@ pub struct AudioSampleEntry {
data_reference_index: u16,
pub channelcount: u32,
pub samplesize: u16,
/// Sample rate stored in the ISOBMFF `AudioSampleEntry`.
///
/// Codec-specific metadata can define a different effective sample rate;
/// for example, high-rate FLAC uses a constrained value here and carries
/// its native rate in [`FLACSpecificBox::stream_info`].
pub samplerate: f64,
pub codec_specific: AudioCodecSpecific,
pub protection_info: TryVec<ProtectionSchemeInfoBox>,
Expand Down Expand Up @@ -1278,12 +1283,46 @@ pub struct FLACMetadataBlock {
pub data: TryVec<u8>,
}

/// Audio properties parsed from a FLAC `METADATA_BLOCK_STREAMINFO` block.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct FLACStreamInfo {
/// Native sample rate of the FLAC bitstream.
pub sample_rate: u32,
/// Number of channels in the FLAC bitstream.
pub channel_count: u8,
/// Number of bits per sample in the FLAC bitstream.
pub bits_per_sample: u8,
}

impl FLACStreamInfo {
fn parse(data: &[u8]) -> Result<Self> {
if data.len() != 34 {
return Status::DflaStreamInfoBadSize.into();
}

// FLAC format § METADATA_BLOCK_STREAMINFO packs these fields into
// bytes 10 through 13 of the fixed-size 34-byte structure.
let sample_rate =
u32::from(data[10]) << 12 | u32::from(data[11]) << 4 | u32::from(data[12] >> 4);
let channel_count = ((data[12] >> 1) & 0x07) + 1;
let bits_per_sample = (((data[12] & 0x01) << 4) | (data[13] >> 4)) + 1;

Ok(Self {
sample_rate,
channel_count,
bits_per_sample,
})
}
}

/// Represents a FLACSpecificBox 'dfLa'
#[derive(Debug)]
pub struct FLACSpecificBox {
#[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340
version: u8,
pub blocks: TryVec<FLACMetadataBlock>,
/// Parsed audio properties from the first, mandatory STREAMINFO block.
pub stream_info: FLACStreamInfo,
}

#[derive(Debug)]
Expand Down Expand Up @@ -5497,29 +5536,62 @@ fn read_esds<T: Read>(src: &mut BMFFBox<T>, strictness: ParseStrictness) -> Resu

/// Parse `FLACSpecificBox`.
/// See [Encapsulation of FLAC in ISO Base Media File Format](https://github.com/xiph/flac/blob/master/doc/isoflac.txt) § 3.3.2
fn read_dfla<T: Read>(src: &mut BMFFBox<T>) -> Result<FLACSpecificBox> {
fn read_dfla<T: Read>(
src: &mut BMFFBox<T>,
strictness: ParseStrictness,
) -> Result<FLACSpecificBox> {
let (version, flags) = read_fullbox_extra(src)?;
if version != 0 {
return Err(Error::Unsupported("unknown dfLa (FLAC) version"));
}
if flags != 0 {
return Status::DflaFlagsNonzero.into();
}
let mut blocks = TryVec::new();
while src.bytes_left() > 0 {
let block = read_flac_metadata(src)?;
blocks.push(block)?;
}
// The box must have at least one meta block, and the first block
// must be the METADATA_BLOCK_STREAMINFO
if blocks.is_empty() {

// STREAMINFO must be present and first. It is required to configure the
// decoder, so failure to parse it is fatal in every strictness mode.
if src.bytes_left() == 0 {
return Status::DflaMissingMetadata.into();
} else if blocks[0].block_type != 0 {
}
let first_block = read_flac_metadata(src)?;
if first_block.block_type != 0 {
return Status::DflaStreamInfoNotFirst.into();
} else if blocks[0].data.len() != 34 {
return Status::DflaStreamInfoBadSize.into();
}
Ok(FLACSpecificBox { version, blocks })
let stream_info = FLACStreamInfo::parse(&first_block.data)?;

let mut blocks = TryVec::new();
blocks.push(first_block)?;

while src.bytes_left() > 0 {
match read_flac_metadata(src) {
Ok(block) => blocks.push(block)?,
Err(error) => {
let recoverable = matches!(
&error,
Error::UnexpectedEOF
| Error::InvalidData(Status::DflaBadMetadataBlockSize | Status::ReadBufErr)
);
if strictness == ParseStrictness::Strict || !recoverable {
return Err(error);
}

// Do not hide physical truncation or an I/O failure. A short
// trailing metadata header contained within dfLa leaves no
// bytes here, while a file ending before dfLa's declared end
// causes this exact skip to fail.
let remaining = src.bytes_left();
skip_exact(src, remaining)?;
warn!("Ignoring malformed trailing FLAC metadata: {error}");
break;
}
}
}

Ok(FLACSpecificBox {
version,
blocks,
stream_info,
})
}

/// Parse `OpusSpecificBox`.
Expand Down Expand Up @@ -6007,7 +6079,7 @@ fn read_audio_sample_entry<T: Read>(
{
return Status::StsdBadAudioSampleEntry.into();
}
let dfla = read_dfla(&mut b)?;
let dfla = read_dfla(&mut b, strictness)?;
codec_type = CodecType::FLAC;
codec_specific = Some(AudioCodecSpecific::FLACSpecificBox(dfla));
}
Expand Down Expand Up @@ -6435,6 +6507,16 @@ fn skip<T: Read>(src: &mut T, bytes: u64) -> Result<()> {
Ok(())
}

/// Skip exactly `bytes`, returning an error if the underlying reader ends
/// before all requested bytes have been consumed.
fn skip_exact<T: Read>(src: &mut T, bytes: u64) -> Result<()> {
let skipped = std::io::copy(&mut src.take(bytes), &mut std::io::sink())?;
if skipped != bytes {
return Err(Error::UnexpectedEOF);
}
Ok(())
}

/// Read size bytes into a Vector or return error.
fn read_buf<T: Read>(src: &mut T, size: u64) -> Result<TryVec<u8>> {
let buf = src.take(size).read_into_try_vec()?;
Expand Down
156 changes: 144 additions & 12 deletions mp4parse/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,15 @@ fn flac_streaminfo() -> Vec<u8> {
]
}

fn flac_streaminfo_with_sample_rate(sample_rate: u32) -> Vec<u8> {
assert!(sample_rate < 1 << 20);
let mut stream_info = flac_streaminfo();
stream_info[10] = (sample_rate >> 12) as u8;
stream_info[11] = (sample_rate >> 4) as u8;
stream_info[12] = (stream_info[12] & 0x0f) | ((sample_rate as u8 & 0x0f) << 4);
stream_info
}

#[test]
fn read_flac() {
let mut stream = make_box(BoxSize::Auto, b"fLaC", |s| {
Expand Down Expand Up @@ -651,6 +660,45 @@ fn read_flac() {
assert!(r.is_ok());
}

#[test]
fn read_high_rate_flac_preserves_sample_entry_rate() {
let mut stream = make_box(BoxSize::Auto, b"fLaC", |s| {
s.append_repeated(0, 6) // reserved
.B16(1) // data reference index
.B32(0) // reserved
.B32(0) // reserved
.B16(2) // channel count
.B16(16) // bits per sample
.B16(0) // pre_defined
.B16(0) // reserved
.B32(48000 << 16) // Greatest expressible division of 96 kHz.
.append_bytes(
&make_dfla(
FlacBlockType::StreamInfo,
true,
&flac_streaminfo_with_sample_rate(96000),
FlacBlockLength::Correct,
)
.into_inner(),
)
});
let mut iter = super::BoxIter::new(&mut stream);
let mut stream = iter.next_box().unwrap().unwrap();
let sample_entry =
super::read_audio_sample_entry(&mut stream, ParseStrictness::Normal).unwrap();

let super::SampleEntry::Audio(audio) = sample_entry else {
panic!("expected an audio sample entry");
};
assert_eq!(audio.samplerate, 48000.0);
let super::AudioCodecSpecific::FLACSpecificBox(flac) = audio.codec_specific else {
panic!("expected FLAC codec-specific metadata");
};
assert_eq!(flac.stream_info.sample_rate, 96000);
assert_eq!(flac.stream_info.channel_count, 2);
assert_eq!(flac.stream_info.bits_per_sample, 16);
}

#[derive(Clone, Copy)]
enum FlacBlockType {
StreamInfo = 0,
Expand Down Expand Up @@ -691,6 +739,24 @@ fn make_dfla(
})
}

fn make_dfla_with_trailing_bytes(trailing: &[u8]) -> Cursor<Vec<u8>> {
make_fullbox(BoxSize::Auto, b"dfLa", 0, |s| {
s.B32(flac_streaminfo().len() as u32)
.append_bytes(&flac_streaminfo())
.append_bytes(trailing)
})
}

fn read_test_dfla(
mut stream: Cursor<Vec<u8>>,
strictness: ParseStrictness,
) -> super::Result<super::FLACSpecificBox> {
let mut iter = super::BoxIter::new(&mut stream);
let mut stream = iter.next_box()?.unwrap();
assert_eq!(stream.head.name, BoxType::FLACSpecificBox);
super::read_dfla(&mut stream, strictness)
}

#[test]
fn read_dfla() {
let mut stream = make_dfla(
Expand All @@ -702,24 +768,90 @@ fn read_dfla() {
let mut iter = super::BoxIter::new(&mut stream);
let mut stream = iter.next_box().unwrap().unwrap();
assert_eq!(stream.head.name, BoxType::FLACSpecificBox);
let dfla = super::read_dfla(&mut stream).unwrap();
let dfla = super::read_dfla(&mut stream, ParseStrictness::Normal).unwrap();
assert_eq!(dfla.version, 0);
assert_eq!(dfla.stream_info.sample_rate, 44100);
assert_eq!(dfla.stream_info.channel_count, 2);
assert_eq!(dfla.stream_info.bits_per_sample, 16);
}

#[test]
fn long_flac_metadata() {
let streaminfo = flac_streaminfo();
let mut stream = make_dfla(
FlacBlockType::StreamInfo,
true,
&streaminfo,
FlacBlockLength::Incorrect(streaminfo.len() + 4),
);
let mut iter = super::BoxIter::new(&mut stream);
let mut stream = iter.next_box().unwrap().unwrap();
assert_eq!(stream.head.name, BoxType::FLACSpecificBox);
let r = super::read_dfla(&mut stream);
assert!(r.is_err());
for strictness in [
ParseStrictness::Permissive,
ParseStrictness::Normal,
ParseStrictness::Strict,
] {
let stream = make_dfla(
FlacBlockType::StreamInfo,
true,
&streaminfo,
FlacBlockLength::Incorrect(streaminfo.len() + 4),
);
assert!(matches!(
read_test_dfla(stream, strictness),
Err(Error::InvalidData(Status::DflaBadMetadataBlockSize))
));
}
}

#[test]
fn oversized_trailing_flac_metadata_respects_strictness() {
// A padding block declares four payload bytes but contains only two.
let trailing = [0x81, 0x00, 0x00, 0x04, 0x00, 0x00];

for strictness in [ParseStrictness::Permissive, ParseStrictness::Normal] {
let dfla = read_test_dfla(make_dfla_with_trailing_bytes(&trailing), strictness).unwrap();
assert_eq!(dfla.blocks.len(), 1);
assert_eq!(dfla.stream_info.sample_rate, 44100);
}

assert!(matches!(
read_test_dfla(
make_dfla_with_trailing_bytes(&trailing),
ParseStrictness::Strict
),
Err(Error::InvalidData(Status::DflaBadMetadataBlockSize))
));
}

#[test]
fn short_trailing_flac_metadata_header_respects_strictness() {
// A second metadata block begins but its four-byte header is incomplete.
let trailing = [0x81, 0x00];

for strictness in [ParseStrictness::Permissive, ParseStrictness::Normal] {
let dfla = read_test_dfla(make_dfla_with_trailing_bytes(&trailing), strictness).unwrap();
assert_eq!(dfla.blocks.len(), 1);
assert_eq!(dfla.stream_info.sample_rate, 44100);
}

assert!(matches!(
read_test_dfla(
make_dfla_with_trailing_bytes(&trailing),
ParseStrictness::Strict
),
Err(Error::UnexpectedEOF)
));
}

#[test]
fn physically_truncated_trailing_flac_metadata_is_rejected() {
// The second block declares four payload bytes and the dfLa box claims
// they are present, but the underlying stream ends after two bytes.
let trailing = [0x81, 0x00, 0x00, 0x04, 0x00, 0x00];

for strictness in [
ParseStrictness::Permissive,
ParseStrictness::Normal,
ParseStrictness::Strict,
] {
let mut stream = make_dfla_with_trailing_bytes(&trailing);
let declared_size = u32::from_be_bytes(stream.get_ref()[0..4].try_into().unwrap()) + 2;
stream.get_mut()[0..4].copy_from_slice(&declared_size.to_be_bytes());
assert!(read_test_dfla(stream, strictness).is_err());
}
}

#[test]
Expand Down
3 changes: 3 additions & 0 deletions mp4parse/tests/public.rs
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,9 @@ fn public_api() {
assert!(!flac.blocks.is_empty());
assert_eq!(flac.blocks[0].block_type, 0);
assert_eq!(flac.blocks[0].data.len(), 34);
let _sample_rate: u32 = flac.stream_info.sample_rate;
let _channel_count: u8 = flac.stream_info.channel_count;
let _bits_per_sample: u8 = flac.stream_info.bits_per_sample;
"FLAC"
}
mp4::AudioCodecSpecific::OpusSpecificBox(ref opus) => {
Expand Down
Loading
Loading