diff --git a/CHANGELOG.md b/CHANGELOG.md index ef97b913..d90ccd61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- **MP4**: + - Support atoms with multiple values ([issue](https://github.com/Serial-ATA/lofty-rs/issues/48)) + - `Atom::from_collection` + ### Changed - **ID3v2**: Discard empty frames, rather than error - **APE**: Allow empty tag items diff --git a/src/lib.rs b/src/lib.rs index 06b074ca..6f36fb40 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -162,7 +162,8 @@ clippy::similar_names, clippy::tabs_in_doc_comments, clippy::len_without_is_empty, - clippy::needless_late_init + clippy::needless_late_init, + clippy::type_complexity )] #![cfg_attr(docsrs, feature(doc_auto_cfg))] diff --git a/src/mp4/ilst/atom.rs b/src/mp4/ilst/atom.rs index 274e3b49..6ea29343 100644 --- a/src/mp4/ilst/atom.rs +++ b/src/mp4/ilst/atom.rs @@ -1,17 +1,107 @@ use crate::mp4::AtomIdent; use crate::picture::Picture; -#[derive(Debug, PartialEq, Clone)] +use std::fmt::{Debug, Formatter}; + +// Atoms with multiple values aren't all that common, +// so there's no need to create a bunch of single-element Vecs +#[derive(PartialEq, Clone)] +pub(super) enum AtomDataStorage { + Single(AtomData), + Multiple(Vec), +} + +impl AtomDataStorage { + pub(super) fn take_first(self) -> AtomData { + match self { + AtomDataStorage::Single(val) => val, + AtomDataStorage::Multiple(mut data) => data.swap_remove(0), + } + } +} + +impl Debug for AtomDataStorage { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match &self { + AtomDataStorage::Single(v) => write!(f, "{:?}", v), + AtomDataStorage::Multiple(v) => f.debug_list().entries(v.iter()).finish(), + } + } +} + +impl<'a> IntoIterator for &'a AtomDataStorage { + type Item = &'a AtomData; + type IntoIter = AtomDataStorageIter<'a>; + + fn into_iter(self) -> Self::IntoIter { + let cap = match self { + AtomDataStorage::Single(_) => 0, + AtomDataStorage::Multiple(v) => v.len(), + }; + + Self::IntoIter { + storage: Some(self), + idx: 0, + cap, + } + } +} + +pub(super) struct AtomDataStorageIter<'a> { + storage: Option<&'a AtomDataStorage>, + idx: usize, + cap: usize, +} + +impl<'a> Iterator for AtomDataStorageIter<'a> { + type Item = &'a AtomData; + + fn next(&mut self) -> Option { + match self.storage { + Some(AtomDataStorage::Single(data)) => { + self.storage = None; + Some(data) + }, + Some(AtomDataStorage::Multiple(data)) => { + if self.idx == self.cap { + self.storage = None; + } + + self.idx += 1; + Some(&data[self.idx]) + }, + _ => None, + } + } +} + +#[derive(PartialEq, Clone)] /// Represents an `MP4` atom pub struct Atom { pub(crate) ident: AtomIdent, - pub(crate) data: AtomData, + pub(super) data: AtomDataStorage, } impl Atom { /// Create a new [`Atom`] pub fn new(ident: AtomIdent, data: AtomData) -> Self { - Self { ident, data } + Self { + ident, + data: AtomDataStorage::Single(data), + } + } + + /// Create a new [`Atom`] from a collection of [`AtomData`]s + /// + /// This will return `None` if `data` is empty, as empty atoms are useless. + pub fn from_collection(ident: AtomIdent, mut data: Vec) -> Option { + let data = match data.len() { + 0 => return None, + 1 => AtomDataStorage::Single(data.swap_remove(0)), + _ => AtomDataStorage::Multiple(data), + }; + + Some(Self { ident, data }) } /// Returns the atom's [`AtomIdent`] @@ -20,8 +110,24 @@ impl Atom { } /// Returns the atom's [`AtomData`] + // TODO: Do this properly to return all values pub fn data(&self) -> &AtomData { - &self.data + match &self.data { + AtomDataStorage::Single(val) => val, + // There must be at least 1 element in here + AtomDataStorage::Multiple(data) => &data[0], + } + } + + // TODO: push_data +} + +impl Debug for Atom { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Atom") + .field("ident", &self.ident) + .field("data", &self.data) + .finish() } } @@ -105,65 +211,3 @@ impl From for AdvisoryRating { } } } - -pub(crate) struct AtomRef<'a> { - pub(crate) ident: AtomIdentRef<'a>, - pub(crate) data: AtomDataRef<'a>, -} - -impl<'a> Into> for &'a Atom { - fn into(self) -> AtomRef<'a> { - AtomRef { - ident: (&self.ident).into(), - data: (&self.data).into(), - } - } -} - -pub(crate) enum AtomIdentRef<'a> { - Fourcc([u8; 4]), - Freeform { mean: &'a str, name: &'a str }, -} - -impl<'a> Into> for &'a AtomIdent { - fn into(self) -> AtomIdentRef<'a> { - match self { - AtomIdent::Fourcc(fourcc) => AtomIdentRef::Fourcc(*fourcc), - AtomIdent::Freeform { mean, name } => AtomIdentRef::Freeform { mean, name }, - } - } -} - -impl<'a> From> for AtomIdent { - fn from(input: AtomIdentRef<'a>) -> Self { - match input { - AtomIdentRef::Fourcc(fourcc) => AtomIdent::Fourcc(fourcc), - AtomIdentRef::Freeform { mean, name } => AtomIdent::Freeform { - mean: mean.to_string(), - name: name.to_string(), - }, - } - } -} - -pub(crate) enum AtomDataRef<'a> { - UTF8(&'a str), - UTF16(&'a str), - Picture(&'a Picture), - SignedInteger(i32), - UnsignedInteger(u32), - Unknown { code: u32, data: &'a [u8] }, -} - -impl<'a> Into> for &'a AtomData { - fn into(self) -> AtomDataRef<'a> { - match self { - AtomData::UTF8(utf8) => AtomDataRef::UTF8(utf8), - AtomData::UTF16(utf16) => AtomDataRef::UTF16(utf16), - AtomData::Picture(pic) => AtomDataRef::Picture(pic), - AtomData::SignedInteger(int) => AtomDataRef::SignedInteger(*int), - AtomData::UnsignedInteger(uint) => AtomDataRef::UnsignedInteger(*uint), - AtomData::Unknown { code, data } => AtomDataRef::Unknown { code: *code, data }, - } - } -} diff --git a/src/mp4/ilst/mod.rs b/src/mp4/ilst/mod.rs index 09a547ef..dbdfc467 100644 --- a/src/mp4/ilst/mod.rs +++ b/src/mp4/ilst/mod.rs @@ -1,17 +1,19 @@ pub(super) mod atom; pub(super) mod constants; pub(super) mod read; +mod r#ref; pub(crate) mod write; use super::AtomIdent; -use crate::error::{LoftyError, Result}; +use crate::error::LoftyError; +use crate::mp4::ilst::atom::AtomDataStorage; use crate::picture::{Picture, PictureType}; use crate::tag::item::{ItemKey, ItemValue, TagItem}; use crate::tag::{Tag, TagType}; use crate::traits::{Accessor, TagExt}; -use atom::{AdvisoryRating, Atom, AtomData, AtomDataRef, AtomIdentRef, AtomRef}; +use atom::{AdvisoryRating, Atom, AtomData}; +use r#ref::AtomIdentRef; -use std::convert::TryInto; use std::fs::{File, OpenOptions}; use std::io::Write; use std::path::Path; @@ -39,7 +41,7 @@ macro_rules! impl_accessor { fn [](&mut self, value: String) { self.replace_atom(Atom { ident: $const, - data: AtomData::UTF8(value), + data: AtomDataStorage::Single(AtomData::UTF8(value)), }) } @@ -134,11 +136,14 @@ impl Ilst { pub fn pictures(&self) -> impl Iterator { const COVR: AtomIdent = AtomIdent::Fourcc(*b"covr"); - self.atoms.iter().filter_map(|a| match a { - Atom { - ident: COVR, - data: AtomData::Picture(pic), - } => Some(pic), + self.atoms.iter().filter_map(|a| match a.ident { + COVR => { + if let AtomData::Picture(pic) = a.data() { + Some(pic) + } else { + None + } + }, _ => None, }) } @@ -150,7 +155,7 @@ impl Ilst { self.atoms.push(Atom { ident: AtomIdent::Fourcc(*b"covr"), - data: AtomData::Picture(picture), + data: AtomDataStorage::Single(AtomData::Picture(picture)), }) } @@ -162,8 +167,8 @@ impl Ilst { /// Returns the parental advisory rating according to the `rtng` atom pub fn advisory_rating(&self) -> Option { - if let Some(Atom { data, .. }) = self.atom(&AtomIdent::Fourcc(*b"rtng")) { - let rating = match data { + if let Some(atom) = self.atom(&AtomIdent::Fourcc(*b"rtng")) { + let rating = match atom.data() { AtomData::SignedInteger(si) => *si as u8, AtomData::Unknown { data: c, .. } if !c.is_empty() => c[0], _ => return None, @@ -181,7 +186,7 @@ impl Ilst { self.replace_atom(Atom { ident: AtomIdent::Fourcc(*b"rtng"), - data: AtomData::SignedInteger(i32::from(byte)), + data: AtomDataStorage::Single(AtomData::SignedInteger(i32::from(byte))), }) } @@ -236,11 +241,11 @@ impl TagExt for Ilst { } fn save_to(&self, file: &mut File) -> std::result::Result<(), Self::Err> { - Into::>::into(self).write_to(file) + self.as_ref().write_to(file) } fn dump_to(&self, writer: &mut W) -> std::result::Result<(), Self::Err> { - Into::>::into(self).dump_to(writer) + self.as_ref().dump_to(writer) } fn remove_from_path>(&self, path: P) -> std::result::Result<(), Self::Err> { @@ -261,7 +266,8 @@ impl From for Tag { let mut tag = Self::new(TagType::Mp4Ilst); for atom in input.atoms { - let value = match atom.data { + let Atom { ident, data } = atom; + let value = match data.take_first() { AtomData::UTF8(text) | AtomData::UTF16(text) => ItemValue::Text(text), AtomData::Picture(pic) => { tag.pictures.push(pic); @@ -269,7 +275,7 @@ impl From for Tag { }, // We have to special case track/disc numbers since they are stored together AtomData::Unknown { code: 0, data } if data.len() >= 6 => { - if let AtomIdent::Fourcc(ref fourcc) = atom.ident { + if let AtomIdent::Fourcc(ref fourcc) = ident { match fourcc { b"trkn" => { let current = u16::from_be_bytes([data[2], data[3]]); @@ -296,7 +302,7 @@ impl From for Tag { let key = ItemKey::from_key( TagType::Mp4Ilst, - &match atom.ident { + &match ident { AtomIdent::Fourcc(fourcc) => { fourcc.iter().map(|b| *b as char).collect::() }, @@ -330,10 +336,10 @@ impl From for Ilst { tag.atoms.push(Atom { ident: AtomIdent::Fourcc(ident), - data: AtomData::Unknown { + data: AtomDataStorage::Single(AtomData::Unknown { code: 0, data: vec![0, 0, current[0], current[1], total[0], total[1], 0, 0], - }, + }), }) }, } @@ -361,7 +367,7 @@ impl From for Ilst { ItemKey::DiscTotal => convert_to_uint(&mut discs.1, data.as_str()), _ => ilst.atoms.push(Atom { ident, - data: AtomData::UTF8(data), + data: AtomDataStorage::Single(AtomData::UTF8(data)), }), } } @@ -374,7 +380,7 @@ impl From for Ilst { ilst.atoms.push(Atom { ident: AtomIdent::Fourcc([b'c', b'o', b'v', b'r']), - data: AtomData::Picture(picture), + data: AtomDataStorage::Single(AtomData::Picture(picture)), }) } @@ -385,50 +391,6 @@ impl From for Ilst { } } -pub(crate) struct IlstRef<'a> { - atoms: Box> + 'a>, -} - -impl<'a> IlstRef<'a> { - pub(crate) fn write_to(&mut self, file: &mut File) -> Result<()> { - write::write_to(file, self) - } - - pub(crate) fn dump_to(&mut self, writer: &mut W) -> Result<()> { - let temp = write::build_ilst(&mut self.atoms)?; - writer.write_all(&*temp)?; - - Ok(()) - } -} - -impl<'a> Into> for &'a Ilst { - fn into(self) -> IlstRef<'a> { - IlstRef { - atoms: Box::new(self.atoms.iter().map(Into::into)), - } - } -} - -impl<'a> Into> for &'a Tag { - fn into(self) -> IlstRef<'a> { - let iter = - self.items - .iter() - .filter_map(|i| match (item_key_to_ident(i.key()), i.value()) { - (Some(ident), ItemValue::Text(text)) => Some(AtomRef { - ident, - data: AtomDataRef::UTF8(text), - }), - _ => None, - }); - - IlstRef { - atoms: Box::new(iter), - } - } -} - fn item_key_to_ident(key: &ItemKey) -> Option> { key.map_key(TagType::Mp4Ilst, true).and_then(|ident| { if ident.starts_with("----") { @@ -458,6 +420,7 @@ fn item_key_to_ident(key: &ItemKey) -> Option> { #[cfg(test)] mod tests { + use crate::mp4::ilst::atom::AtomDataStorage; use crate::mp4::{AdvisoryRating, Atom, AtomData, AtomIdent, Ilst, Mp4File}; use crate::tag::utils::test_utils::read_path; use crate::{Accessor, AudioFile, ItemKey, Tag, TagExt, TagType}; @@ -717,7 +680,7 @@ mod tests { let mut tag = Ilst::default(); tag.insert_atom(Atom { ident: AtomIdent::Fourcc(*b"\xa9ART"), - data: AtomData::UTF8(String::from("Foo artist")), + data: AtomDataStorage::Single(AtomData::UTF8(String::from("Foo artist"))), }); assert!(tag.save_to(&mut file).is_ok()); @@ -732,4 +695,25 @@ mod tests { &AtomData::UTF8(String::from("Foo artist")), ); } + + #[test] + fn multi_value_atom() { + let ilst = read_ilst("tests/tags/assets/ilst/multi_value_atom.ilst"); + let artist_atom = ilst.atom(&AtomIdent::Fourcc(*b"\xa9ART")).unwrap(); + + assert_eq!( + artist_atom.data, + AtomDataStorage::Multiple(vec![ + AtomData::UTF8(String::from("Foo artist")), + AtomData::UTF8(String::from("Bar artist")), + ]) + ); + + // Sanity single value atom + verify_atom( + &ilst, + *b"\xa9gen", + &AtomData::UTF8(String::from("Classical")), + ); + } } diff --git a/src/mp4/ilst/read.rs b/src/mp4/ilst/read.rs index 55fdd670..68032201 100644 --- a/src/mp4/ilst/read.rs +++ b/src/mp4/ilst/read.rs @@ -14,6 +14,7 @@ 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(reader: &mut R, len: u64) -> Result @@ -28,35 +29,34 @@ where let mut tag = Ilst::default(); while let Ok(atom) = AtomInfo::read(&mut cursor) { - let ident = match atom.ident { - AtomIdent::Fourcc(ref fourcc) => match fourcc { + if let AtomIdent::Fourcc(ref fourcc) = atom.ident { + match fourcc { b"free" | b"skip" => { skip_unneeded(&mut cursor, atom.extended, atom.len)?; continue; }, b"covr" => { - handle_covr(&mut cursor, &mut tag)?; + handle_covr(&mut cursor, &mut tag, &atom)?; continue; }, // Upgrade this to a \xa9gen atom b"gnre" => { - let content = parse_data(&mut cursor)?; + if let Some(atom_data) = parse_data_inner(&mut cursor, &atom)? { + let mut data = Vec::new(); - if let Some(AtomData::Unknown { - code: BE_UNSIGNED_INTEGER | 0, - data, - }) = content - { - if data.len() >= 2 { - let index = data[1] as usize; - - if index > 0 && index <= GENRES.len() { - tag.atoms.push(Atom { - ident: AtomIdent::Fourcc(*b"\xa9gen"), - data: AtomData::UTF8(String::from(GENRES[index - 1])), - }) + for (flags, content) in atom_data { + if (flags == BE_SIGNED_INTEGER || flags == 0) && content.len() >= 2 { + let index = content[1] as usize; + if index > 0 && index <= GENRES.len() { + data.push(AtomData::UTF8(String::from(GENRES[index - 1]))); + } } } + + tag.atoms.push(Atom { + ident: AtomIdent::Fourcc(*b"\xa9gen"), + data: AtomDataStorage::Multiple(data), + }) } continue; @@ -64,94 +64,117 @@ 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((code, content)) = parse_data_inner(&mut cursor)? { - if (code == BE_SIGNED_INTEGER || code == BE_64BIT_SIGNED_INTEGER) - && content.len() == 8 - { - tag.atoms.push(Atom { - ident: AtomIdent::Fourcc(*b"plID"), - data: AtomData::Unknown { + if let Some(atom_data) = parse_data_inner(&mut cursor, &atom)? { + let mut data = Vec::new(); + + for (code, content) in atom_data { + if (code == BE_SIGNED_INTEGER || code == BE_64BIT_SIGNED_INTEGER) + && content.len() == 8 + { + data.push(AtomData::Unknown { code, data: content, - }, - }) + }) + } } + + tag.atoms.push(Atom { + ident: AtomIdent::Fourcc(*b"plID"), + data: AtomDataStorage::Multiple(data), + }) } continue; }, - _ => atom.ident, - }, - ident => ident, - }; - - if let Some(data) = parse_data(&mut cursor)? { - tag.atoms.push(Atom { ident, data }) + _ => {}, + } } + + parse_data(&mut cursor, &mut tag, atom)?; } Ok(tag) } -fn parse_data(data: &mut R) -> Result> +fn parse_data(data: &mut R, tag: &mut Ilst, atom_info: AtomInfo) -> Result<()> where R: Read + Seek, { - if let Some((flags, content)) = parse_data_inner(data)? { - // https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW35 - let value = match flags { - UTF8 => AtomData::UTF8(String::from_utf8(content)?), - UTF16 => AtomData::UTF16(utf16_decode(&*content, u16::from_be_bytes)?), - BE_SIGNED_INTEGER => AtomData::SignedInteger(parse_int(&content)?), - BE_UNSIGNED_INTEGER => AtomData::UnsignedInteger(parse_uint(&content)?), - code => AtomData::Unknown { - code, - data: content, - }, - }; + if let Some(mut atom_data) = parse_data_inner(data, &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); + let data = interpret_atom_content(flags, content)?; - return Ok(Some(value)); + tag.atoms.push(Atom { + ident: atom_info.ident, + data: AtomDataStorage::Single(data), + }); + + return Ok(()); + } + + let mut data = Vec::new(); + for (flags, content) in atom_data { + let value = interpret_atom_content(flags, content)?; + data.push(value); + } + + tag.atoms.push(Atom { + ident: atom_info.ident, + data: AtomDataStorage::Multiple(data), + }); } - Ok(None) + Ok(()) } -fn parse_data_inner(data: &mut R) -> Result)>> +fn parse_data_inner(data: &mut R, atom_info: &AtomInfo) -> Result)>>> where R: Read + Seek, { - let atom = AtomInfo::read(data)?; + // An atom can contain multiple data atoms + let mut ret = Vec::new(); - match atom.ident { - AtomIdent::Fourcc(ref name) if name == b"data" => {}, - _ => { - return Err(LoftyError::new(ErrorKind::BadAtom( - "Expected atom \"data\" to follow name", - ))) - }, + let to_read = (atom_info.start + atom_info.len) - data.stream_position()?; + let mut pos = 0; + while pos < to_read { + let data_atom = AtomInfo::read(data)?; + match data_atom.ident { + AtomIdent::Fourcc(ref name) if name == b"data" => {}, + _ => { + return Err(LoftyError::new(ErrorKind::BadAtom( + "Expected atom \"data\" to follow name", + ))) + }, + } + + // We don't care about the version + let _version = data.read_u8()?; + + let mut flags = [0; 3]; + data.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))?; + + let content_len = (data_atom.len - 16) as usize; + if content_len == 0 { + // We won't add empty atoms + return Ok(None); + } + + let mut content = try_vec![0; content_len]; + data.read_exact(&mut content)?; + + pos += data_atom.len; + ret.push((flags, content)); } - // We don't care about the version - let _version = data.read_u8()?; - - let mut flags = [0; 3]; - data.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))?; - - let content_len = (atom.len - 16) as usize; - if content_len == 0 { - // We won't add empty atoms - return Ok(None); - } - - let mut content = try_vec![0; content_len]; - data.read_exact(&mut content)?; - - Ok(Some((flags, content))) + let ret = if ret.is_empty() { None } else { Some(ret) }; + Ok(ret) } fn parse_uint(bytes: &[u8]) -> Result { @@ -182,40 +205,65 @@ fn parse_int(bytes: &[u8]) -> Result { }) } -fn handle_covr(reader: &mut Cursor>, tag: &mut Ilst) -> Result<()> { - if let Some(value) = parse_data(reader)? { - let (mime_type, data) = match value { - AtomData::Unknown { code, data } => match code { +fn handle_covr(reader: &mut Cursor>, tag: &mut Ilst, atom_info: &AtomInfo) -> Result<()> { + if let Some(atom_data) = parse_data_inner(reader, atom_info)? { + let mut data = Vec::new(); + + let len = atom_data.len(); + for (flags, value) in atom_data { + let mime_type = match flags { // Type 0 is implicit - RESERVED => (MimeType::None, data), + RESERVED => MimeType::None, // GIF is deprecated - 12 => (MimeType::Gif, data), - JPEG => (MimeType::Jpeg, data), - PNG => (MimeType::Png, data), - BMP => (MimeType::Bmp, data), + 12 => MimeType::Gif, + JPEG => MimeType::Jpeg, + PNG => MimeType::Png, + BMP => MimeType::Bmp, _ => { return Err(LoftyError::new(ErrorKind::BadAtom( "\"covr\" atom has an unknown type", ))) }, - }, - _ => { - return Err(LoftyError::new(ErrorKind::BadAtom( - "\"covr\" atom has an unknown type", - ))) - }, - }; + }; - tag.atoms.push(Atom { - ident: AtomIdent::Fourcc(*b"covr"), - data: AtomData::Picture(Picture { + let picture_data = AtomData::Picture(Picture { pic_type: PictureType::Other, mime_type, description: None, - data: Cow::from(data), - }), + data: Cow::from(value), + }); + + if len == 1 { + tag.atoms.push(Atom { + ident: AtomIdent::Fourcc(*b"covr"), + data: AtomDataStorage::Single(picture_data), + }); + + return Ok(()); + } + + data.push(picture_data); + } + + tag.atoms.push(Atom { + ident: AtomIdent::Fourcc(*b"covr"), + data: AtomDataStorage::Multiple(data), }); } Ok(()) } + +fn interpret_atom_content(flags: u32, content: Vec) -> Result { + // https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW35 + Ok(match flags { + UTF8 => AtomData::UTF8(String::from_utf8(content)?), + UTF16 => AtomData::UTF16(utf16_decode(&*content, u16::from_be_bytes)?), + BE_SIGNED_INTEGER => AtomData::SignedInteger(parse_int(&content)?), + BE_UNSIGNED_INTEGER => AtomData::UnsignedInteger(parse_uint(&content)?), + code => AtomData::Unknown { + code, + data: content, + }, + }) +} diff --git a/src/mp4/ilst/ref.rs b/src/mp4/ilst/ref.rs new file mode 100644 index 00000000..9cebdfaf --- /dev/null +++ b/src/mp4/ilst/ref.rs @@ -0,0 +1,77 @@ +// ********************* +// Reference Conversions +// ********************* + +use crate::error::Result; +use crate::mp4::{Atom, AtomData, AtomIdent, Ilst}; + +use std::fs::File; +use std::io::Write; + +impl Ilst { + pub(crate) fn as_ref(&self) -> IlstRef<'_, impl IntoIterator> { + IlstRef { + atoms: Box::new(self.atoms.iter().map(Atom::as_ref)), + } + } +} + +pub(crate) struct IlstRef<'a, I> { + pub(super) atoms: Box> + 'a>, +} + +impl<'a, I: 'a> IlstRef<'a, I> +where + I: IntoIterator, +{ + pub(crate) fn write_to(&mut self, file: &mut File) -> Result<()> { + super::write::write_to(file, self) + } + + pub(crate) fn dump_to(&mut self, writer: &mut W) -> Result<()> { + let temp = super::write::build_ilst(&mut self.atoms)?; + writer.write_all(&*temp)?; + + Ok(()) + } +} + +impl Atom { + pub(super) fn as_ref(&self) -> AtomRef<'_, impl IntoIterator> { + AtomRef { + ident: (&self.ident).into(), + data: (&self.data).into_iter(), + } + } +} + +pub(crate) struct AtomRef<'a, I> { + pub(crate) ident: AtomIdentRef<'a>, + pub(crate) data: I, +} + +pub(crate) enum AtomIdentRef<'a> { + Fourcc([u8; 4]), + Freeform { mean: &'a str, name: &'a str }, +} + +impl<'a> Into> for &'a AtomIdent { + fn into(self) -> AtomIdentRef<'a> { + match self { + AtomIdent::Fourcc(fourcc) => AtomIdentRef::Fourcc(*fourcc), + AtomIdent::Freeform { mean, name } => AtomIdentRef::Freeform { mean, name }, + } + } +} + +impl<'a> From> for AtomIdent { + fn from(input: AtomIdentRef<'a>) -> Self { + match input { + AtomIdentRef::Fourcc(fourcc) => AtomIdent::Fourcc(fourcc), + AtomIdentRef::Freeform { mean, name } => AtomIdent::Freeform { + mean: mean.to_string(), + name: name.to_string(), + }, + } + } +} diff --git a/src/mp4/ilst/write.rs b/src/mp4/ilst/write.rs index ffa521ea..2d9afe45 100644 --- a/src/mp4/ilst/write.rs +++ b/src/mp4/ilst/write.rs @@ -1,11 +1,12 @@ -use super::{AtomDataRef, IlstRef}; +use super::r#ref::IlstRef; use crate::error::{ErrorKind, FileEncodingError, LoftyError, Result}; use crate::file::FileType; use crate::macros::try_vec; use crate::mp4::atom_info::{AtomIdent, AtomInfo}; -use crate::mp4::ilst::{AtomIdentRef, AtomRef}; +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::AtomData; use crate::picture::{MimeType, Picture}; use std::fs::File; @@ -13,7 +14,10 @@ use std::io::{Cursor, Read, Seek, SeekFrom, Write}; use byteorder::{BigEndian, WriteBytesExt}; -pub(in crate) fn write_to(data: &mut File, tag: &mut IlstRef<'_>) -> Result<()> { +pub(in crate) fn write_to<'a, I: 'a>(data: &mut File, tag: &mut IlstRef<'a, I>) -> Result<()> +where + I: IntoIterator, +{ verify_mp4(data)?; let moov = Moov::find(data)?; @@ -292,7 +296,12 @@ fn write_size(start: u64, size: u64, extended: bool, writer: &mut Cursor Ok(()) } -pub(super) fn build_ilst(atoms: &mut dyn Iterator>) -> Result> { +pub(super) fn build_ilst<'a, I: 'a>( + atoms: &mut dyn Iterator>, +) -> Result> +where + I: IntoIterator, +{ let mut peek = atoms.peekable(); if peek.peek().is_none() { @@ -313,7 +322,7 @@ pub(super) fn build_ilst(atoms: &mut dyn Iterator>) -> Result AtomIdentRef::Freeform { mean, name } => write_freeform(mean, name, &mut writer)?, } - write_atom_data(&atom.data, &mut writer)?; + write_atom_data(atom.data, &mut writer)?; let end = writer.stream_position()?; @@ -357,15 +366,22 @@ fn write_freeform(mean: &str, name: &str, writer: &mut Cursor>) -> Resul Ok(()) } -fn write_atom_data(value: &AtomDataRef<'_>, writer: &mut Cursor>) -> Result<()> { - match value { - AtomDataRef::UTF8(text) => write_data(1, text.as_bytes(), writer), - AtomDataRef::UTF16(text) => write_data(2, text.as_bytes(), writer), - AtomDataRef::Picture(pic) => write_picture(pic, writer), - AtomDataRef::SignedInteger(int) => write_signed_int(*int, writer), - AtomDataRef::UnsignedInteger(uint) => write_unsigned_int(*uint, writer), - AtomDataRef::Unknown { code, data } => write_data(*code, data, writer), +fn write_atom_data<'a, I: 'a>(data: I, writer: &mut Cursor>) -> Result<()> +where + I: IntoIterator, +{ + for value in data { + match value { + AtomData::UTF8(text) => write_data(1, text.as_bytes(), writer)?, + AtomData::UTF16(text) => write_data(2, text.as_bytes(), writer)?, + AtomData::Picture(ref pic) => write_picture(pic, writer)?, + AtomData::SignedInteger(int) => write_signed_int(*int, writer)?, + AtomData::UnsignedInteger(uint) => write_unsigned_int(*uint, writer)?, + AtomData::Unknown { code, ref data } => write_data(*code, data, writer)?, + }; } + + Ok(()) } fn write_signed_int(int: i32, writer: &mut Cursor>) -> Result<()> { diff --git a/src/tag/utils.rs b/src/tag/utils.rs index 414573ce..e1779a8c 100644 --- a/src/tag/utils.rs +++ b/src/tag/utils.rs @@ -8,7 +8,7 @@ use crate::id3::v1::tag::Id3v1TagRef; #[cfg(feature = "id3v2")] use crate::id3::v2::{self, tag::Id3v2TagRef, Id3v2TagFlags}; #[cfg(feature = "mp4_ilst")] -use crate::mp4::ilst::IlstRef; +use crate::mp4::Ilst; #[cfg(feature = "vorbis_comments")] use crate::ogg::tag::{create_vorbis_comments_ref, VorbisCommentsRef}; #[cfg(feature = "ape")] @@ -32,7 +32,9 @@ pub(crate) fn write_tag(tag: &Tag, file: &mut File, file_type: FileType) -> Resu }, FileType::MP3 => mp3::write::write_to(file, tag), #[cfg(feature = "mp4_ilst")] - FileType::MP4 => crate::mp4::ilst::write::write_to(file, &mut Into::>::into(tag)), + FileType::MP4 => { + crate::mp4::ilst::write::write_to(file, &mut Into::::into(tag.clone()).as_ref()) + }, FileType::WAV => iff::wav::write::write_to(file, tag), _ => Err(LoftyError::new(ErrorKind::UnsupportedTag)), } @@ -56,7 +58,7 @@ pub(crate) fn dump_tag(tag: &Tag, writer: &mut W) -> Result<()> { } .dump_to(writer), #[cfg(feature = "mp4_ilst")] - TagType::Mp4Ilst => Into::>::into(tag).dump_to(writer), + TagType::Mp4Ilst => Into::::into(tag.clone()).as_ref().dump_to(writer), #[cfg(feature = "vorbis_comments")] TagType::VorbisComments => { let (vendor, items, pictures) = create_vorbis_comments_ref(tag); diff --git a/tests/tags/assets/ilst/multi_value_atom.ilst b/tests/tags/assets/ilst/multi_value_atom.ilst new file mode 100644 index 00000000..a8f0ea80 Binary files /dev/null and b/tests/tags/assets/ilst/multi_value_atom.ilst differ