diff --git a/mp4parse/src/lib.rs b/mp4parse/src/lib.rs index 8fb39403..7cb41924 100644 --- a/mp4parse/src/lib.rs +++ b/mp4parse/src/lib.rs @@ -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, @@ -1278,12 +1283,46 @@ pub struct FLACMetadataBlock { pub data: TryVec, } +/// 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 { + 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, + /// Parsed audio properties from the first, mandatory STREAMINFO block. + pub stream_info: FLACStreamInfo, } #[derive(Debug)] @@ -5497,7 +5536,10 @@ fn read_esds(src: &mut BMFFBox, 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(src: &mut BMFFBox) -> Result { +fn read_dfla( + src: &mut BMFFBox, + strictness: ParseStrictness, +) -> Result { let (version, flags) = read_fullbox_extra(src)?; if version != 0 { return Err(Error::Unsupported("unknown dfLa (FLAC) version")); @@ -5505,21 +5547,51 @@ fn read_dfla(src: &mut BMFFBox) -> Result { 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`. @@ -6007,7 +6079,7 @@ fn read_audio_sample_entry( { 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)); } @@ -6435,6 +6507,16 @@ fn skip(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(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(src: &mut T, size: u64) -> Result> { let buf = src.take(size).read_into_try_vec()?; diff --git a/mp4parse/src/tests.rs b/mp4parse/src/tests.rs index b144f6f5..2644e788 100644 --- a/mp4parse/src/tests.rs +++ b/mp4parse/src/tests.rs @@ -623,6 +623,15 @@ fn flac_streaminfo() -> Vec { ] } +fn flac_streaminfo_with_sample_rate(sample_rate: u32) -> Vec { + 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| { @@ -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, @@ -691,6 +739,24 @@ fn make_dfla( }) } +fn make_dfla_with_trailing_bytes(trailing: &[u8]) -> Cursor> { + 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>, + strictness: ParseStrictness, +) -> super::Result { + 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( @@ -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] diff --git a/mp4parse/tests/public.rs b/mp4parse/tests/public.rs index 82f858a6..c273171e 100644 --- a/mp4parse/tests/public.rs +++ b/mp4parse/tests/public.rs @@ -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) => { diff --git a/mp4parse_capi/src/lib.rs b/mp4parse_capi/src/lib.rs index af104a31..6d8650ea 100644 --- a/mp4parse_capi/src/lib.rs +++ b/mp4parse_capi/src/lib.rs @@ -232,6 +232,8 @@ pub struct Mp4parseTrackAudioSampleInfo { pub codec_type: Mp4parseCodec, pub channels: u16, pub bit_depth: u16, + /// Effective sample rate after applying codec-specific configuration. + /// This may differ from the raw ISOBMFF `AudioSampleEntry` value. pub sample_rate: u32, pub profile: u16, pub extended_profile: u16, @@ -797,6 +799,17 @@ pub unsafe extern "C" fn mp4parse_get_track_audio_info( get_track_audio_info(&mut *parser, track_index, &mut *info).into() } +fn apply_flac_stream_info( + sample_info: &mut Mp4parseTrackAudioSampleInfo, + stream_info: &mp4parse::FLACStreamInfo, +) { + // The FLAC-in-ISOBMFF mapping requires readers to use the native sample + // rate from STREAMINFO. The AudioSampleEntry field is only a constrained + // 16.16 representation and may contain a regular division for rates above + // 65535 Hz. + sample_info.sample_rate = stream_info.sample_rate; +} + fn get_track_audio_info( parser: &mut Mp4parseParser, track_index: u32, @@ -910,6 +923,7 @@ fn get_track_audio_info( return Err(Mp4parseStatus::Invalid); } sample_info.codec_specific_config.set_data(&streaminfo.data); + apply_flac_stream_info(&mut sample_info, &flac.stream_info); } AudioCodecSpecific::OpusSpecificBox(ref opus) => { let mut v = TryVec::new(); @@ -1981,6 +1995,27 @@ fn minimal_mp4_get_track_audio_info() { } } +#[test] +fn flac_streaminfo_sample_rate_overrides_sample_entry_rate() { + let mut sample_info = Mp4parseTrackAudioSampleInfo { + channels: 2, + bit_depth: 24, + sample_rate: 48000, + ..Default::default() + }; + let stream_info = mp4parse::FLACStreamInfo { + sample_rate: 96000, + channel_count: 2, + bits_per_sample: 24, + }; + + apply_flac_stream_info(&mut sample_info, &stream_info); + + assert_eq!(sample_info.sample_rate, 96000); + assert_eq!(sample_info.channels, 2); + assert_eq!(sample_info.bit_depth, 24); +} + #[test] fn minimal_mp4_get_track_info_invalid_track_number() { let parser = parse_minimal_mp4();