diff --git a/src/logic/ape/tag/item.rs b/src/logic/ape/tag/item.rs index cdd68697..3f79480e 100644 --- a/src/logic/ape/tag/item.rs +++ b/src/logic/ape/tag/item.rs @@ -5,6 +5,7 @@ use crate::types::tag::TagType; use std::convert::TryFrom; +#[derive(Debug, PartialEq)] pub struct ApeItem { pub read_only: bool, pub(crate) key: String, diff --git a/src/logic/ape/tag/mod.rs b/src/logic/ape/tag/mod.rs index 631d6106..3d8c9a6b 100644 --- a/src/logic/ape/tag/mod.rs +++ b/src/logic/ape/tag/mod.rs @@ -10,8 +10,9 @@ use crate::types::tag::{Tag, TagType}; use std::collections::HashMap; use std::convert::TryInto; use std::fs::File; +use std::io::{Read, Seek}; -#[derive(Default)] +#[derive(Default, Debug, PartialEq)] /// An APE tag pub struct ApeTag { pub read_only: bool, @@ -33,6 +34,13 @@ impl ApeTag { } impl ApeTag { + pub fn read_from(reader: &mut R) -> Result + where + R: Read + Seek, + { + Ok(read::read_ape_tag(reader, false)?.0) + } + pub fn write_to(&self, file: &mut File) -> Result<()> { Into::::into(self).write_to(file) } diff --git a/src/logic/id3/v1/read.rs b/src/logic/id3/v1/read.rs index 1ecf9c2d..f269c5b8 100644 --- a/src/logic/id3/v1/read.rs +++ b/src/logic/id3/v1/read.rs @@ -19,10 +19,10 @@ pub fn parse_id3v1(reader: [u8; 128]) -> Id3v1Tag { tag.album = decode_text(&reader[60..90]); tag.year = decode_text(&reader[90..94]); - let range = if reader[119] == 0 && reader[122] != 0 { - tag.track_number = Some(reader[122]); + let range = if reader[119] == 0 && reader[123] != 0 { + tag.track_number = Some(reader[123]); - 94_usize..123 + 94_usize..122 } else { 94..124 }; diff --git a/src/logic/id3/v1/tag.rs b/src/logic/id3/v1/tag.rs index af601373..71bb1abe 100644 --- a/src/logic/id3/v1/tag.rs +++ b/src/logic/id3/v1/tag.rs @@ -5,7 +5,7 @@ use crate::types::tag::{Tag, TagType}; use std::fs::File; -#[derive(Default, Debug)] +#[derive(Default, Debug, PartialEq)] /// An ID3v1 tag /// /// ID3v1 is a severely limited format, with each field @@ -59,6 +59,10 @@ impl Id3v1Tag { && self.genre.is_none() } + pub fn read_from(tag: [u8; 128]) -> Self { + super::read::parse_id3v1(tag) + } + pub fn write_to(&self, file: &mut File) -> Result<()> { Into::::into(self).write_to(file) } diff --git a/src/logic/id3/v2/read.rs b/src/logic/id3/v2/read.rs index 66751a55..49b5e2df 100644 --- a/src/logic/id3/v2/read.rs +++ b/src/logic/id3/v2/read.rs @@ -11,7 +11,10 @@ use std::io::Read; use byteorder::{BigEndian, ReadBytesExt}; -pub(crate) fn parse_id3v2(bytes: &mut &[u8]) -> Result { +pub(crate) fn parse_id3v2(bytes: &mut R) -> Result +where + R: Read, +{ let mut header = [0; 10]; bytes.read_exact(&mut header)?; diff --git a/src/logic/id3/v2/tag.rs b/src/logic/id3/v2/tag.rs index d5917935..a083a38d 100644 --- a/src/logic/id3/v2/tag.rs +++ b/src/logic/id3/v2/tag.rs @@ -11,9 +11,11 @@ use crate::types::tag::{Tag, TagType}; use std::convert::TryInto; use std::fs::File; +use std::io::Read; use byteorder::ByteOrder; +#[derive(PartialEq, Debug)] pub struct Id3v2Tag { flags: Id3v2TagFlags, pub(super) original_version: Id3v2Version, @@ -80,6 +82,13 @@ impl Id3v2Tag { } impl Id3v2Tag { + pub fn read_from(reader: &mut R) -> Result + where + R: Read, + { + super::read::parse_id3v2(reader) + } + pub fn write_to(&self, file: &mut File) -> Result<()> { Into::::into(self).write_to(file) } @@ -156,7 +165,7 @@ impl From for Id3v2Tag { } } -#[derive(Default, Copy, Clone)] +#[derive(Default, Copy, Clone, Debug, PartialEq)] #[allow(clippy::struct_excessive_bools)] /// Flags that apply to the entire tag pub struct Id3v2TagFlags { diff --git a/src/logic/iff/chunk.rs b/src/logic/iff/chunk.rs index 8451e301..49fa2557 100644 --- a/src/logic/iff/chunk.rs +++ b/src/logic/iff/chunk.rs @@ -27,7 +27,7 @@ impl Chunks { pub fn next(&mut self, data: &mut R) -> Result<()> where - R: Read + Seek, + R: Read, { data.read_exact(&mut self.fourcc)?; self.size = data.read_u32::()?; @@ -37,7 +37,7 @@ impl Chunks { pub fn content(&mut self, data: &mut R) -> Result> where - R: Read + Seek, + R: Read, { let mut content = vec![0; self.size as usize]; data.read_exact(&mut content)?; diff --git a/src/logic/iff/wav/tag/mod.rs b/src/logic/iff/wav/tag/mod.rs index 66d7c5e1..9a7de86c 100644 --- a/src/logic/iff/wav/tag/mod.rs +++ b/src/logic/iff/wav/tag/mod.rs @@ -6,8 +6,9 @@ use crate::types::item::{ItemKey, ItemValue, TagItem}; use crate::types::tag::{Tag, TagType}; use std::fs::File; +use std::io::{Read, Seek}; -#[derive(Default)] +#[derive(Default, Debug, PartialEq)] /// A RIFF INFO LIST pub struct RiffInfoList { /// A collection of chunk-value pairs @@ -15,8 +16,12 @@ pub struct RiffInfoList { } impl RiffInfoList { - pub fn push(&mut self, key: String, value: String) { + pub fn insert(&mut self, key: String, value: String) { if valid_key(key.as_str()) { + self.items + .iter() + .position(|(k, _)| k == &key) + .map(|p| self.items.remove(p)); self.items.push((key, value)) } } @@ -34,6 +39,17 @@ impl RiffInfoList { } impl RiffInfoList { + pub fn read_from(reader: &mut R, end: u64) -> Result + where + R: Read + Seek, + { + let mut tag = Self::default(); + + read::parse_riff_info(reader, end, &mut tag)?; + + Ok(tag) + } + pub fn write_to(&self, file: &mut File) -> Result<()> { Into::::into(self).write_to(file) } diff --git a/src/logic/mp3/read.rs b/src/logic/mp3/read.rs index 8566838a..444a173d 100644 --- a/src/logic/mp3/read.rs +++ b/src/logic/mp3/read.rs @@ -13,7 +13,7 @@ use std::time::Duration; use byteorder::{BigEndian, ByteOrder, ReadBytesExt}; fn read_properties( - mut first_frame: (Header, u64), + first_frame: (Header, u64), last_frame: (Header, u64), xing_header: Option, file_length: u64, diff --git a/src/logic/mp4/atom_info.rs b/src/logic/mp4/atom_info.rs index 45a28a6f..20217509 100644 --- a/src/logic/mp4/atom_info.rs +++ b/src/logic/mp4/atom_info.rs @@ -1,8 +1,8 @@ use crate::error::{LoftyError, Result}; +use crate::logic::mp4::ilst::AtomIdent; use std::io::{Read, Seek, SeekFrom}; -use crate::mp4::AtomIdent; use byteorder::{BigEndian, ReadBytesExt}; pub(crate) struct AtomInfo { diff --git a/src/logic/mp4/ilst/mod.rs b/src/logic/mp4/ilst/mod.rs index f63d91fe..eb4053f3 100644 --- a/src/logic/mp4/ilst/mod.rs +++ b/src/logic/mp4/ilst/mod.rs @@ -1,19 +1,48 @@ pub(in crate::logic::mp4) mod read; pub(in crate::logic) mod write; +use crate::error::Result; use crate::types::item::{ItemKey, ItemValue, TagItem}; use crate::types::picture::{Picture, PictureType}; use crate::types::tag::{Tag, TagType}; use std::convert::TryInto; +use std::io::Read; #[cfg(feature = "mp4_atoms")] -#[derive(Default)] +#[derive(Default, PartialEq, Debug)] /// An Mp4 pub struct Ilst { pub(crate) atoms: Vec, } +impl Ilst { + pub fn atom(&self, ident: &AtomIdent) -> Option<&Atom> { + self.atoms.iter().find(|a| &a.ident == ident) + } + + pub fn insert_atom(&mut self, atom: Atom) { + self.remove_atom(&atom.ident); + self.atoms.push(atom); + } + + pub fn remove_atom(&mut self, ident: &AtomIdent) { + self.atoms + .iter() + .position(|a| &a.ident == ident) + .map(|p| self.atoms.remove(p)); + } +} + +impl Ilst { + pub fn read_from(reader: &mut R, len: u64) -> Result + where + R: Read, + { + read::parse_ilst(reader, len) + } +} + #[cfg(feature = "mp4_atoms")] impl From for Tag { fn from(input: Ilst) -> Self { @@ -80,11 +109,18 @@ impl From for Ilst { } #[cfg(feature = "mp4_atoms")] +#[derive(Debug, PartialEq)] pub struct Atom { ident: AtomIdent, data: AtomData, } +impl Atom { + pub fn new(ident: AtomIdent, data: AtomData) -> Self { + Self { ident, data } + } +} + #[derive(Eq, PartialEq, Debug)] pub enum AtomIdent { /// A four byte identifier @@ -109,6 +145,7 @@ pub enum AtomIdent { } #[cfg(feature = "mp4_atoms")] +#[derive(Debug, PartialEq)] /// The data of an atom /// /// NOTES: diff --git a/src/logic/mp4/ilst/read.rs b/src/logic/mp4/ilst/read.rs index b5a6bddc..a14771fb 100644 --- a/src/logic/mp4/ilst/read.rs +++ b/src/logic/mp4/ilst/read.rs @@ -10,12 +10,12 @@ use std::io::{Cursor, Read, Seek, SeekFrom}; use byteorder::ReadBytesExt; -pub(crate) fn parse_ilst(data: &mut R, len: u64) -> Result> +pub(crate) fn parse_ilst(reader: &mut R, len: u64) -> Result where - R: Read + Seek, + R: Read, { let mut contents = vec![0; len as usize]; - data.read_exact(&mut contents)?; + reader.read_exact(&mut contents)?; let mut cursor = Cursor::new(contents); @@ -71,7 +71,7 @@ where tag.atoms.push(Atom { ident, data }) } - Ok(Some(tag)) + Ok(tag) } fn parse_data(data: &mut R) -> Result diff --git a/src/logic/mp4/moov.rs b/src/logic/mp4/moov.rs index 4cce4619..f5fb4428 100644 --- a/src/logic/mp4/moov.rs +++ b/src/logic/mp4/moov.rs @@ -111,7 +111,7 @@ where #[cfg(feature = "mp4_atoms")] if islt.0 { - return parse_ilst(data, islt.1 - 8); + return parse_ilst(data, islt.1 - 8).map(Some); } Ok(None) diff --git a/src/logic/ogg/tag.rs b/src/logic/ogg/tag.rs index 2f78d398..0cf576cd 100644 --- a/src/logic/ogg/tag.rs +++ b/src/logic/ogg/tag.rs @@ -8,19 +8,67 @@ use crate::types::picture::PictureInformation; use crate::types::tag::{Tag, TagType}; use std::fs::File; +use std::io::Read; -#[derive(Default)] +#[derive(Default, PartialEq, Debug)] /// Vorbis comments pub struct VorbisComments { /// An identifier for the encoding software - pub vendor: String, + pub(crate) vendor: String, /// A collection of key-value pairs - pub items: Vec<(String, String)>, + pub(crate) items: Vec<(String, String)>, /// A collection of all pictures - pub pictures: Vec<(Picture, PictureInformation)>, + pub(crate) pictures: Vec<(Picture, PictureInformation)>, } impl VorbisComments { + pub fn vendor(&self) -> &str { + &self.vendor + } + + pub fn set_vendor(&mut self, vendor: String) { + self.vendor = vendor + } + + pub fn items(&self) -> &[(String, String)] { + &self.items + } + + pub fn get_item(&self, key: &str) -> Option<&str> { + self.items + .iter() + .find(|(k, _)| k == key) + .map(|(_, v)| v.as_str()) + } + + pub fn insert_item(&mut self, key: String, value: String, replace_all: bool) { + if replace_all { + self.items + .iter() + .position(|(k, _)| k == &key) + .map(|p| self.items.remove(p)); + } + + self.items.push((key, value)) + } + + pub fn remove_key(&mut self, key: &str) { + self.items.retain(|(k, _)| k != key); + } +} + +impl VorbisComments { + pub fn read_from(reader: &mut R) -> Result + where + R: Read, + { + let mut tag = Self::default(); + + super::read::read_comments(reader, &mut tag)?; + + Ok(tag) + } + pub fn write_to(&self, file: &mut File) -> Result<()> { Into::::into(self).write_to(file) } diff --git a/tests/tags/assets/test.apev2 b/tests/tags/assets/test.apev2 new file mode 100644 index 00000000..266798c6 Binary files /dev/null and b/tests/tags/assets/test.apev2 differ diff --git a/tests/tags/assets/test.id3v1 b/tests/tags/assets/test.id3v1 new file mode 100644 index 00000000..4ab4388e Binary files /dev/null and b/tests/tags/assets/test.id3v1 differ diff --git a/tests/tags/assets/test.id3v2 b/tests/tags/assets/test.id3v2 new file mode 100644 index 00000000..2ed7ec1c Binary files /dev/null and b/tests/tags/assets/test.id3v2 differ diff --git a/tests/tags/assets/test.ilst b/tests/tags/assets/test.ilst new file mode 100644 index 00000000..b15f126e Binary files /dev/null and b/tests/tags/assets/test.ilst differ diff --git a/tests/tags/assets/test.riff b/tests/tags/assets/test.riff new file mode 100644 index 00000000..c3e600d5 Binary files /dev/null and b/tests/tags/assets/test.riff differ diff --git a/tests/tags/assets/test.vorbis b/tests/tags/assets/test.vorbis new file mode 100644 index 00000000..b0830831 Binary files /dev/null and b/tests/tags/assets/test.vorbis differ diff --git a/tests/tags/main.rs b/tests/tags/main.rs new file mode 100644 index 00000000..0bec210a --- /dev/null +++ b/tests/tags/main.rs @@ -0,0 +1 @@ +mod read; diff --git a/tests/tags/read.rs b/tests/tags/read.rs new file mode 100644 index 00000000..7290d6ca --- /dev/null +++ b/tests/tags/read.rs @@ -0,0 +1,268 @@ +use lofty::ape::{ApeItem, ApeTag}; +use lofty::id3::v1::Id3v1Tag; +use lofty::id3::v2::{Frame, FrameFlags, FrameValue, Id3v2Tag, LanguageFrame, TextEncoding}; +use lofty::iff::RiffInfoList; +use lofty::mp4::{Atom, AtomData, AtomIdent, Ilst}; +use lofty::ogg::VorbisComments; +use lofty::ItemValue; + +const APE: [u8; 209] = *include_bytes!("assets/test.apev2"); +const ID3V1: [u8; 128] = *include_bytes!("assets/test.id3v1"); +const ID3V2: [u8; 1168] = *include_bytes!("assets/test.id3v2"); +const ILST: [u8; 1024] = *include_bytes!("assets/test.ilst"); +const RIFF_INFO: [u8; 100] = *include_bytes!("assets/test.riff"); +const VORBIS_COMMENTS: [u8; 152] = *include_bytes!("assets/test.vorbis"); + +#[test] +fn read_ape() { + let mut expected_tag = ApeTag::default(); + + let title_item = ApeItem::new( + String::from("TITLE"), + ItemValue::Text(String::from("Foo title")), + ) + .unwrap(); + + let artist_item = ApeItem::new( + String::from("ARTIST"), + ItemValue::Text(String::from("Bar artist")), + ) + .unwrap(); + + let album_item = ApeItem::new( + String::from("ALBUM"), + ItemValue::Text(String::from("Baz album")), + ) + .unwrap(); + + let comment_item = ApeItem::new( + String::from("COMMENT"), + ItemValue::Text(String::from("Qux comment")), + ) + .unwrap(); + + let year_item = + ApeItem::new(String::from("YEAR"), ItemValue::Text(String::from("1984"))).unwrap(); + + let track_number_item = + ApeItem::new(String::from("TRACK"), ItemValue::Text(String::from("1"))).unwrap(); + + let genre_item = ApeItem::new( + String::from("GENRE"), + ItemValue::Text(String::from("Classical")), + ) + .unwrap(); + + expected_tag.push_item(title_item); + expected_tag.push_item(artist_item); + expected_tag.push_item(album_item); + expected_tag.push_item(comment_item); + expected_tag.push_item(year_item); + expected_tag.push_item(track_number_item); + expected_tag.push_item(genre_item); + + let parsed_tag = ApeTag::read_from(&mut std::io::Cursor::new(APE)).unwrap(); + + assert_eq!(expected_tag, parsed_tag); +} + +#[test] +fn read_id3v1() { + let expected_tag = Id3v1Tag { + title: Some(String::from("Foo title")), + artist: Some(String::from("Bar artist")), + album: Some(String::from("Baz album")), + year: Some(String::from("1984")), + comment: Some(String::from("Qux comment")), + track_number: Some(1), + genre: Some(32), + }; + + let parsed_tag = Id3v1Tag::read_from(ID3V1); + + assert_eq!(expected_tag, parsed_tag); +} + +#[test] +fn read_id3v2() { + let mut expected_tag = Id3v2Tag::default(); + + let encoding = TextEncoding::Latin1; + let flags = FrameFlags::default(); + + expected_tag.insert( + Frame::new( + "TPE1", + FrameValue::Text { + encoding, + value: String::from("Bar artist"), + }, + flags, + ) + .unwrap(), + ); + + expected_tag.insert( + Frame::new( + "TIT2", + FrameValue::Text { + encoding, + value: String::from("Foo title"), + }, + flags, + ) + .unwrap(), + ); + + expected_tag.insert( + Frame::new( + "TALB", + FrameValue::Text { + encoding, + value: String::from("Baz album"), + }, + flags, + ) + .unwrap(), + ); + + expected_tag.insert( + Frame::new( + "COMM", + FrameValue::Comment(LanguageFrame { + encoding, + language: String::from("eng"), + description: String::new(), + content: String::from("Qux comment"), + }), + flags, + ) + .unwrap(), + ); + + expected_tag.insert( + Frame::new( + "TDRC", + FrameValue::Text { + encoding, + value: String::from("1984"), + }, + flags, + ) + .unwrap(), + ); + + expected_tag.insert( + Frame::new( + "TRCK", + FrameValue::Text { + encoding, + value: String::from("1"), + }, + flags, + ) + .unwrap(), + ); + + expected_tag.insert( + Frame::new( + "TCON", + FrameValue::Text { + encoding, + value: String::from("Classical"), + }, + flags, + ) + .unwrap(), + ); + + let parsed_tag = Id3v2Tag::read_from(&mut &ID3V2[..]).unwrap(); + + assert_eq!(expected_tag, parsed_tag); +} + +#[test] +fn read_mp4_ilst() { + let mut expected_tag = Ilst::default(); + + // The track number is stored with a code 0, + // meaning the there is no need to indicate the type, + // which is `u64` in this case + expected_tag.insert_atom(Atom::new( + AtomIdent::Fourcc(*b"trkn"), + AtomData::Unknown { + code: 0, + data: vec![0, 0, 0, 1, 0, 0, 0, 0], + }, + )); + + expected_tag.insert_atom(Atom::new( + AtomIdent::Fourcc(*b"\xa9ART"), + AtomData::UTF8(String::from("Bar artist")), + )); + + expected_tag.insert_atom(Atom::new( + AtomIdent::Fourcc(*b"\xa9alb"), + AtomData::UTF8(String::from("Baz album")), + )); + + expected_tag.insert_atom(Atom::new( + AtomIdent::Fourcc(*b"\xa9cmt"), + AtomData::UTF8(String::from("Qux comment")), + )); + + expected_tag.insert_atom(Atom::new( + AtomIdent::Fourcc(*b"\xa9day"), + AtomData::UTF8(String::from("1984")), + )); + + expected_tag.insert_atom(Atom::new( + AtomIdent::Fourcc(*b"\xa9gen"), + AtomData::UTF8(String::from("Classical")), + )); + + expected_tag.insert_atom(Atom::new( + AtomIdent::Fourcc(*b"\xa9nam"), + AtomData::UTF8(String::from("Foo title")), + )); + + let parsed_tag = Ilst::read_from(&mut &ILST[..], ILST.len() as u64).unwrap(); + + assert_eq!(expected_tag, parsed_tag); +} + +#[test] +fn read_riff_info() { + let mut expected_tag = RiffInfoList::default(); + + expected_tag.insert(String::from("IART"), String::from("Bar artist")); + expected_tag.insert(String::from("ICMT"), String::from("Qux comment")); + expected_tag.insert(String::from("ICRD"), String::from("1984")); + expected_tag.insert(String::from("INAM"), String::from("Foo title")); + expected_tag.insert(String::from("IPRD"), String::from("Baz album")); + expected_tag.insert(String::from("IPRT"), String::from("1")); + + let mut reader = std::io::Cursor::new(&RIFF_INFO[..]); + let parsed_tag = RiffInfoList::read_from(&mut reader, (RIFF_INFO.len() - 1) as u64).unwrap(); + + assert_eq!(expected_tag, parsed_tag); +} + +#[test] +fn read_vorbis_comments() { + let mut expected_tag = VorbisComments::default(); + + expected_tag.set_vendor(String::from("Lavf58.76.100")); + + expected_tag.insert_item(String::from("ALBUM"), String::from("Baz album"), false); + expected_tag.insert_item(String::from("ARTIST"), String::from("Bar artist"), false); + expected_tag.insert_item(String::from("COMMENT"), String::from("Qux comment"), false); + expected_tag.insert_item(String::from("DATE"), String::from("1984"), false); + expected_tag.insert_item(String::from("GENRE"), String::from("Classical"), false); + expected_tag.insert_item(String::from("TITLE"), String::from("Foo title"), false); + expected_tag.insert_item(String::from("TRACKNUMBER"), String::from("1"), false); + + let parsed_tag = VorbisComments::read_from(&mut &VORBIS_COMMENTS[..]).unwrap(); + + assert_eq!(expected_tag, parsed_tag); +}