MP4: Better handle invalid atom sizes

This commit is contained in:
Serial 2022-07-11 23:01:17 -04:00
parent d298199a07
commit 542f7dabc7
No known key found for this signature in database
GPG key ID: DA95198DC17C4568
12 changed files with 367 additions and 183 deletions

View file

@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- **PictureInformation**: Fix potential overflow on an invalid picture
- **MP4**: The parser has received a major facelift, and shouldn't be so eager to allocate or trust user data (Fixes OOM)
## [0.7.1] - 2022-07-08

View file

@ -47,7 +47,7 @@ macro_rules! tag_methods {
macro_rules! try_vec {
($elem:expr; $size:expr) => {{
let mut v = Vec::new();
v.try_reserve($size)?;
v.try_reserve_exact($size)?;
v.resize($size, $elem);
v

View file

@ -39,25 +39,18 @@ pub(crate) struct AtomInfo {
}
impl AtomInfo {
pub(crate) fn read<R>(data: &mut R) -> Result<Self>
pub(crate) fn read<R>(data: &mut R, mut reader_size: u64) -> Result<Self>
where
R: Read + Seek,
{
let start = data.stream_position()?;
let len = data.read_u32::<BigEndian>()?;
let len_raw = u64::from(data.read_u32::<BigEndian>()?);
let mut ident = [0; 4];
data.read_exact(&mut ident)?;
let mut atom_ident = AtomIdent::Fourcc(ident);
// Encountered a freeform identifier
if &ident == b"----" {
atom_ident = parse_freeform(data)?;
}
let (len, extended) = match len {
let (len, extended) = match len_raw {
// The atom extends to the end of the file
0 => {
let pos = data.stream_position()?;
@ -69,7 +62,7 @@ impl AtomInfo {
},
// There's an extended length
1 => (data.read_u64::<BigEndian>()?, true),
_ => (u64::from(len), false),
_ => (len_raw, false),
};
if len < 8 {
@ -81,6 +74,26 @@ impl AtomInfo {
)));
}
// `len` includes itself
if (len - 4) > reader_size {
data.seek(SeekFrom::Current(-4))?;
return Err(LoftyError::new(ErrorKind::TooMuchData));
}
let mut atom_ident = AtomIdent::Fourcc(ident);
// Encountered a freeform identifier
if &ident == b"----" {
reader_size -= 8;
if reader_size < 8 {
return Err(LoftyError::new(ErrorKind::BadAtom(
"Found an incomplete freeform identifier",
)));
}
atom_ident = parse_freeform(data, reader_size)?;
}
Ok(Self {
start,
len,
@ -90,21 +103,21 @@ impl AtomInfo {
}
}
fn parse_freeform<R>(data: &mut R) -> Result<AtomIdent>
fn parse_freeform<R>(data: &mut R, reader_size: u64) -> Result<AtomIdent>
where
R: Read + Seek,
{
let mean = freeform_chunk(data, b"mean")?;
let name = freeform_chunk(data, b"name")?;
let mean = freeform_chunk(data, b"mean", reader_size - 4)?;
let name = freeform_chunk(data, b"name", reader_size - 8)?;
Ok(AtomIdent::Freeform { mean, name })
}
fn freeform_chunk<R>(data: &mut R, name: &[u8]) -> Result<String>
fn freeform_chunk<R>(data: &mut R, name: &[u8], reader_size: u64) -> Result<String>
where
R: Read + Seek,
{
let atom = AtomInfo::read(data)?;
let atom = AtomInfo::read(data, reader_size)?;
match atom.ident {
AtomIdent::Fourcc(ref fourcc) if fourcc == name => {

View file

@ -519,14 +519,21 @@ fn item_key_to_ident(key: &ItemKey) -> Option<AtomIdentRef<'_>> {
#[cfg(test)]
mod tests {
use crate::mp4::ilst::atom::AtomDataStorage;
use crate::mp4::read::AtomReader;
use crate::mp4::{AdvisoryRating, Atom, AtomData, AtomIdent, Ilst, Mp4File};
use crate::tag::utils::test_utils;
use crate::tag::utils::test_utils::read_path;
use crate::{Accessor, AudioFile, ItemKey, Tag, TagExt, TagType};
use std::io::{Cursor, Read, Seek, Write};
fn read_ilst(path: &str) -> Ilst {
let tag = crate::tag::utils::test_utils::read_path(path);
super::read::parse_ilst(&mut &tag[..], tag.len() as u64).unwrap()
let len = tag.len();
let cursor = Cursor::new(tag);
let mut reader = AtomReader::new(cursor).unwrap();
super::read::parse_ilst(&mut reader, len as u64).unwrap()
}
fn verify_atom(ilst: &Ilst, ident: [u8; 4], data: &AtomData) {
@ -589,8 +596,12 @@ mod tests {
));
let tag = crate::tag::utils::test_utils::read_path("tests/tags/assets/ilst/test.ilst");
let len = tag.len();
let parsed_tag = super::read::parse_ilst(&mut &tag[..], tag.len() as u64).unwrap();
let cursor = Cursor::new(tag);
let mut reader = AtomReader::new(cursor).unwrap();
let parsed_tag = super::read::parse_ilst(&mut reader, len as u64).unwrap();
assert_eq!(expected_tag, parsed_tag);
}
@ -602,19 +613,25 @@ mod tests {
let mut writer = Vec::new();
parsed_tag.dump_to(&mut writer).unwrap();
let cursor = Cursor::new(&writer[8..]);
let mut reader = AtomReader::new(cursor).unwrap();
// Remove the ilst identifier and size
let temp_parsed_tag =
super::read::parse_ilst(&mut &writer[8..], (writer.len() - 8) as u64).unwrap();
super::read::parse_ilst(&mut reader, (writer.len() - 8) as u64).unwrap();
assert_eq!(parsed_tag, temp_parsed_tag);
}
#[test]
fn ilst_to_tag() {
let tag_bytes =
crate::tag::utils::test_utils::read_path("tests/tags/assets/ilst/test.ilst");
let tag = crate::tag::utils::test_utils::read_path("tests/tags/assets/ilst/test.ilst");
let len = tag.len();
let ilst = super::read::parse_ilst(&mut &tag_bytes[..], tag_bytes.len() as u64).unwrap();
let cursor = Cursor::new(tag);
let mut reader = AtomReader::new(cursor).unwrap();
let ilst = super::read::parse_ilst(&mut reader, len as u64).unwrap();
let tag: Tag = ilst.into();
@ -717,13 +734,20 @@ mod tests {
let file_bytes = read_path("tests/files/assets/ilst_trailing_padding.m4a");
assert!(Mp4File::read_from(&mut Cursor::new(&file_bytes), false).is_ok());
let ilst_bytes = &file_bytes[ILST_START..ILST_END];
let mut ilst;
let old_free_size;
{
let ilst_bytes = &file_bytes[ILST_START..ILST_END];
let old_free_size =
u32::from_be_bytes(file_bytes[ILST_END..ILST_END + 4].try_into().unwrap());
assert_eq!(old_free_size, PADDING_SIZE as u32);
old_free_size =
u32::from_be_bytes(file_bytes[ILST_END..ILST_END + 4].try_into().unwrap());
assert_eq!(old_free_size, PADDING_SIZE as u32);
let mut ilst = super::read::parse_ilst(&mut &*ilst_bytes, ilst_bytes.len() as u64).unwrap();
let cursor = Cursor::new(ilst_bytes);
let mut reader = AtomReader::new(cursor).unwrap();
ilst = super::read::parse_ilst(&mut reader, ilst_bytes.len() as u64).unwrap();
}
let mut file = tempfile::tempfile().unwrap();
file.write_all(&file_bytes).unwrap();
@ -781,7 +805,7 @@ mod tests {
data: AtomDataStorage::Single(AtomData::UTF8(String::from("Foo artist"))),
});
assert!(tag.save_to(&mut file).is_ok());
tag.save_to(&mut file).unwrap();
file.rewind().unwrap();
let mp4_file = Mp4File::read_from(&mut file, true).unwrap();
@ -814,4 +838,15 @@ mod tests {
&AtomData::UTF8(String::from("Classical")),
);
}
#[test]
fn zero_sized_ilst() {
let file = Mp4File::read_from(
&mut Cursor::new(test_utils::read_path("tests/files/assets/zero/zero.ilst")),
false,
)
.unwrap();
assert_eq!(file.ilst, Some(Ilst::default()));
}
}

View file

@ -8,40 +8,40 @@ use crate::id3::v1::constants::GENRES;
use crate::id3::v2::util::text_utils::utf16_decode;
use crate::macros::try_vec;
use crate::mp4::atom_info::AtomInfo;
use crate::mp4::read::skip_unneeded;
use crate::mp4::ilst::atom::AtomDataStorage;
use crate::mp4::read::{skip_unneeded, AtomReader};
use crate::picture::{MimeType, Picture, PictureType};
use std::borrow::Cow;
use std::io::{Cursor, Read, Seek, SeekFrom};
use crate::mp4::ilst::atom::AtomDataStorage;
use byteorder::ReadBytesExt;
pub(in crate::mp4) fn parse_ilst<R>(reader: &mut R, len: u64) -> Result<Ilst>
pub(in crate::mp4) fn parse_ilst<R>(reader: &mut AtomReader<R>, len: u64) -> Result<Ilst>
where
R: Read,
R: Read + Seek,
{
let mut contents = try_vec![0; len as usize];
reader.read_exact(&mut contents)?;
let mut cursor = Cursor::new(contents);
let mut ilst_reader = AtomReader::new(&mut cursor)?;
let mut tag = Ilst::default();
while let Ok(atom) = AtomInfo::read(&mut cursor) {
while let Ok(atom) = ilst_reader.next() {
if let AtomIdent::Fourcc(ref fourcc) = atom.ident {
match fourcc {
b"free" | b"skip" => {
skip_unneeded(&mut cursor, atom.extended, atom.len)?;
skip_unneeded(&mut ilst_reader, atom.extended, atom.len)?;
continue;
},
b"covr" => {
handle_covr(&mut cursor, &mut tag, &atom)?;
handle_covr(&mut ilst_reader, &mut tag, &atom)?;
continue;
},
// Upgrade this to a \xa9gen atom
b"gnre" => {
if let Some(atom_data) = parse_data_inner(&mut cursor, &atom)? {
if let Some(atom_data) = parse_data_inner(&mut ilst_reader, &atom)? {
let mut data = Vec::new();
for (flags, content) in atom_data {
@ -64,7 +64,7 @@ where
// Special case the "Album ID", as it has the code "BE signed integer" (21), but
// must be interpreted as a "BE 64-bit Signed Integer" (74)
b"plID" => {
if let Some(atom_data) = parse_data_inner(&mut cursor, &atom)? {
if let Some(atom_data) = parse_data_inner(&mut ilst_reader, &atom)? {
let mut data = Vec::new();
for (code, content) in atom_data {
@ -90,17 +90,17 @@ where
}
}
parse_data(&mut cursor, &mut tag, atom)?;
parse_data(&mut ilst_reader, &mut tag, atom)?;
}
Ok(tag)
}
fn parse_data<R>(data: &mut R, tag: &mut Ilst, atom_info: AtomInfo) -> Result<()>
fn parse_data<R>(reader: &mut AtomReader<R>, tag: &mut Ilst, atom_info: AtomInfo) -> Result<()>
where
R: Read + Seek,
{
if let Some(mut atom_data) = parse_data_inner(data, &atom_info)? {
if let Some(mut atom_data) = parse_data_inner(reader, &atom_info)? {
// Most atoms we encounter are only going to have 1 value, so store them as such
if atom_data.len() == 1 {
let (flags, content) = atom_data.remove(0);
@ -129,17 +129,20 @@ where
Ok(())
}
fn parse_data_inner<R>(data: &mut R, atom_info: &AtomInfo) -> Result<Option<Vec<(u32, Vec<u8>)>>>
fn parse_data_inner<R>(
reader: &mut AtomReader<R>,
atom_info: &AtomInfo,
) -> Result<Option<Vec<(u32, Vec<u8>)>>>
where
R: Read + Seek,
{
// An atom can contain multiple data atoms
let mut ret = Vec::new();
let to_read = (atom_info.start + atom_info.len) - data.stream_position()?;
let to_read = (atom_info.start + atom_info.len) - reader.position()?;
let mut pos = 0;
while pos < to_read {
let data_atom = AtomInfo::read(data)?;
let data_atom = reader.next()?;
match data_atom.ident {
AtomIdent::Fourcc(ref name) if name == b"data" => {},
_ => {
@ -150,15 +153,15 @@ where
}
// We don't care about the version
let _version = data.read_u8()?;
let _version = reader.read_u8()?;
let mut flags = [0; 3];
data.read_exact(&mut flags)?;
reader.read_exact(&mut flags)?;
let flags = u32::from_be_bytes([0, flags[0], flags[1], flags[2]]);
// We don't care about the locale
data.seek(SeekFrom::Current(4))?;
reader.seek(SeekFrom::Current(4))?;
let content_len = (data_atom.len - 16) as usize;
if content_len == 0 {
@ -167,7 +170,7 @@ where
}
let mut content = try_vec![0; content_len];
data.read_exact(&mut content)?;
reader.read_exact(&mut content)?;
pos += data_atom.len;
ret.push((flags, content));
@ -205,7 +208,10 @@ fn parse_int(bytes: &[u8]) -> Result<i32> {
})
}
fn handle_covr(reader: &mut Cursor<Vec<u8>>, tag: &mut Ilst, atom_info: &AtomInfo) -> Result<()> {
fn handle_covr<R>(reader: &mut AtomReader<R>, tag: &mut Ilst, atom_info: &AtomInfo) -> Result<()>
where
R: Read + Seek,
{
if let Some(atom_data) = parse_data_inner(reader, atom_info)? {
let mut data = Vec::new();

View file

@ -5,7 +5,7 @@ use crate::macros::try_vec;
use crate::mp4::atom_info::{AtomIdent, AtomInfo};
use crate::mp4::ilst::r#ref::{AtomIdentRef, AtomRef};
use crate::mp4::moov::Moov;
use crate::mp4::read::{atom_tree, meta_is_full, nested_atom, verify_mp4};
use crate::mp4::read::{atom_tree, meta_is_full, nested_atom, verify_mp4, AtomReader};
use crate::mp4::AtomData;
use crate::picture::{MimeType, Picture};
@ -18,15 +18,17 @@ pub(crate) fn write_to<'a, I: 'a>(data: &mut File, tag: &mut IlstRef<'a, I>) ->
where
I: IntoIterator<Item = &'a AtomData>,
{
verify_mp4(data)?;
let mut reader = AtomReader::new(data)?;
let moov = Moov::find(data)?;
let pos = data.stream_position()?;
verify_mp4(&mut reader)?;
data.rewind()?;
let moov = Moov::find(&mut reader)?;
let pos = reader.position()?;
reader.rewind()?;
let mut file_bytes = Vec::new();
data.read_to_end(&mut file_bytes)?;
reader.read_to_end(&mut file_bytes)?;
let mut cursor = Cursor::new(file_bytes);
cursor.seek(SeekFrom::Start(pos))?;
@ -125,6 +127,8 @@ where
&mut cursor,
)?;
let data = reader.into_inner();
data.rewind()?;
data.set_len(0)?;
data.write_all(&cursor.into_inner())?;
@ -142,7 +146,7 @@ fn save_to_existing(
let replacement;
let range;
let (ilst_idx, tree) = atom_tree(cursor, meta.len - 4, b"ilst")?;
let (ilst_idx, tree) = atom_tree(cursor, meta.len - 8, b"ilst")?;
if tree.is_empty() {
// Nothing to do

View file

@ -4,7 +4,7 @@ use super::trak::Trak;
#[cfg(feature = "mp4_ilst")]
use super::{
ilst::{read::parse_ilst, Ilst},
read::meta_is_full,
read::{meta_is_full, AtomReader},
};
use crate::error::{FileDecodingError, Result};
use crate::file::FileType;
@ -19,19 +19,19 @@ pub(crate) struct Moov {
}
impl Moov {
pub(crate) fn find<R>(data: &mut R) -> Result<AtomInfo>
pub(super) fn find<R>(reader: &mut AtomReader<R>) -> Result<AtomInfo>
where
R: Read + Seek,
{
let mut moov = None;
while let Ok(atom) = AtomInfo::read(data) {
while let Ok(atom) = reader.next() {
if atom.ident == AtomIdent::Fourcc(*b"moov") {
moov = Some(atom);
break;
}
skip_unneeded(data, atom.extended, atom.len)?;
skip_unneeded(reader, atom.extended, atom.len)?;
}
if let Some(moov) = moov {
@ -41,7 +41,7 @@ impl Moov {
}
}
pub(crate) fn parse<R>(data: &mut R, read_properties: bool) -> Result<Self>
pub(super) fn parse<R>(reader: &mut AtomReader<R>, read_properties: bool) -> Result<Self>
where
R: Read + Seek,
{
@ -49,21 +49,21 @@ impl Moov {
#[cfg(feature = "mp4_ilst")]
let mut meta = None;
while let Ok(atom) = AtomInfo::read(data) {
while let Ok(atom) = reader.next() {
if let AtomIdent::Fourcc(fourcc) = atom.ident {
match &fourcc {
b"trak" if read_properties => traks.push(Trak::parse(data, &atom)?),
b"trak" if read_properties => traks.push(Trak::parse(reader, &atom)?),
#[cfg(feature = "mp4_ilst")]
b"udta" => {
meta = meta_from_udta(data, atom.len - 8)?;
meta = meta_from_udta(reader, atom.len - 8)?;
},
_ => skip_unneeded(data, atom.extended, atom.len)?,
_ => skip_unneeded(reader, atom.extended, atom.len)?,
}
continue;
}
skip_unneeded(data, atom.extended, atom.len)?
skip_unneeded(reader, atom.extended, atom.len)?
}
Ok(Self {
@ -75,7 +75,7 @@ impl Moov {
}
#[cfg(feature = "mp4_ilst")]
fn meta_from_udta<R>(data: &mut R, len: u64) -> Result<Option<Ilst>>
fn meta_from_udta<R>(reader: &mut AtomReader<R>, len: u64) -> Result<Option<Ilst>>
where
R: Read + Seek,
{
@ -83,7 +83,7 @@ where
let mut meta = (false, 0_u64);
while read < len {
let atom = AtomInfo::read(data)?;
let atom = reader.next()?;
if atom.ident == AtomIdent::Fourcc(*b"meta") {
meta = (true, atom.len);
@ -91,7 +91,7 @@ where
}
read += atom.len;
skip_unneeded(data, atom.extended, atom.len)?;
skip_unneeded(reader, atom.extended, atom.len)?;
}
if !meta.0 {
@ -100,7 +100,7 @@ where
// It's possible for the `meta` atom to be non-full,
// so we have to check for that case
let full_meta_atom = meta_is_full(data)?;
let full_meta_atom = meta_is_full(reader)?;
if full_meta_atom {
read = 12;
@ -111,7 +111,7 @@ where
let mut islt = (false, 0_u64);
while read < meta.1 {
let atom = AtomInfo::read(data)?;
let atom = reader.next()?;
if atom.ident == AtomIdent::Fourcc(*b"ilst") {
islt = (true, atom.len);
@ -119,11 +119,11 @@ where
}
read += atom.len;
skip_unneeded(data, atom.extended, atom.len)?;
skip_unneeded(reader, atom.extended, atom.len)?;
}
if islt.0 {
return parse_ilst(data, islt.1 - 8).map(Some);
return parse_ilst(reader, islt.1 - 8).map(Some);
}
Ok(None)

View file

@ -1,5 +1,5 @@
use super::atom_info::{AtomIdent, AtomInfo};
use super::read::{nested_atom, skip_unneeded};
use super::read::{nested_atom, skip_unneeded, AtomReader};
use super::trak::Trak;
use crate::error::{ErrorKind, FileDecodingError, LoftyError, Result};
use crate::file::FileType;
@ -217,8 +217,8 @@ impl Mp4Properties {
}
}
pub(crate) fn read_properties<R>(
data: &mut R,
pub(super) fn read_properties<R>(
reader: &mut AtomReader<R>,
traks: &[Trak],
file_length: u64,
) -> Result<Mp4Properties>
@ -236,42 +236,42 @@ where
break;
}
data.seek(SeekFrom::Start(mdia.start + 8))?;
reader.seek(SeekFrom::Start(mdia.start + 8))?;
let mut read = 8;
while read < mdia.len {
let atom = AtomInfo::read(data)?;
let atom = reader.next()?;
read += atom.len;
if let AtomIdent::Fourcc(fourcc) = atom.ident {
match &fourcc {
b"mdhd" => {
skip_unneeded(data, atom.extended, atom.len)?;
skip_unneeded(reader, atom.extended, atom.len)?;
mdhd = Some(atom)
},
b"hdlr" => {
// The hdlr atom is followed by 8 zeros
data.seek(SeekFrom::Current(8))?;
reader.seek(SeekFrom::Current(8))?;
let mut handler_type = [0; 4];
data.read_exact(&mut handler_type)?;
reader.read_exact(&mut handler_type)?;
if &handler_type == b"soun" {
audio_track = true
}
skip_unneeded(data, atom.extended, atom.len - 12)?;
skip_unneeded(reader, atom.extended, atom.len - 12)?;
},
b"minf" => minf = Some(atom),
_ => {
skip_unneeded(data, atom.extended, atom.len)?;
skip_unneeded(reader, atom.extended, atom.len)?;
},
}
continue;
}
skip_unneeded(data, atom.extended, atom.len)?;
skip_unneeded(reader, atom.extended, atom.len)?;
}
}
@ -288,26 +288,26 @@ where
},
};
data.seek(SeekFrom::Start(mdhd.start + 8))?;
reader.seek(SeekFrom::Start(mdhd.start + 8))?;
let version = data.read_u8()?;
let _flags = data.read_uint::<BigEndian>(3)?;
let version = reader.read_u8()?;
let _flags = reader.read_uint(3)?;
let (timescale, duration) = if version == 1 {
// We don't care about these two values
let _creation_time = data.read_u64::<BigEndian>()?;
let _modification_time = data.read_u64::<BigEndian>()?;
let _creation_time = reader.read_u64()?;
let _modification_time = reader.read_u64()?;
let timescale = data.read_u32::<BigEndian>()?;
let duration = data.read_u64::<BigEndian>()?;
let timescale = reader.read_u32()?;
let duration = reader.read_u64()?;
(timescale, duration)
} else {
let _creation_time = data.read_u32::<BigEndian>()?;
let _modification_time = data.read_u32::<BigEndian>()?;
let _creation_time = reader.read_u32()?;
let _modification_time = reader.read_u32()?;
let timescale = data.read_u32::<BigEndian>()?;
let duration = data.read_u32::<BigEndian>()?;
let timescale = reader.read_u32()?;
let duration = reader.read_u32()?;
(timescale, u64::from(duration))
};
@ -321,14 +321,16 @@ where
};
if let Some(minf) = minf {
data.seek(SeekFrom::Start(minf.start + 8))?;
reader.seek(SeekFrom::Start(minf.start + 8))?;
if let Some(stbl) = nested_atom(data, minf.len, b"stbl")? {
if let Some(stsd) = nested_atom(data, stbl.len, b"stsd")? {
if let Some(stbl) = nested_atom(reader, minf.len, b"stbl")? {
if let Some(stsd) = nested_atom(reader, stbl.len, b"stsd")? {
let mut stsd = try_vec![0; (stsd.len - 8) as usize];
data.read_exact(&mut stsd)?;
reader.read_exact(&mut stsd)?;
let mut stsd_reader = Cursor::new(&*stsd);
let mut cursor = Cursor::new(&*stsd);
let mut stsd_reader = AtomReader::new(&mut cursor)?;
// Skipping 8 bytes
// Version (1)
@ -336,7 +338,7 @@ where
// Number of entries (4)
stsd_reader.seek(SeekFrom::Current(8))?;
let atom = AtomInfo::read(&mut stsd_reader)?;
let atom = AtomInfo::read(&mut stsd_reader, stsd.len() as u64)?;
if let AtomIdent::Fourcc(ref fourcc) = atom.ident {
match fourcc {
@ -357,7 +359,7 @@ where
if properties.audio_bitrate == 0 {
properties.audio_bitrate =
(u128::from(mdat_length(data)? * 8) / duration_millis) as u32;
(u128::from(mdat_length(reader)? * 8) / duration_millis) as u32;
}
}
}
@ -368,7 +370,7 @@ where
Ok(properties)
}
fn mp4a_properties<R>(stsd: &mut R, properties: &mut Mp4Properties) -> Result<()>
fn mp4a_properties<R>(stsd: &mut AtomReader<R>, properties: &mut Mp4Properties) -> Result<()>
where
R: Read + Seek,
{
@ -393,23 +395,23 @@ where
// Vendor (4)
stsd.seek(SeekFrom::Current(16))?;
properties.channels = stsd.read_u16::<BigEndian>()? as u8;
properties.channels = stsd.read_u16()? as u8;
// Skipping 4 bytes
// Sample size (2)
// Compression ID (2)
stsd.seek(SeekFrom::Current(4))?;
properties.sample_rate = stsd.read_u32::<BigEndian>()?;
properties.sample_rate = stsd.read_u32()?;
stsd.seek(SeekFrom::Current(2))?;
// This information is often followed by an esds (elementary stream descriptor) atom containing the bitrate
if let Ok(esds) = AtomInfo::read(stsd) {
if let Ok(esds) = stsd.next() {
// There are 4 bytes we expect to be zeroed out
// Version (1)
// Flags (3)
if esds.ident == AtomIdent::Fourcc(*b"esds") && stsd.read_u32::<BigEndian>()? == 0 {
if esds.ident == AtomIdent::Fourcc(*b"esds") && stsd.read_u32()? == 0 {
let descriptor = Descriptor::read(stsd)?;
if descriptor.tag == ELEMENTARY_DESCRIPTOR_TAG {
// Skipping 3 bytes
@ -434,7 +436,7 @@ where
// Max bitrate (4)
stsd.seek(SeekFrom::Current(8))?;
let average_bitrate = stsd.read_u32::<BigEndian>()?;
let average_bitrate = stsd.read_u32()?;
// Yet another descriptor to check
let descriptor = Descriptor::read(stsd)?;
@ -513,11 +515,11 @@ where
stsd.read_exact(&mut ident)?;
if &ident == b"\0ALS\0" {
properties.sample_rate = stsd.read_u32::<BigEndian>()?;
properties.sample_rate = stsd.read_u32()?;
// Sample count
stsd.seek(SeekFrom::Current(4))?;
properties.channels = stsd.read_u16::<BigEndian>()? as u8 + 1;
properties.channels = stsd.read_u16()? as u8 + 1;
}
}
}
@ -533,7 +535,7 @@ where
Ok(())
}
fn alac_properties<R>(stsd: &mut R, properties: &mut Mp4Properties) -> Result<()>
fn alac_properties<R>(stsd: &mut AtomReader<R>, properties: &mut Mp4Properties) -> Result<()>
where
R: Read + Seek,
{
@ -551,7 +553,7 @@ where
// First alac atom's content (28)
stsd.seek(SeekFrom::Start(44))?;
if let Ok(alac) = AtomInfo::read(stsd) {
if let Ok(alac) = stsd.next() {
if alac.ident == AtomIdent::Fourcc(*b"alac") {
properties.codec = Mp4Codec::ALAC;
@ -578,15 +580,15 @@ where
// Max frame size (4)
stsd.seek(SeekFrom::Current(6))?;
properties.audio_bitrate = stsd.read_u32::<BigEndian>()? / 1000;
properties.sample_rate = stsd.read_u32::<BigEndian>()?;
properties.audio_bitrate = stsd.read_u32()? / 1000;
properties.sample_rate = stsd.read_u32()?;
}
}
Ok(())
}
fn flac_properties<R>(stsd: &mut R, properties: &mut Mp4Properties) -> Result<()>
fn flac_properties<R>(stsd: &mut AtomReader<R>, properties: &mut Mp4Properties) -> Result<()>
where
R: Read + Seek,
{
@ -601,8 +603,8 @@ where
// Vendor (4)
stsd.seek(SeekFrom::Current(16))?;
properties.channels = stsd.read_u16::<BigEndian>()? as u8;
properties.bit_depth = Some(stsd.read_u16::<BigEndian>()? as u8);
properties.channels = stsd.read_u16()? as u8;
properties.bit_depth = Some(stsd.read_u16()? as u8);
// Skipping 4 bytes
//
@ -610,11 +612,11 @@ where
// Packet size (2)
stsd.seek(SeekFrom::Current(4))?;
properties.sample_rate = u32::from(stsd.read_u16::<BigEndian>()?);
properties.sample_rate = u32::from(stsd.read_u16()?);
let _reserved = stsd.read_u16::<BigEndian>()?;
let _reserved = stsd.read_u16()?;
let dfla_atom = AtomInfo::read(stsd)?;
let dfla_atom = stsd.next()?;
match dfla_atom.ident {
// There should be a dfla atom, but it's not worth erroring if absent.
AtomIdent::Fourcc(ref fourcc) if fourcc == b"dfla" => {},
@ -645,18 +647,18 @@ where
}
// Used to calculate the bitrate, when it isn't readily available to us
fn mdat_length<R>(data: &mut R) -> Result<u64>
fn mdat_length<R>(reader: &mut AtomReader<R>) -> Result<u64>
where
R: Read + Seek,
{
data.seek(SeekFrom::Start(0))?;
reader.seek(SeekFrom::Start(0))?;
while let Ok(atom) = AtomInfo::read(data) {
while let Ok(atom) = reader.next() {
if atom.ident == AtomIdent::Fourcc(*b"mdat") {
return Ok(atom.len);
}
skip_unneeded(data, atom.extended, atom.len)?;
skip_unneeded(reader, atom.extended, atom.len)?;
}
Err(FileDecodingError::new(FileType::MP4, "Failed to find \"mdat\" atom").into())

View file

@ -7,14 +7,126 @@ use crate::file::FileType;
use std::io::{Read, Seek, SeekFrom};
#[cfg(feature = "mp4_ilst")]
use byteorder::{BigEndian, ReadBytesExt};
pub(in crate::mp4) fn verify_mp4<R>(data: &mut R) -> Result<String>
pub(super) struct AtomReader<R>
where
R: Read + Seek,
{
let atom = AtomInfo::read(data)?;
reader: R,
remaining_size: u64,
len: u64,
}
impl<R> AtomReader<R>
where
R: Read + Seek,
{
pub(super) fn new(mut reader: R) -> Result<Self> {
use crate::traits::SeekStreamLen;
#[allow(unstable_name_collisions)]
let len = reader.stream_len()?;
Ok(Self {
reader,
remaining_size: len,
len,
})
}
pub(super) fn read_u8(&mut self) -> std::io::Result<u8> {
self.remaining_size = self.remaining_size.saturating_sub(1);
self.reader.read_u8()
}
pub(super) fn read_u16(&mut self) -> std::io::Result<u16> {
self.remaining_size = self.remaining_size.saturating_sub(2);
self.reader.read_u16::<BigEndian>()
}
pub(super) fn read_u32(&mut self) -> std::io::Result<u32> {
self.remaining_size = self.remaining_size.saturating_sub(4);
self.reader.read_u32::<BigEndian>()
}
pub(super) fn read_u64(&mut self) -> std::io::Result<u64> {
self.remaining_size = self.remaining_size.saturating_sub(8);
self.reader.read_u64::<BigEndian>()
}
pub(super) fn read_uint(&mut self, size: usize) -> std::io::Result<u64> {
self.remaining_size = self.remaining_size.saturating_sub(size as u64);
self.reader.read_uint::<BigEndian>(size)
}
pub(super) fn next(&mut self) -> Result<AtomInfo> {
if self.remaining_size < 8 {
return Err(LoftyError::new(ErrorKind::TooMuchData));
}
AtomInfo::read(self, self.remaining_size)
}
pub(super) fn position(&mut self) -> std::io::Result<u64> {
self.reader.stream_position()
}
pub(super) fn into_inner(self) -> R {
self.reader
}
}
impl<R> Seek for AtomReader<R>
where
R: Read + Seek,
{
fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
match pos {
SeekFrom::Start(_) | SeekFrom::End(_) => {
let ret = self.reader.seek(pos)?;
let new_rem = self.len.saturating_sub(ret);
self.remaining_size = new_rem;
Ok(ret)
},
SeekFrom::Current(s) => {
if s.is_negative() {
self.remaining_size = self.remaining_size.saturating_add(s.unsigned_abs());
} else {
self.remaining_size = self.remaining_size.saturating_sub(s as u64);
}
self.reader.seek(pos)
},
}
}
fn stream_position(&mut self) -> std::io::Result<u64> {
self.position()
}
}
impl<R> Read for AtomReader<R>
where
R: Read + Seek,
{
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
if self.remaining_size == 0 {
return Ok(0);
}
let r = self.reader.read(buf)?;
self.remaining_size = self.remaining_size.saturating_sub(r as u64);
Ok(r)
}
}
pub(in crate::mp4) fn verify_mp4<R>(reader: &mut AtomReader<R>) -> Result<String>
where
R: Read + Seek,
{
let atom = reader.next()?;
if atom.ident != AtomIdent::Fourcc(*b"ftyp") {
return Err(LoftyError::new(ErrorKind::UnknownFormat));
@ -27,9 +139,9 @@ where
}
let mut major_brand = vec![0; 4];
data.read_exact(&mut major_brand)?;
reader.read_exact(&mut major_brand)?;
data.seek(SeekFrom::Current((atom.len - 12) as i64))?;
reader.seek(SeekFrom::Current((atom.len - 12) as i64))?;
String::from_utf8(major_brand)
.map_err(|_| LoftyError::new(ErrorKind::BadAtom("Unable to parse \"ftyp\"'s major brand")))
@ -39,53 +151,58 @@ pub(crate) fn read_from<R>(data: &mut R, read_properties: bool) -> Result<Mp4Fil
where
R: Read + Seek,
{
let ftyp = verify_mp4(data)?;
let mut reader = AtomReader::new(data)?;
Moov::find(data)?;
let moov = Moov::parse(data, read_properties)?;
let ftyp = verify_mp4(&mut reader)?;
let file_length = data.seek(SeekFrom::End(0))?;
Moov::find(&mut reader)?;
let moov = Moov::parse(&mut reader, read_properties)?;
let file_length = reader.seek(SeekFrom::End(0))?;
Ok(Mp4File {
ftyp,
#[cfg(feature = "mp4_ilst")]
ilst: moov.meta,
properties: if read_properties {
super::properties::read_properties(data, &moov.traks, file_length)?
super::properties::read_properties(&mut reader, &moov.traks, file_length)?
} else {
Mp4Properties::default()
},
})
}
pub(super) fn skip_unneeded<R>(data: &mut R, ext: bool, len: u64) -> Result<()>
pub(super) fn skip_unneeded<R>(reader: &mut R, ext: bool, len: u64) -> Result<()>
where
R: Read + Seek,
{
if ext {
let pos = data.stream_position()?;
let pos = reader.stream_position()?;
if let (pos, false) = pos.overflowing_add(len - 8) {
data.seek(SeekFrom::Start(pos))?;
reader.seek(SeekFrom::Start(pos))?;
} else {
return Err(LoftyError::new(ErrorKind::TooMuchData));
}
} else {
data.seek(SeekFrom::Current(i64::from(len as u32) - 8))?;
reader.seek(SeekFrom::Current(i64::from(len as u32) - 8))?;
}
Ok(())
}
pub(super) fn nested_atom<R>(data: &mut R, len: u64, expected: &[u8]) -> Result<Option<AtomInfo>>
pub(super) fn nested_atom<R>(
reader: &mut R,
mut len: u64,
expected: &[u8],
) -> Result<Option<AtomInfo>>
where
R: Read + Seek,
{
let mut read = 8;
let mut ret = None;
while read < len {
let atom = AtomInfo::read(data)?;
while len > 8 {
let atom = AtomInfo::read(reader, len)?;
match atom.ident {
AtomIdent::Fourcc(ref fourcc) if fourcc == expected => {
@ -93,8 +210,8 @@ where
break;
},
_ => {
skip_unneeded(data, atom.extended, atom.len)?;
read += atom.len
skip_unneeded(reader, atom.extended, atom.len)?;
len = len.saturating_sub(atom.len);
},
}
}
@ -104,21 +221,24 @@ where
#[cfg(feature = "mp4_ilst")]
// Creates a tree of nested atoms
pub(super) fn atom_tree<R>(data: &mut R, len: u64, up_to: &[u8]) -> Result<(usize, Vec<AtomInfo>)>
pub(super) fn atom_tree<R>(
reader: &mut R,
mut len: u64,
up_to: &[u8],
) -> Result<(usize, Vec<AtomInfo>)>
where
R: Read + Seek,
{
let mut read = 8;
let mut found_idx: usize = 0;
let mut buf = Vec::new();
let mut i = 0;
while read < len {
let atom = AtomInfo::read(data)?;
while len > 8 {
let atom = AtomInfo::read(reader, len)?;
skip_unneeded(data, atom.extended, atom.len)?;
read += atom.len;
skip_unneeded(reader, atom.extended, atom.len)?;
len = len.saturating_sub(atom.len);
if let AtomIdent::Fourcc(ref fourcc) = atom.ident {
i += 1;
@ -137,11 +257,11 @@ where
}
#[cfg(feature = "mp4_ilst")]
pub(super) fn meta_is_full<R>(data: &mut R) -> Result<bool>
pub(super) fn meta_is_full<R>(reader: &mut R) -> Result<bool>
where
R: Read + Seek,
{
let meta_pos = data.stream_position()?;
let meta_pos = reader.stream_position()?;
// A full `meta` atom should have the following:
//
@ -150,39 +270,20 @@ where
//
// However, it's possible that it is written as a normal atom,
// meaning this would be the size of the next atom.
let _version_flags = data.read_u32::<BigEndian>()?;
let _version_flags = reader.read_u32::<BigEndian>()?;
// Check if the next four bytes is one of the nested `meta` atoms
let mut possible_ident = [0; 4];
data.read_exact(&mut possible_ident)?;
reader.read_exact(&mut possible_ident)?;
match &possible_ident {
b"hdlr" | b"ilst" | b"mhdr" | b"ctry" | b"lang" => {
data.seek(SeekFrom::Start(meta_pos))?;
reader.seek(SeekFrom::Start(meta_pos))?;
Ok(false)
},
_ => {
data.seek(SeekFrom::Start(meta_pos + 4))?;
reader.seek(SeekFrom::Start(meta_pos + 4))?;
Ok(true)
},
}
}
#[cfg(test)]
mod tests {
use crate::mp4::{Ilst, Mp4File};
use crate::tag::utils::test_utils;
use crate::AudioFile;
use std::io::Cursor;
#[test]
fn zero_sized_ilst() {
let file = Mp4File::read_from(
&mut Cursor::new(test_utils::read_path("tests/files/assets/zero/zero.ilst")),
false,
)
.unwrap();
assert_eq!(file.ilst, Some(Ilst::default()));
}
}

View file

@ -1,5 +1,5 @@
use super::atom_info::{AtomIdent, AtomInfo};
use super::read::skip_unneeded;
use super::read::{skip_unneeded, AtomReader};
use crate::error::Result;
use std::io::{Read, Seek, SeekFrom};
@ -9,7 +9,7 @@ pub(crate) struct Trak {
}
impl Trak {
pub(crate) fn parse<R>(data: &mut R, trak: &AtomInfo) -> Result<Self>
pub(super) fn parse<R>(reader: &mut AtomReader<R>, trak: &AtomInfo) -> Result<Self>
where
R: Read + Seek,
{
@ -18,15 +18,15 @@ impl Trak {
let mut read = 8;
while read < trak.len {
let atom = AtomInfo::read(data)?;
let atom = reader.next()?;
if atom.ident == AtomIdent::Fourcc(*b"mdia") {
mdia = Some(atom);
data.seek(SeekFrom::Current((trak.len - read - 8) as i64))?;
reader.seek(SeekFrom::Current((trak.len - read - 8) as i64))?;
break;
}
skip_unneeded(data, atom.extended, atom.len)?;
skip_unneeded(reader, atom.extended, atom.len)?;
read += atom.len;
}

View file

@ -196,3 +196,19 @@ pub trait TagExt: Accessor + Into<Tag> + Sized {
/// NOTE: This will **not** remove any format-specific extras, such as flags
fn clear(&mut self);
}
// TODO: https://github.com/rust-lang/rust/issues/59359
pub(crate) trait SeekStreamLen: std::io::Seek {
fn stream_len(&mut self) -> crate::error::Result<u64> {
use std::io::SeekFrom;
let current_pos = self.stream_position()?;
let len = self.seek(SeekFrom::End(0))?;
self.seek(SeekFrom::Start(current_pos))?;
Ok(len)
}
}
impl<T> SeekStreamLen for T where T: std::io::Seek {}

View file

@ -1 +1,7 @@
// TODO
use crate::oom_test;
use lofty::mp4::Mp4File;
#[test]
fn oom1() {
oom_test::<Mp4File>("mp4file_read_from/oom-db2665d79ec9c045bdb9c1e9a3d0c93e7e59393e");
}