diff --git a/Cargo.toml b/Cargo.toml index c3e98643..d848bee1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ flate2 = { version = "1.0.21", optional = true } # Ogg ogg_pager = "0.1.7" +lazy_static = "1.4.0" paste = "1.0.5" base64 = "0.13.0" byteorder = "1.4.3" diff --git a/src/error.rs b/src/error.rs index 052902f3..53679a91 100644 --- a/src/error.rs +++ b/src/error.rs @@ -36,10 +36,8 @@ pub enum LoftyError { #[cfg(feature = "id3v2")] /// Errors that arise while decoding ID3v2 text TextDecode(&'static str), - #[cfg(feature = "id3v2")] /// Errors that arise while reading/writing ID3v2 tags Id3v2(&'static str), - #[cfg(feature = "id3v2")] /// Arises when an invalid ID3v2 version is found BadId3v2Version(u8, u8), #[cfg(feature = "id3v2")] @@ -51,7 +49,6 @@ pub enum LoftyError { #[cfg(feature = "id3v2")] /// Arises when invalid data is encountered while reading an ID3v2 synchronized text frame BadSyncText, - #[cfg(feature = "mp4_ilst")] /// Arises when an atom contains invalid data BadAtom(&'static str), @@ -116,9 +113,7 @@ impl Display for LoftyError { }, #[cfg(feature = "id3v2")] LoftyError::TextDecode(message) => write!(f, "Text decoding: {}", message), - #[cfg(feature = "id3v2")] LoftyError::Id3v2(message) => write!(f, "ID3v2: {}", message), - #[cfg(feature = "id3v2")] LoftyError::BadId3v2Version(major, minor) => write!( f, "ID3v2: Found an invalid version (v{}.{}), expected any major revision in: (2, 3, \ @@ -134,7 +129,6 @@ impl Display for LoftyError { ), #[cfg(feature = "id3v2")] LoftyError::BadSyncText => write!(f, "ID3v2: Encountered invalid data in SYLT frame"), - #[cfg(feature = "mp4_ilst")] LoftyError::BadAtom(message) => write!(f, "MP4 Atom: {}", message), // Files diff --git a/src/lib.rs b/src/lib.rs index e7e56187..9a1a9e5e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -191,6 +191,7 @@ pub mod id3 { //! * [Frame] pub use { + crate::logic::id3::v2::flags::Id3v2TagFlags, crate::logic::id3::v2::frame::{ EncodedTextFrame, Frame, FrameFlags, FrameID, FrameValue, LanguageFrame, }, @@ -200,7 +201,7 @@ pub mod id3 { crate::logic::id3::v2::items::sync_text::{ SyncTextContentType, SyncTextInformation, SynchronizedText, TimestampFormat, }, - crate::logic::id3::v2::tag::{Id3v2Tag, Id3v2TagFlags}, + crate::logic::id3::v2::tag::Id3v2Tag, crate::logic::id3::v2::util::text_utils::TextEncoding, crate::logic::id3::v2::util::upgrade::{upgrade_v2, upgrade_v3}, crate::logic::id3::v2::Id3v2Version, @@ -241,12 +242,12 @@ pub mod ape { //! It is possible for an `APE` file to contain an `ID3v2` tag. For the sake of data preservation, //! this tag will be read, but **cannot** be written. The only tags allowed by spec are `APEv1/2` and //! `ID3v1`. - #[cfg(feature = "ape")] - pub use crate::logic::ape::tag::item::ApeItem; - #[cfg(feature = "ape")] - pub use crate::logic::ape::tag::ApeTag; pub use crate::logic::ape::{ApeFile, ApeProperties}; - pub use crate::types::picture::APE_PICTURE_TYPES; + #[cfg(feature = "ape")] + pub use crate::{ + logic::ape::tag::{ape_tag::ApeTag, item::ApeItem}, + types::picture::APE_PICTURE_TYPES, + }; } pub mod mp3 { diff --git a/src/logic/ape/mod.rs b/src/logic/ape/mod.rs index 451e2dca..d159c55c 100644 --- a/src/logic/ape/mod.rs +++ b/src/logic/ape/mod.rs @@ -1,18 +1,20 @@ mod constants; mod properties; pub(crate) mod read; -#[cfg(feature = "ape")] pub(crate) mod tag; pub(crate) mod write; +use crate::error::Result; #[cfg(feature = "id3v1")] use crate::logic::id3::v1::tag::Id3v1Tag; +#[cfg(feature = "id3v2")] use crate::logic::id3::v2::tag::Id3v2Tag; use crate::logic::tag_methods; use crate::types::file::{AudioFile, FileType, TaggedFile}; -use crate::{FileProperties, Result, TagType}; - -use tag::ApeTag; +use crate::types::properties::FileProperties; +use crate::types::tag::{Tag, TagType}; +#[cfg(feature = "ape")] +use tag::ape_tag::ApeTag; use std::io::{Read, Seek}; use std::time::Duration; @@ -107,18 +109,21 @@ pub struct ApeFile { } impl From for TaggedFile { + #[allow(clippy::vec_init_then_push)] fn from(input: ApeFile) -> Self { + let mut tags = Vec::>::with_capacity(3); + + #[cfg(feature = "ape")] + tags.push(input.ape_tag.map(Into::into)); + #[cfg(feature = "id3v1")] + tags.push(input.id3v1_tag.map(Into::into)); + #[cfg(feature = "id3v2")] + tags.push(input.id3v2_tag.map(Into::into)); + Self { ty: FileType::APE, properties: FileProperties::from(input.properties), - tags: vec![ - input.ape_tag.map(Into::into), - input.id3v1_tag.map(Into::into), - input.id3v2_tag.map(Into::into), - ] - .into_iter() - .flatten() - .collect(), + tags: tags.into_iter().flatten().collect(), } } } @@ -138,23 +143,16 @@ impl AudioFile for ApeFile { &self.properties } - #[allow(clippy::match_same_arms)] + #[allow(unreachable_code)] fn contains_tag(&self) -> bool { - match self { - #[cfg(feature = "ape")] - ApeFile { - ape_tag: Some(_), .. - } => true, - #[cfg(feature = "id3v1")] - ApeFile { - id3v1_tag: Some(_), .. - } => true, - #[cfg(feature = "id3v2")] - ApeFile { - id3v2_tag: Some(_), .. - } => true, - _ => false, - } + #[cfg(feature = "ape")] + return self.ape_tag.is_some(); + #[cfg(feature = "id3v1")] + return self.id3v1_tag.is_some(); + #[cfg(feature = "id3v2")] + return self.id3v2_tag.is_some(); + + false } fn contains_tag_type(&self, tag_type: &TagType) -> bool { @@ -170,6 +168,13 @@ impl AudioFile for ApeFile { } } -tag_methods! { - ApeFile => ID3v2, id3v2_tag, Id3v2Tag; ID3v1, id3v1_tag, Id3v1Tag; APE, ape_tag, ApeTag +impl ApeFile { + tag_methods! { + #[cfg(feature = "id3v2")]; + ID3v2, id3v2_tag, Id3v2Tag; + #[cfg(feature = "id3v1")]; + ID3v1, id3v1_tag, Id3v1Tag; + #[cfg(feature = "ape")]; + APE, ape_tag, ApeTag + } } diff --git a/src/logic/ape/read.rs b/src/logic/ape/read.rs index 0d17f148..d0792bdd 100644 --- a/src/logic/ape/read.rs +++ b/src/logic/ape/read.rs @@ -1,19 +1,18 @@ use super::constants::APE_PREAMBLE; use super::properties::{properties_gt_3980, properties_lt_3980}; -use super::tag::read::read_ape_tag; +#[cfg(feature = "ape")] +use super::tag::{ape_tag::ApeTag, read::read_ape_tag}; use super::{ApeFile, ApeProperties}; use crate::error::{LoftyError, Result}; +use crate::logic::ape::tag::read_ape_header; #[cfg(feature = "id3v1")] use crate::logic::id3::v1::tag::Id3v1Tag; -#[cfg(any(feature = "id3v2", feature = "id3v1"))] -use crate::logic::id3::{find_id3v1, find_lyrics3v2}; #[cfg(feature = "id3v2")] -use {crate::logic::id3::v2::find_id3v2, crate::logic::id3::v2::read::parse_id3v2}; +use crate::logic::id3::v2::{read::parse_id3v2, tag::Id3v2Tag}; +use crate::logic::id3::{find_id3v1, find_id3v2, find_lyrics3v2}; use std::io::{Read, Seek, SeekFrom}; -use crate::id3::v2::Id3v2Tag; -use crate::logic::ape::tag::ApeTag; use byteorder::{LittleEndian, ReadBytesExt}; fn read_properties(data: &mut R, stream_len: u64, file_length: u64) -> Result @@ -43,22 +42,29 @@ where let mut stream_len = end - start; + #[cfg(feature = "id3v2")] let mut id3v2_tag: Option = None; + #[cfg(feature = "id3v1")] let mut id3v1_tag: Option = None; + #[cfg(feature = "ape")] let mut ape_tag: Option = None; // ID3v2 tags are unsupported in APE files, but still possible - if let Some(id3v2_read) = find_id3v2(data, true)? { - stream_len -= id3v2_read.len() as u64; + if let (Some(header), Some(content)) = find_id3v2(data, true)? { + stream_len -= u64::from(header.size); - let id3v2 = parse_id3v2(&mut &*id3v2_read)?; - - // Skip over the footer - if id3v2.flags().footer { - data.seek(SeekFrom::Current(10))?; + // Exclude the footer + if header.flags.footer { + stream_len -= 10; } - id3v2_tag = Some(id3v2) + #[cfg(feature = "id3v2")] + { + let reader = &mut &*content; + + let id3v2 = parse_id3v2(reader, header)?; + id3v2_tag = Some(id3v2) + } } let mut found_mac = false; @@ -89,10 +95,17 @@ where return Err(LoftyError::Ape("Found incomplete APE tag")); } - let (ape, size) = read_ape_tag(data, false)?; - stream_len -= u64::from(size); + let ape_header = read_ape_header(data, false)?; + stream_len -= u64::from(ape_header.size); - ape_tag = Some(ape) + #[cfg(feature = "ape")] + { + let ape = read_ape_tag(data, ape_header)?; + ape_tag = Some(ape) + } + + #[cfg(not(feature = "ape"))] + data.seek(SeekFrom::Current(ape_header.size as i64))?; }, _ => { return Err(LoftyError::Ape( @@ -111,7 +124,10 @@ where if found_id3v1 { stream_len -= 128; - id3v1_tag = id3v1; + #[cfg(feature = "id3v1")] + { + id3v1_tag = id3v1; + } } // Next, check for a Lyrics3v2 tag, and skip over it, as it's no use to us @@ -132,10 +148,17 @@ where data.read_exact(&mut ape_preamble)?; if &ape_preamble == APE_PREAMBLE { - let (ape, size) = read_ape_tag(data, true)?; + let ape_header = read_ape_header(data, true)?; + stream_len -= u64::from(ape_header.size); - stream_len -= u64::from(size); - ape_tag = Some(ape) + #[cfg(feature = "ape")] + { + let ape = read_ape_tag(data, ape_header)?; + ape_tag = Some(ape) + } + + #[cfg(not(feature = "ape"))] + data.seek(SeekFrom::Current(ape_header.size as i64))?; } let file_length = data.seek(SeekFrom::Current(0))?; diff --git a/src/logic/ape/tag/ape_tag.rs b/src/logic/ape/tag/ape_tag.rs new file mode 100644 index 00000000..1fe77df7 --- /dev/null +++ b/src/logic/ape/tag/ape_tag.rs @@ -0,0 +1,351 @@ +use crate::error::Result; +use crate::logic::ape::tag::item::{ApeItem, ApeItemRef}; +use crate::types::item::{ItemKey, ItemValue, TagItem}; +use crate::types::tag::{Accessor, Tag, TagType}; + +use std::convert::TryInto; +use std::fs::File; + +macro_rules! impl_accessor { + ($($name:ident, $($key:literal)|+;)+) => { + paste::paste! { + impl Accessor for ApeTag { + $( + fn $name(&self) -> Option<&str> { + $( + if let Some(i) = self.get_key($key) { + if let ItemValue::Text(val) = i.value() { + return Some(val) + } + } + )+ + + None + } + + fn [](&mut self, value: String) { + self.insert(ApeItem { + read_only: false, + key: String::from(crate::types::item::first_key!($($key)|*)), + value: ItemValue::Text(value) + }) + } + + fn [](&mut self) { + $( + self.remove_key($key); + )+ + } + )+ + } + } + } +} + +#[derive(Default, Debug, PartialEq, Clone)] +/// An `APE` tag +/// +/// ## Supported file types +/// +/// * [`FileType::APE`](crate::FileType::APE) +/// * [`FileType::MP3`](crate::FileType::MP3) +/// +/// ## Item storage +/// +/// `APE` isn't a very strict format. An [`ApeItem`] only restricted by its name, meaning it can use +/// a normal [`ItemValue`](crate::ItemValue) unlike other formats. +/// +/// Pictures are stored as [`ItemValue::Binary`](crate::ItemValue::Binary), and can be converted with +/// [`Picture::from_ape_bytes`](crate::Picture::from_ape_bytes). For the appropriate item keys, see +/// [APE_PICTURE_TYPES](crate::ape::APE_PICTURE_TYPES). +/// +/// ## Conversions +/// +/// ### From `Tag` +/// +/// When converting pictures, any of type [`PictureType::Undefined`](crate::PictureType::Undefined) will be discarded. +/// For items, see [ApeItem::new]. +pub struct ApeTag { + /// Whether or not to mark the tag as read only + pub read_only: bool, + pub(super) items: Vec, +} + +impl_accessor!( + artist, "Artist"; + title, "Title"; + album, "Album"; + album_artist, "Album Artist" | "ALBUMARTST"; + genre, "GENRE"; +); + +impl ApeTag { + /// Get an [`ApeItem`] by key + /// + /// NOTE: While `APE` items are supposed to be case-sensitive, + /// this rule is rarely followed, so this will ignore case when searching. + pub fn get_key(&self, key: &str) -> Option<&ApeItem> { + self.items + .iter() + .find(|i| i.key().eq_ignore_ascii_case(key)) + } + + /// Insert an [`ApeItem`] + /// + /// This will remove any item with the same key prior to insertion + pub fn insert(&mut self, value: ApeItem) { + self.remove_key(value.key()); + self.items.push(value); + } + + /// Remove an [`ApeItem`] by key + /// + /// NOTE: Like [`ApeTag::get_key`], this is not case-sensitive + pub fn remove_key(&mut self, key: &str) { + self.items + .iter() + .position(|i| i.key().eq_ignore_ascii_case(key)) + .map(|p| self.items.remove(p)); + } + + /// Returns all of the tag's items + pub fn items(&self) -> &[ApeItem] { + &self.items + } +} + +impl ApeTag { + /// Write an `APE` tag to a file + /// + /// # Errors + /// + /// * Attempting to write the tag to a format that does not support it + /// * An existing tag has an invalid size + pub fn write_to(&self, file: &mut File) -> Result<()> { + Into::::into(self).write_to(file) + } +} + +impl From for Tag { + fn from(input: ApeTag) -> Self { + fn split_pair( + content: &str, + tag: &mut Tag, + current_key: ItemKey, + total_key: ItemKey, + ) -> Option<()> { + let mut split = content.splitn(2, '/'); + let current = split.next()?.to_string(); + tag.insert_item_unchecked(TagItem::new(current_key, ItemValue::Text(current))); + + if let Some(total) = split.next() { + tag.insert_item_unchecked(TagItem::new( + total_key, + ItemValue::Text(total.to_string()), + )) + } + + Some(()) + } + + let mut tag = Tag::new(TagType::Ape); + + for item in input.items { + let item_key = ItemKey::from_key(TagType::Ape, item.key()); + + // The text pairs need some special treatment + match (item_key, item.value()) { + (ItemKey::TrackNumber | ItemKey::TrackTotal, ItemValue::Text(val)) + if split_pair(val, &mut tag, ItemKey::TrackNumber, ItemKey::TrackTotal) + .is_some() => + { + continue + }, + (ItemKey::DiscNumber | ItemKey::DiscTotal, ItemValue::Text(val)) + if split_pair(val, &mut tag, ItemKey::DiscNumber, ItemKey::DiscTotal) + .is_some() => + { + continue + }, + (k, _) => tag.insert_item_unchecked(TagItem::new(k, item.value)), + } + } + + tag + } +} + +impl From for ApeTag { + fn from(input: Tag) -> Self { + let mut ape_tag = Self::default(); + + for item in input.items { + if let Ok(ape_item) = item.try_into() { + ape_tag.insert(ape_item) + } + } + + for pic in input.pictures { + if let Some(key) = pic.pic_type.as_ape_key() { + if let Ok(item) = + ApeItem::new(key.to_string(), ItemValue::Binary(pic.as_ape_bytes())) + { + ape_tag.insert(item) + } + } + } + + ape_tag + } +} + +pub(in crate::logic) struct ApeTagRef<'a> { + pub(crate) read_only: bool, + pub(super) items: Box> + 'a>, +} + +impl<'a> ApeTagRef<'a> { + pub(crate) fn write_to(&mut self, file: &mut File) -> Result<()> { + super::write::write_to(file, self) + } +} + +impl<'a> Into> for &'a Tag { + fn into(self) -> ApeTagRef<'a> { + ApeTagRef { + read_only: false, + items: Box::new(self.items.iter().filter_map(|i| { + i.key().map_key(TagType::Ape, true).map(|key| ApeItemRef { + read_only: false, + key, + value: (&i.item_value).into(), + }) + })), + } + } +} + +impl<'a> Into> for &'a ApeTag { + fn into(self) -> ApeTagRef<'a> { + ApeTagRef { + read_only: self.read_only, + items: Box::new(self.items.iter().map(Into::into)), + } + } +} + +#[cfg(test)] +mod tests { + use crate::ape::{ApeItem, ApeTag}; + use crate::{ItemValue, Tag, TagType}; + + use crate::logic::ape::tag::read_ape_header; + use std::io::{Cursor, Read}; + + #[test] + #[allow(clippy::similar_names)] + fn parse_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.insert(title_item); + expected_tag.insert(artist_item); + expected_tag.insert(album_item); + expected_tag.insert(comment_item); + expected_tag.insert(year_item); + expected_tag.insert(track_number_item); + expected_tag.insert(genre_item); + + let mut tag = Vec::new(); + std::fs::File::open("tests/tags/assets/test.apev2") + .unwrap() + .read_to_end(&mut tag) + .unwrap(); + + let mut reader = Cursor::new(tag); + + let header = read_ape_header(&mut reader, false).unwrap(); + let parsed_tag = crate::logic::ape::tag::read::read_ape_tag(&mut reader, header).unwrap(); + + assert_eq!(expected_tag.items().len(), parsed_tag.items().len()); + + for item in expected_tag.items() { + assert!(parsed_tag.items().contains(item)) + } + } + + #[test] + #[allow(clippy::similar_names)] + fn ape_to_tag() { + let mut tag_bytes = Vec::new(); + std::fs::File::open("tests/tags/assets/test.apev2") + .unwrap() + .read_to_end(&mut tag_bytes) + .unwrap(); + + let mut reader = Cursor::new(tag_bytes); + + let header = read_ape_header(&mut reader, false).unwrap(); + let ape = crate::logic::ape::tag::read::read_ape_tag(&mut reader, header).unwrap(); + + let tag: Tag = ape.into(); + + crate::logic::test_utils::verify_tag(&tag, true, true); + } + + #[test] + fn tag_to_ape() { + fn verify_key(tag: &ApeTag, key: &str, expected_val: &str) { + assert_eq!( + tag.get_key(key).map(ApeItem::value), + Some(&ItemValue::Text(String::from(expected_val))) + ); + } + + let tag = crate::logic::test_utils::create_tag(TagType::Ape); + + let ape_tag: ApeTag = tag.into(); + + verify_key(&ape_tag, "Title", "Foo title"); + verify_key(&ape_tag, "Artist", "Bar artist"); + verify_key(&ape_tag, "Album", "Baz album"); + verify_key(&ape_tag, "Comment", "Qux comment"); + verify_key(&ape_tag, "Track", "1"); + verify_key(&ape_tag, "Genre", "Classical"); + } +} diff --git a/src/logic/ape/tag/mod.rs b/src/logic/ape/tag/mod.rs index 9e84095c..c0f53e1a 100644 --- a/src/logic/ape/tag/mod.rs +++ b/src/logic/ape/tag/mod.rs @@ -1,313 +1,55 @@ +#[cfg(feature = "ape")] +pub(crate) mod ape_tag; +#[cfg(feature = "ape")] pub(crate) mod item; +#[cfg(feature = "ape")] pub(in crate::logic) mod read; +#[cfg(feature = "ape")] pub(in crate::logic) mod write; -use crate::error::Result; -use crate::logic::ape::tag::item::{ApeItem, ApeItemRef}; -use crate::types::item::{ItemKey, ItemValue, TagItem}; -use crate::types::tag::{Accessor, Tag, TagType}; +use crate::error::{LoftyError, Result}; -use std::convert::TryInto; -use std::fs::File; +use std::io::{Read, Seek, SeekFrom}; +use std::ops::Neg; -macro_rules! impl_accessor { - ($($name:ident, $($key:literal)|+;)+) => { - paste::paste! { - impl Accessor for ApeTag { - $( - fn $name(&self) -> Option<&str> { - $( - if let Some(i) = self.get_key($key) { - if let ItemValue::Text(val) = i.value() { - return Some(val) - } - } - )+ +use byteorder::{LittleEndian, ReadBytesExt}; - None - } - - fn [](&mut self, value: String) { - self.insert(ApeItem { - read_only: false, - key: String::from(crate::types::item::first_key!($($key)|*)), - value: ItemValue::Text(value) - }) - } - - fn [](&mut self) { - $( - self.remove_key($key); - )+ - } - )+ - } - } - } +#[derive(Copy, Clone)] +pub(crate) struct ApeHeader { + pub(crate) size: u32, + pub(crate) item_count: u32, } -#[derive(Default, Debug, PartialEq, Clone)] -/// An `APE` tag -/// -/// ## Supported file types -/// -/// * [`FileType::APE`](crate::FileType::APE) -/// * [`FileType::MP3`](crate::FileType::MP3) -/// -/// ## Item storage -/// -/// `APE` isn't a very strict format. An [`ApeItem`] only restricted by its name, meaning it can use -/// a normal [`ItemValue`](crate::ItemValue) unlike other formats. -/// -/// Pictures are stored as [`ItemValue::Binary`](crate::ItemValue::Binary), and can be converted with -/// [`Picture::from_ape_bytes`](crate::Picture::from_ape_bytes). For the appropriate item keys, see -/// [APE_PICTURE_TYPES](crate::ape::APE_PICTURE_TYPES). -/// -/// ## Conversions -/// -/// ### From `Tag` -/// -/// When converting pictures, any of type [`PictureType::Undefined`](crate::PictureType::Undefined) will be discarded. -/// For items, see [ApeItem::new]. -pub struct ApeTag { - /// Whether or not to mark the tag as read only - pub read_only: bool, - pub(super) items: Vec, -} - -impl_accessor!( - artist, "Artist"; - title, "Title"; - album, "Album"; - album_artist, "Album Artist" | "ALBUMARTST"; - genre, "GENRE"; -); - -impl ApeTag { - /// Get an [`ApeItem`] by key - /// - /// NOTE: While `APE` items are supposed to be case-sensitive, - /// this rule is rarely followed, so this will ignore case when searching. - pub fn get_key(&self, key: &str) -> Option<&ApeItem> { - self.items - .iter() - .find(|i| i.key().eq_ignore_ascii_case(key)) - } - - /// Insert an [`ApeItem`] - /// - /// This will remove any item with the same key prior to insertion - pub fn insert(&mut self, value: ApeItem) { - self.remove_key(value.key()); - self.items.push(value); - } - - /// Remove an [`ApeItem`] by key - /// - /// NOTE: Like [`ApeTag::get_key`], this is not case-sensitive - pub fn remove_key(&mut self, key: &str) { - self.items - .iter() - .position(|i| i.key().eq_ignore_ascii_case(key)) - .map(|p| self.items.remove(p)); - } - - /// Returns all of the tag's items - pub fn items(&self) -> &[ApeItem] { - &self.items - } -} - -impl ApeTag { - /// Write an `APE` tag to a file - /// - /// # Errors - /// - /// * Attempting to write the tag to a format that does not support it - /// * An existing tag has an invalid size - pub fn write_to(&self, file: &mut File) -> Result<()> { - Into::::into(self).write_to(file) - } -} - -impl From for Tag { - fn from(input: ApeTag) -> Self { - let mut tag = Tag::new(TagType::Ape); - - for item in input.items { - let item = TagItem::new(ItemKey::from_key(TagType::Ape, &*item.key), item.value); - - tag.insert_item_unchecked(item) - } - - tag - } -} - -impl From for ApeTag { - fn from(input: Tag) -> Self { - let mut ape_tag = Self::default(); - - for item in input.items { - if let Ok(ape_item) = item.try_into() { - ape_tag.insert(ape_item) - } - } - - for pic in input.pictures { - if let Some(key) = pic.pic_type.as_ape_key() { - if let Ok(item) = - ApeItem::new(key.to_string(), ItemValue::Binary(pic.as_ape_bytes())) - { - ape_tag.insert(item) - } - } - } - - ape_tag - } -} - -pub(in crate::logic) struct ApeTagRef<'a> { - read_only: bool, - pub(super) items: Box> + 'a>, -} - -impl<'a> ApeTagRef<'a> { - pub(crate) fn write_to(&mut self, file: &mut File) -> Result<()> { - write::write_to(file, self) - } -} - -impl<'a> Into> for &'a Tag { - fn into(self) -> ApeTagRef<'a> { - ApeTagRef { - read_only: false, - items: Box::new(self.items.iter().filter_map(|i| { - i.key().map_key(TagType::Ape, true).map(|key| ApeItemRef { - read_only: false, - key, - value: (&i.item_value).into(), - }) - })), - } - } -} - -impl<'a> Into> for &'a ApeTag { - fn into(self) -> ApeTagRef<'a> { - ApeTagRef { - read_only: self.read_only, - items: Box::new(self.items.iter().map(Into::into)), - } - } -} - -#[cfg(test)] -mod tests { - use crate::ape::{ApeItem, ApeTag}; - use crate::{ItemValue, Tag, TagType}; - - use std::io::{Cursor, Read}; - - #[test] - fn parse_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.insert(title_item); - expected_tag.insert(artist_item); - expected_tag.insert(album_item); - expected_tag.insert(comment_item); - expected_tag.insert(year_item); - expected_tag.insert(track_number_item); - expected_tag.insert(genre_item); - - let mut tag = Vec::new(); - std::fs::File::open("tests/tags/assets/test.apev2") - .unwrap() - .read_to_end(&mut tag) - .unwrap(); - - let mut reader = Cursor::new(tag); - let parsed_tag = super::read::read_ape_tag(&mut reader, false).unwrap().0; - - assert_eq!(expected_tag.items().len(), parsed_tag.items().len()); - - for item in expected_tag.items() { - assert!(parsed_tag.items().contains(item)) - } - } - - #[test] - fn ape_to_tag() { - let mut tag_bytes = Vec::new(); - std::fs::File::open("tests/tags/assets/test.apev2") - .unwrap() - .read_to_end(&mut tag_bytes) - .unwrap(); - - let mut reader = Cursor::new(tag_bytes); - let ape = super::read::read_ape_tag(&mut reader, false).unwrap().0; - - let tag: Tag = ape.into(); - - crate::logic::test_utils::verify_tag(&tag, true, true); - } - - #[test] - fn tag_to_ape() { - fn verify_key(tag: &ApeTag, key: &str, expected_val: &str) { - assert_eq!( - tag.get_key(key).map(ApeItem::value), - Some(&ItemValue::Text(String::from(expected_val))) - ); - } - - let tag = crate::logic::test_utils::create_tag(TagType::Ape); - - let ape_tag: ApeTag = tag.into(); - - verify_key(&ape_tag, "Title", "Foo title"); - verify_key(&ape_tag, "Artist", "Bar artist"); - verify_key(&ape_tag, "Album", "Baz album"); - verify_key(&ape_tag, "Comment", "Qux comment"); - verify_key(&ape_tag, "Track", "1"); - verify_key(&ape_tag, "Genre", "Classical"); - } +pub(crate) fn read_ape_header(data: &mut R, footer: bool) -> Result +where + R: Read + Seek, +{ + let version = data.read_u32::()?; + + let mut size = data.read_u32::()?; + + if size < 32 { + // If the size is < 32, something went wrong during encoding + // The size includes the footer and all items + return Err(LoftyError::Ape("Tag has an invalid size (< 32)")); + } + + let item_count = data.read_u32::()?; + + if footer { + // No point in reading the rest of the footer, just seek back to the end of the header + data.seek(SeekFrom::Current(i64::from(size - 12).neg()))?; + } else { + // There are 12 bytes remaining in the header + // Flags (4) + // Reserved (8) + data.seek(SeekFrom::Current(12))?; + } + + // Version 1 doesn't include a header + if version == 2000 { + size += 32 + } + + Ok(ApeHeader { size, item_count }) } diff --git a/src/logic/ape/tag/read.rs b/src/logic/ape/tag/read.rs index 2b3fb0a4..767727b4 100644 --- a/src/logic/ape/tag/read.rs +++ b/src/logic/ape/tag/read.rs @@ -1,42 +1,19 @@ -use super::{ApeItem, ApeTag}; +use super::{ape_tag::ApeTag, item::ApeItem, ApeHeader}; use crate::error::{LoftyError, Result}; use crate::logic::ape::constants::INVALID_KEYS; use crate::types::item::ItemValue; use std::io::{Read, Seek, SeekFrom}; -use std::ops::Neg; use byteorder::{LittleEndian, ReadBytesExt}; -pub(crate) fn read_ape_tag(data: &mut R, footer: bool) -> Result<(ApeTag, u32)> +pub(crate) fn read_ape_tag(data: &mut R, header: ApeHeader) -> Result where R: Read + Seek, { - let version = data.read_u32::()?; - - let mut size = data.read_u32::()?; - - if size < 32 { - // If the size is < 32, something went wrong during encoding - // The size includes the footer and all items - return Err(LoftyError::Ape("Tag has an invalid size (< 32)")); - } - - let item_count = data.read_u32::()?; - - if footer { - // No point in reading the rest of the footer, just seek back to the end of the header - data.seek(SeekFrom::Current(i64::from(size - 12).neg()))?; - } else { - // There are 12 bytes remaining in the header - // Flags (4) - // Reserved (8) - data.seek(SeekFrom::Current(12))?; - } - let mut tag = ApeTag::default(); - for _ in 0..item_count { + for _ in 0..header.item_count { let value_size = data.read_u32::()?; if value_size == 0 { @@ -60,10 +37,6 @@ where return Err(LoftyError::Ape("Tag item contains an illegal key")); } - if key.chars().any(|c| !c.is_ascii()) { - return Err(LoftyError::Ape("Tag item contains a non ASCII key")); - } - let read_only = (flags & 1) == 1; let item_type = (flags & 6) >> 1; @@ -91,13 +64,8 @@ where tag.insert(item); } - // Version 1 doesn't include a header - if version == 2000 { - size += 32 - } - // Skip over footer data.seek(SeekFrom::Current(32))?; - Ok((tag, size)) + Ok(tag) } diff --git a/src/logic/ape/tag/write.rs b/src/logic/ape/tag/write.rs index bc1d5eaa..8c9b7a77 100644 --- a/src/logic/ape/tag/write.rs +++ b/src/logic/ape/tag/write.rs @@ -1,9 +1,8 @@ use super::read::read_ape_tag; use crate::error::{LoftyError, Result}; use crate::logic::ape::constants::APE_PREAMBLE; -use crate::logic::ape::tag::ApeTagRef; -use crate::logic::id3::v2::find_id3v2; -use crate::logic::id3::{find_id3v1, find_lyrics3v2}; +use crate::logic::ape::tag::ape_tag::ApeTagRef; +use crate::logic::id3::{find_id3v1, find_id3v2, find_lyrics3v2}; use crate::probe::Probe; use crate::types::file::FileType; use crate::types::item::ItemValueRef; @@ -11,6 +10,7 @@ use crate::types::item::ItemValueRef; use std::fs::File; use std::io::{Cursor, Read, Seek, SeekFrom, Write}; +use crate::logic::ape::tag::read_ape_header; use byteorder::{LittleEndian, WriteBytesExt}; #[allow(clippy::shadow_unrelated)] @@ -41,7 +41,11 @@ pub(in crate::logic) fn write_to(data: &mut File, tag: &mut ApeTagRef) -> Result let start = data.seek(SeekFrom::Current(-8))?; data.seek(SeekFrom::Current(8))?; - let (mut existing, size) = read_ape_tag(data, false)?; + + let header = read_ape_header(data, false)?; + let size = header.size; + + let mut existing = read_ape_tag(data, header)?; // Only keep metadata around that's marked read only existing.items.retain(|i| i.read_only); @@ -73,7 +77,10 @@ pub(in crate::logic) fn write_to(data: &mut File, tag: &mut ApeTagRef) -> Result if &ape_preamble == APE_PREAMBLE { let start = data.seek(SeekFrom::Current(0))? as usize + 24; - let (mut existing, size) = read_ape_tag(data, true)?; + let header = read_ape_header(data, true)?; + let size = header.size; + + let mut existing = read_ape_tag(data, header)?; existing.items.retain(|i| i.read_only); diff --git a/src/logic/ape/write.rs b/src/logic/ape/write.rs index 1798ecb9..a4cfe476 100644 --- a/src/logic/ape/write.rs +++ b/src/logic/ape/write.rs @@ -1,13 +1,18 @@ use crate::error::{LoftyError, Result}; -use crate::logic::ape::tag::ApeTagRef; +#[cfg(feature = "ape")] +use crate::logic::ape::tag::ape_tag::ApeTagRef; +#[cfg(feature = "id3v1")] use crate::logic::id3::v1::tag::Id3v1TagRef; +#[allow(unused_imports)] use crate::types::tag::{Tag, TagType}; use std::fs::File; pub(in crate::logic) fn write_to(data: &mut File, tag: &Tag) -> Result<()> { match tag.tag_type() { + #[cfg(feature = "ape")] TagType::Ape => Into::::into(tag).write_to(data), + #[cfg(feature = "id3v1")] TagType::Id3v1 => Into::::into(tag).write_to(data), _ => Err(LoftyError::UnsupportedTag), } diff --git a/src/logic/id3/mod.rs b/src/logic/id3/mod.rs index c7638cb7..f1495118 100644 --- a/src/logic/id3/mod.rs +++ b/src/logic/id3/mod.rs @@ -1,31 +1,13 @@ #[cfg(feature = "id3v1")] pub(crate) mod v1; - -#[cfg(feature = "id3v2")] pub(crate) mod v2; -use crate::{LoftyError, Result}; +use crate::error::{LoftyError, Result}; +use v2::{read_id3v2_header, Id3v2Header}; use std::io::{Read, Seek, SeekFrom}; use std::ops::Neg; -// https://github.com/polyfloyd/rust-id3/blob/e142ec656bf70a8153f6e5b34a37f26df144c3c1/src/stream/unsynch.rs#L18-L20 -pub(crate) fn unsynch_u32(n: u32) -> u32 { - n & 0xFF | (n & 0xFF00) >> 1 | (n & 0xFF_0000) >> 2 | (n & 0xFF00_0000) >> 3 -} - -// https://github.com/polyfloyd/rust-id3/blob/e142ec656bf70a8153f6e5b34a37f26df144c3c1/src/stream/unsynch.rs#L9-L15 -pub(crate) fn synch_u32(n: u32) -> Result { - if n > 0x1000_0000 { - return Err(LoftyError::TooMuchData); - } - - let mut x: u32 = n & 0x7F | (n & 0xFFFF_FF80) << 1; - x = x & 0x7FFF | (x & 0xFFFF_8000) << 1; - x = x & 0x7F_FFFF | (x & 0xFF80_0000) << 1; - Ok(x) -} - pub(crate) fn find_lyrics3v2(data: &mut R) -> Result<(bool, u32)> where R: Read + Seek, @@ -114,3 +96,55 @@ where Ok((exists, None)) } + +#[cfg(feature = "id3v2")] +pub(crate) fn find_id3v2( + data: &mut R, + read: bool, +) -> Result<(Option, Option>)> +where + R: Read + Seek, +{ + let mut header = None; + let mut id3v2 = None; + + if let Ok(id3v2_header) = read_id3v2_header(data) { + if read { + let mut tag = vec![0; id3v2_header.size as usize]; + data.read_exact(&mut tag)?; + + id3v2 = Some(tag) + } else { + data.seek(SeekFrom::Current(i64::from(id3v2_header.size)))?; + } + + if id3v2_header.flags.footer { + data.seek(SeekFrom::Current(10))?; + } + + header = Some(id3v2_header); + } else { + data.seek(SeekFrom::Current(-10))?; + } + + Ok((header, id3v2)) +} + +#[cfg(not(feature = "id3v2"))] +pub(crate) fn find_id3v2(data: &mut R, _read: bool) -> Result<(Option, Option<()>)> +where + R: Read + Seek, +{ + if let Ok(id3v2_header) = read_id3v2_header(data) { + data.seek(SeekFrom::Current(id3v2_header.size as i64))?; + + if id3v2_header.flags.footer { + data.seek(SeekFrom::Current(10))?; + } + + Ok((Some(id3v2_header), Some(()))) + } else { + data.seek(SeekFrom::Current(-10))?; + Ok((None, None)) + } +} diff --git a/src/logic/id3/v2/flags.rs b/src/logic/id3/v2/flags.rs new file mode 100644 index 00000000..3f364613 --- /dev/null +++ b/src/logic/id3/v2/flags.rs @@ -0,0 +1,23 @@ +#[cfg(feature = "id3v2_restrictions")] +use super::items::restrictions::TagRestrictions; + +#[derive(Default, Copy, Clone, Debug, PartialEq)] +#[allow(clippy::struct_excessive_bools)] +/// Flags that apply to the entire tag +pub struct Id3v2TagFlags { + /// Whether or not all frames are unsynchronised. See [`FrameFlags::unsynchronisation`](crate::id3::v2::FrameFlags::unsynchronisation) + pub unsynchronisation: bool, + /// Indicates if the tag is in an experimental stage + pub experimental: bool, + /// Indicates that the tag includes a footer + pub footer: bool, + /// Whether or not to include a CRC-32 in the extended header + /// + /// This is calculated if the tag is written + pub crc: bool, + #[cfg(feature = "id3v2_restrictions")] + /// Restrictions on the tag, written in the extended header + /// + /// In addition to being setting this flag, all restrictions must be provided. See [`TagRestrictions`] + pub restrictions: (bool, TagRestrictions), +} diff --git a/src/logic/id3/v2/frame/header.rs b/src/logic/id3/v2/frame/header.rs index cf5d0195..c3304349 100644 --- a/src/logic/id3/v2/frame/header.rs +++ b/src/logic/id3/v2/frame/header.rs @@ -52,7 +52,7 @@ where let id_str = std::str::from_utf8(&frame_header[..4]).map_err(|_| LoftyError::BadFrameID)?; let (id, size) = if synchsafe { - let size = crate::logic::id3::unsynch_u32(u32::from_be_bytes([ + let size = crate::logic::id3::v2::unsynch_u32(u32::from_be_bytes([ frame_header[4], frame_header[5], frame_header[6], diff --git a/src/logic/id3/v2/mod.rs b/src/logic/id3/v2/mod.rs index 632ccbb3..26340b96 100644 --- a/src/logic/id3/v2/mod.rs +++ b/src/logic/id3/v2/mod.rs @@ -1,16 +1,25 @@ +pub(crate) mod flags; +#[cfg(feature = "id3v2")] pub(crate) mod frame; +#[cfg(feature = "id3v2")] pub(crate) mod items; +#[cfg(feature = "id3v2")] pub(crate) mod read; +#[cfg(feature = "id3v2")] pub(crate) mod tag; +#[cfg(feature = "id3v2")] pub(crate) mod util; +#[cfg(feature = "id3v2")] pub(in crate::logic) mod write; -use crate::error::Result; -use crate::logic::id3::unsynch_u32; +use crate::error::{LoftyError, Result}; +#[cfg(feature = "id3v2_restrictions")] +use crate::logic::id3::v2::items::restrictions::TagRestrictions; +use flags::Id3v2TagFlags; -use std::io::{Read, Seek, SeekFrom}; +use std::io::Read; -use byteorder::{BigEndian, ByteOrder}; +use byteorder::{BigEndian, ByteOrder, ReadBytesExt}; #[derive(PartialEq, Debug, Clone, Copy)] /// The ID3v2 version @@ -23,29 +32,112 @@ pub enum Id3v2Version { V4, } -pub(crate) fn find_id3v2(data: &mut R, read: bool) -> Result>> +// https://github.com/polyfloyd/rust-id3/blob/e142ec656bf70a8153f6e5b34a37f26df144c3c1/src/stream/unsynch.rs#L18-L20 +pub(crate) fn unsynch_u32(n: u32) -> u32 { + n & 0xFF | (n & 0xFF00) >> 1 | (n & 0xFF_0000) >> 2 | (n & 0xFF00_0000) >> 3 +} + +// https://github.com/polyfloyd/rust-id3/blob/e142ec656bf70a8153f6e5b34a37f26df144c3c1/src/stream/unsynch.rs#L9-L15 +pub(crate) fn synch_u32(n: u32) -> Result { + if n > 0x1000_0000 { + return Err(LoftyError::TooMuchData); + } + + let mut x: u32 = n & 0x7F | (n & 0xFFFF_FF80) << 1; + x = x & 0x7FFF | (x & 0xFFFF_8000) << 1; + x = x & 0x7F_FFFF | (x & 0xFF80_0000) << 1; + Ok(x) +} + +#[derive(Copy, Clone)] +pub(crate) struct Id3v2Header { + pub version: Id3v2Version, + pub flags: Id3v2TagFlags, + pub size: u32, +} + +pub(crate) fn read_id3v2_header(bytes: &mut R) -> Result where - R: Read + Seek, + R: Read, { - let mut id3v2 = None; + let mut header = [0; 10]; + bytes.read_exact(&mut header)?; - let mut id3_header = [0; 10]; - data.read_exact(&mut id3_header)?; + if &header[..3] != b"ID3" { + return Err(LoftyError::FakeTag); + } - data.seek(SeekFrom::Current(-10))?; + // Version is stored as [major, minor], but here we don't care about minor revisions unless there's an error. + let version = match header[3] { + 2 => Id3v2Version::V2, + 3 => Id3v2Version::V3, + 4 => Id3v2Version::V4, + major => return Err(LoftyError::BadId3v2Version(major, header[4])), + }; - if &id3_header[..3] == b"ID3" { - let size = unsynch_u32(BigEndian::read_u32(&id3_header[6..])); + let flags = header[5]; - if read { - let mut tag = vec![0; (size + 10) as usize]; - data.read_exact(&mut tag)?; + // Compression was a flag only used in ID3v2.2 (bit 2). + // At the time the ID3v2.2 specification was written, a compression scheme wasn't decided. + // The spec recommends just ignoring the tag in this case. + if version == Id3v2Version::V2 && flags & 0x40 == 0x40 { + return Err(LoftyError::Id3v2("Encountered a compressed ID3v2.2 tag")); + } - id3v2 = Some(tag) - } else { - data.seek(SeekFrom::Current(i64::from(size + 10)))?; + let mut flags_parsed = Id3v2TagFlags { + unsynchronisation: flags & 0x80 == 0x80, + experimental: (version == Id3v2Version::V4 || version == Id3v2Version::V3) + && flags & 0x20 == 0x20, + footer: (version == Id3v2Version::V4 || version == Id3v2Version::V3) + && flags & 0x10 == 0x10, + crc: false, // Retrieved later if applicable + #[cfg(feature = "id3v2_restrictions")] + restrictions: (false, TagRestrictions::default()), // Retrieved later if applicable + }; + + let extended_header = + (version == Id3v2Version::V4 || version == Id3v2Version::V3) && flags & 0x40 == 0x40; + + if extended_header { + let extended_size = unsynch_u32(bytes.read_u32::()?); + + if extended_size < 6 { + return Err(LoftyError::Id3v2( + "Found an extended header with an invalid size (< 6)", + )); + } + + // Useless byte since there's only 1 byte for flags + let _num_flag_bytes = bytes.read_u8()?; + + let extended_flags = bytes.read_u8()?; + + // The only flags we care about here are the CRC and restrictions + + if extended_flags & 0x20 == 0x20 { + flags_parsed.crc = true; + + // We don't care about the existing CRC (5) or its length byte (1) + let mut crc = [0; 6]; + bytes.read_exact(&mut crc)?; + } + + #[cfg(feature = "id3v2_restrictions")] + if extended_flags & 0x10 == 0x10 { + flags_parsed.restrictions.0 = true; + + // We don't care about the length byte, it is always 1 + let _data_length = bytes.read_u8()?; + + flags_parsed.restrictions.1 = TagRestrictions::from_byte(bytes.read_u8()?); } } - Ok(id3v2) + let size = unsynch_u32(BigEndian::read_u32(&header[6..])); + + Ok(Id3v2Header { + version, + flags: flags_parsed, + size, + }) } diff --git a/src/logic/id3/v2/read.rs b/src/logic/id3/v2/read.rs index 49b5e2df..c73d011d 100644 --- a/src/logic/id3/v2/read.rs +++ b/src/logic/id3/v2/read.rs @@ -1,99 +1,26 @@ use super::frame::Frame; use super::tag::Id3v2Tag; -use super::tag::Id3v2TagFlags; -use crate::error::{LoftyError, Result}; -use crate::logic::id3::unsynch_u32; -#[cfg(feature = "id3v2_restrictions")] -use crate::logic::id3::v2::items::restrictions::TagRestrictions; -use crate::logic::id3::v2::Id3v2Version; +use super::Id3v2Header; +use crate::error::Result; use std::io::Read; -use byteorder::{BigEndian, ReadBytesExt}; - -pub(crate) fn parse_id3v2(bytes: &mut R) -> Result +#[allow(clippy::similar_names)] +pub(crate) fn parse_id3v2(bytes: &mut R, header: Id3v2Header) -> Result where R: Read, { - let mut header = [0; 10]; - bytes.read_exact(&mut header)?; - - if &header[..3] != b"ID3" { - return Err(LoftyError::FakeTag); - } - - // Version is stored as [major, minor], but here we don't care about minor revisions unless there's an error. - let version = match header[3] { - 2 => Id3v2Version::V2, - 3 => Id3v2Version::V3, - 4 => Id3v2Version::V4, - major => return Err(LoftyError::BadId3v2Version(major, header[4])), - }; - - let flags = header[5]; - - // Compression was a flag only used in ID3v2.2 (bit 2). - // At the time the ID3v2.2 specification was written, a compression scheme wasn't decided. - // The spec recommends just ignoring the tag in this case. - if version == Id3v2Version::V2 && flags & 0x40 == 0x40 { - return Err(LoftyError::Id3v2("Encountered a compressed ID3v2.2 tag")); - } - - let mut flags_parsed = Id3v2TagFlags { - unsynchronisation: flags & 0x80 == 0x80, - experimental: (version == Id3v2Version::V4 || version == Id3v2Version::V3) - && flags & 0x20 == 0x20, - footer: (version == Id3v2Version::V4 || version == Id3v2Version::V3) - && flags & 0x10 == 0x10, - crc: false, // Retrieved later if applicable - #[cfg(feature = "id3v2_restrictions")] - restrictions: (false, TagRestrictions::default()), // Retrieved later if applicable - }; - - let extended_header = - (version == Id3v2Version::V4 || version == Id3v2Version::V3) && flags & 0x40 == 0x40; - - if extended_header { - let extended_size = unsynch_u32(bytes.read_u32::()?); - - if extended_size < 6 { - return Err(LoftyError::Id3v2( - "Found an extended header with an invalid size (< 6)", - )); - } - - // Useless byte since there's only 1 byte for flags - let _num_flag_bytes = bytes.read_u8()?; - - let extended_flags = bytes.read_u8()?; - - // The only flags we care about here are the CRC and restrictions - - if extended_flags & 0x20 == 0x20 { - flags_parsed.crc = true; - - // We don't care about the existing CRC (5) or its length byte (1) - let mut crc = [0; 6]; - bytes.read_exact(&mut crc)?; - } - - #[cfg(feature = "id3v2_restrictions")] - if extended_flags & 0x10 == 0x10 { - flags_parsed.restrictions.0 = true; - - // We don't care about the length byte, it is always 1 - let _data_length = bytes.read_u8()?; - - flags_parsed.restrictions.1 = TagRestrictions::from_byte(bytes.read_u8()?); - } - } + let mut tag_bytes = vec![0; header.size as usize]; + bytes.read_exact(&mut tag_bytes)?; let mut tag = Id3v2Tag::default(); - tag.original_version = version; - tag.set_flags(flags_parsed); + tag.original_version = header.version; + tag.set_flags(header.flags); + + let reader = &mut &*tag_bytes; loop { - match Frame::read(bytes, version)? { + match Frame::read(reader, header.version)? { None => break, Some(f) => drop(tag.insert(f)), } diff --git a/src/logic/id3/v2/tag.rs b/src/logic/id3/v2/tag.rs index e1b94dbd..a35db02e 100644 --- a/src/logic/id3/v2/tag.rs +++ b/src/logic/id3/v2/tag.rs @@ -1,21 +1,16 @@ +use super::flags::Id3v2TagFlags; use super::frame::{EncodedTextFrame, FrameFlags, LanguageFrame}; use super::frame::{Frame, FrameID, FrameValue}; -#[cfg(feature = "id3v2_restrictions")] -use super::items::restrictions::TagRestrictions; use super::util::text_utils::TextEncoding; use super::Id3v2Version; -use crate::error::{LoftyError, Result}; +use crate::error::Result; use crate::logic::id3::v2::frame::FrameRef; -use crate::probe::Probe; -use crate::types::file::FileType; use crate::types::item::{ItemKey, ItemValue, TagItem}; use crate::types::tag::{Accessor, Tag, TagType}; use std::convert::TryInto; use std::fs::File; -use byteorder::ByteOrder; - macro_rules! impl_accessor { ($($name:ident, $id:literal;)+) => { paste::paste! { @@ -192,18 +187,6 @@ impl Id3v2Tag { pub fn write_to(&self, file: &mut File) -> Result<()> { Into::::into(self).write_to(file) } - - /// Write the tag to a chunk file - /// - /// NOTE: This is only for chunk files (eg. `WAV` and `AIFF`) - /// - /// # Errors - /// - /// * Attempting to write the tag to a format that does not support it - /// * Attempting to write an encrypted frame without a valid method symbol or data length indicator - pub fn write_to_chunk_file(&self, file: &mut File) -> Result<()> { - Into::::into(self).write_to_chunk_file::(file) - } } impl IntoIterator for Id3v2Tag { @@ -217,10 +200,50 @@ impl IntoIterator for Id3v2Tag { impl From for Tag { fn from(input: Id3v2Tag) -> Self { + fn split_pair( + content: &str, + tag: &mut Tag, + current_key: ItemKey, + total_key: ItemKey, + ) -> Option<()> { + let mut split = content.splitn(2, &['\0', '/'][..]); + let current = split.next()?.to_string(); + tag.insert_item_unchecked(TagItem::new(current_key, ItemValue::Text(current))); + + if let Some(total) = split.next() { + tag.insert_item_unchecked(TagItem::new( + total_key, + ItemValue::Text(total.to_string()), + )) + } + + Some(()) + } + let mut tag = Self::new(TagType::Id3v2); for frame in input.frames { - let item_key = ItemKey::from_key(TagType::Id3v2, frame.id_str()); + let id = frame.id_str(); + + // The text pairs need some special treatment + match (id, frame.content()) { + ("TRCK", FrameValue::Text { value: content, .. }) + if split_pair(content, &mut tag, ItemKey::TrackNumber, ItemKey::TrackTotal) + .is_some() => + { + continue + }, + ("TPOS", FrameValue::Text { value: content, .. }) + if split_pair(content, &mut tag, ItemKey::DiscNumber, ItemKey::DiscTotal) + .is_some() => + { + continue + }, + _ => {}, + } + + let item_key = ItemKey::from_key(TagType::Id3v2, id); + let item_value = match frame.value { FrameValue::Comment(LanguageFrame { content, .. }) | FrameValue::UnSyncText(LanguageFrame { content, .. }) @@ -273,27 +296,6 @@ impl From for Id3v2Tag { } } -#[derive(Default, Copy, Clone, Debug, PartialEq)] -#[allow(clippy::struct_excessive_bools)] -/// Flags that apply to the entire tag -pub struct Id3v2TagFlags { - /// Whether or not all frames are unsynchronised. See [`FrameFlags::unsynchronisation`](crate::id3::v2::FrameFlags::unsynchronisation) - pub unsynchronisation: bool, - /// Indicates if the tag is in an experimental stage - pub experimental: bool, - /// Indicates that the tag includes a footer - pub footer: bool, - /// Whether or not to include a CRC-32 in the extended header - /// - /// This is calculated if the tag is written - pub crc: bool, - #[cfg(feature = "id3v2_restrictions")] - /// Restrictions on the tag, written in the extended header - /// - /// In addition to being setting this flag, all restrictions must be provided. See [`TagRestrictions`] - pub restrictions: (bool, TagRestrictions), -} - pub(crate) struct Id3v2TagRef<'a> { pub(crate) flags: Id3v2TagFlags, pub(crate) frames: Box> + 'a>, @@ -303,20 +305,6 @@ impl<'a> Id3v2TagRef<'a> { pub(in crate::logic) fn write_to(&mut self, file: &mut File) -> Result<()> { super::write::write_id3v2(file, self) } - - pub(in crate::logic) fn write_to_chunk_file( - &mut self, - file: &mut File, - ) -> Result<()> { - let probe = Probe::new(file).guess_file_type()?; - - match probe.file_type() { - Some(ft) if ft == FileType::WAV || ft == FileType::AIFF => {}, - _ => return Err(LoftyError::UnsupportedTag), - } - - super::write::write_id3v2_to_chunk_file::(probe.into_inner(), self) - } } impl<'a> Into> for &'a Tag { @@ -347,9 +335,11 @@ mod tests { use crate::id3::v2::{Frame, FrameFlags, FrameValue, Id3v2Tag, LanguageFrame, TextEncoding}; use crate::{Tag, TagType}; + use crate::logic::id3::v2::read_id3v2_header; use std::io::Read; #[test] + #[allow(clippy::similar_names)] fn parse_id3v2() { let mut expected_tag = Id3v2Tag::default(); @@ -450,12 +440,14 @@ mod tests { let mut reader = std::io::Cursor::new(&tag[..]); - let parsed_tag = crate::logic::id3::v2::read::parse_id3v2(&mut reader).unwrap(); + let header = read_id3v2_header(&mut reader).unwrap(); + let parsed_tag = crate::logic::id3::v2::read::parse_id3v2(&mut reader, header).unwrap(); assert_eq!(expected_tag, parsed_tag); } #[test] + #[allow(clippy::similar_names)] fn id3v2_to_tag() { let mut tag_bytes = Vec::new(); std::fs::File::open("tests/tags/assets/test.id3v2") @@ -465,7 +457,8 @@ mod tests { let mut reader = std::io::Cursor::new(&tag_bytes[..]); - let id3v2 = crate::logic::id3::v2::read::parse_id3v2(&mut reader).unwrap(); + let header = read_id3v2_header(&mut reader).unwrap(); + let id3v2 = crate::logic::id3::v2::read::parse_id3v2(&mut reader, header).unwrap(); let tag: Tag = id3v2.into(); diff --git a/src/logic/id3/v2/util/text_utils.rs b/src/logic/id3/v2/util/text_utils.rs index b6bd12f2..f2f8cecf 100644 --- a/src/logic/id3/v2/util/text_utils.rs +++ b/src/logic/id3/v2/util/text_utils.rs @@ -183,6 +183,7 @@ mod tests { use crate::id3::v2::TextEncoding; use std::io::Cursor; + #[allow(clippy::non_ascii_literal)] const TEST_STRING: &str = "løft¥"; #[test] diff --git a/src/logic/id3/v2/write/frame.rs b/src/logic/id3/v2/write/frame.rs index 3ea91a22..2c27184a 100644 --- a/src/logic/id3/v2/write/frame.rs +++ b/src/logic/id3/v2/write/frame.rs @@ -1,7 +1,7 @@ use crate::error::{LoftyError, Result}; use crate::id3::v2::Id3v2Version; -use crate::logic::id3::synch_u32; use crate::logic::id3::v2::frame::{FrameFlags, FrameRef, FrameValueRef}; +use crate::logic::id3::v2::synch_u32; use std::io::Write; diff --git a/src/logic/id3/v2/write/mod.rs b/src/logic/id3/v2/write/mod.rs index 990cae9d..639b7928 100644 --- a/src/logic/id3/v2/write/mod.rs +++ b/src/logic/id3/v2/write/mod.rs @@ -1,24 +1,30 @@ mod chunk_file; mod frame; -use super::find_id3v2; +use super::Id3v2TagFlags; use crate::error::{LoftyError, Result}; -use crate::logic::id3::synch_u32; -use crate::logic::id3::v2::tag::{Id3v2TagFlags, Id3v2TagRef}; +use crate::logic::id3::v2::tag::Id3v2TagRef; +use crate::logic::id3::{find_id3v2, v2::synch_u32}; use crate::probe::Probe; use crate::types::file::FileType; use std::fs::File; use std::io::{Cursor, Read, Seek, SeekFrom, Write}; -use byteorder::{BigEndian, ByteOrder, WriteBytesExt}; +use byteorder::{BigEndian, ByteOrder, LittleEndian, WriteBytesExt}; #[allow(clippy::shadow_unrelated)] pub(in crate::logic) fn write_id3v2(data: &mut File, tag: &mut Id3v2TagRef) -> Result<()> { let probe = Probe::new(data).guess_file_type()?; match probe.file_type() { - Some(ft) if ft == FileType::APE || ft == FileType::MP3 => {}, + Some(FileType::APE | FileType::MP3) => {}, + Some(FileType::WAV) => { + return write_id3v2_to_chunk_file::(probe.into_inner(), tag) + }, + Some(FileType::AIFF) => { + return write_id3v2_to_chunk_file::(probe.into_inner(), tag) + }, _ => return Err(LoftyError::UnsupportedTag), } diff --git a/src/logic/iff/aiff/mod.rs b/src/logic/iff/aiff/mod.rs index 92b7dd1e..a2f46713 100644 --- a/src/logic/iff/aiff/mod.rs +++ b/src/logic/iff/aiff/mod.rs @@ -5,11 +5,13 @@ pub(crate) mod tag; pub(in crate::logic) mod write; use crate::error::Result; +#[cfg(feature = "id3v2")] use crate::logic::id3::v2::tag::Id3v2Tag; use crate::logic::tag_methods; use crate::types::file::{AudioFile, FileType, TaggedFile}; use crate::types::properties::FileProperties; -use crate::types::tag::TagType; +use crate::types::tag::{Tag, TagType}; +#[cfg(feature = "aiff_text_chunks")] use tag::AiffTextChunks; use std::io::{Read, Seek}; @@ -28,16 +30,17 @@ pub struct AiffFile { impl From for TaggedFile { fn from(input: AiffFile) -> Self { + let mut tags = Vec::>::with_capacity(3); + + #[cfg(feature = "aiff_text_chunks")] + tags.push(input.text_chunks.map(Into::into)); + #[cfg(feature = "id3v2")] + tags.push(input.id3v2_tag.map(Into::into)); + Self { ty: FileType::AIFF, properties: input.properties, - tags: vec![ - input.text_chunks.map(Into::into), - input.id3v2_tag.map(Into::into), - ] - .into_iter() - .flatten() - .collect(), + tags: tags.into_iter().flatten().collect(), } } } @@ -57,19 +60,32 @@ impl AudioFile for AiffFile { &self.properties } + #[allow(unreachable_code)] fn contains_tag(&self) -> bool { - self.id3v2_tag.is_some() || self.text_chunks.is_some() + #[cfg(feature = "id3v2")] + return self.id3v2_tag.is_some(); + #[cfg(feature = "aiff_text_chunks")] + return self.text_chunks.is_some(); + + false } fn contains_tag_type(&self, tag_type: &TagType) -> bool { match tag_type { + #[cfg(feature = "id3v2")] TagType::Id3v2 => self.id3v2_tag.is_some(), + #[cfg(feature = "aiff_text_chunks")] TagType::AiffText => self.text_chunks.is_some(), _ => false, } } } -tag_methods! { - AiffFile => ID3v2, id3v2_tag, Id3v2Tag; Text_Chunks, text_chunks, AiffTextChunks +impl AiffFile { + tag_methods! { + #[cfg(feature = "id3v2")]; + ID3v2, id3v2_tag, Id3v2Tag; + #[cfg(feature = "aiff_text_chunks")]; + Text_Chunks, text_chunks, AiffTextChunks + } } diff --git a/src/logic/iff/aiff/write.rs b/src/logic/iff/aiff/write.rs index a767ea51..db3e9bc4 100644 --- a/src/logic/iff/aiff/write.rs +++ b/src/logic/iff/aiff/write.rs @@ -1,18 +1,19 @@ use crate::error::{LoftyError, Result}; +#[cfg(feature = "id3v2")] use crate::logic::id3::v2::tag::Id3v2TagRef; +#[cfg(feature = "aiff_text_chunks")] use crate::logic::iff::aiff::tag::AiffTextChunksRef; +#[allow(unused_imports)] use crate::types::tag::{Tag, TagType}; use std::fs::File; -use byteorder::BigEndian; - pub(crate) fn write_to(data: &mut File, tag: &Tag) -> Result<()> { match tag.tag_type() { #[cfg(feature = "aiff_text_chunks")] TagType::AiffText => Into::::into(tag).write_to(data), #[cfg(feature = "id3v2")] - TagType::Id3v2 => Into::::into(tag).write_to_chunk_file::(data), + TagType::Id3v2 => Into::::into(tag).write_to(data), _ => Err(LoftyError::UnsupportedTag), } } diff --git a/src/logic/iff/chunk.rs b/src/logic/iff/chunk.rs index 49fa2557..e3c59445 100644 --- a/src/logic/iff/chunk.rs +++ b/src/logic/iff/chunk.rs @@ -1,10 +1,13 @@ use crate::error::Result; +#[cfg(feature = "id3v2")] use crate::logic::id3::v2::read::parse_id3v2; +#[cfg(feature = "id3v2")] use crate::logic::id3::v2::tag::Id3v2Tag; use std::io::{Read, Seek, SeekFrom}; use std::marker::PhantomData; +use crate::logic::id3::v2::read_id3v2_header; use byteorder::{ByteOrder, ReadBytesExt}; pub(in crate::logic) struct Chunks @@ -45,6 +48,8 @@ impl Chunks { Ok(content) } + #[cfg(feature = "id3v2")] + #[allow(clippy::similar_names)] pub fn id3_chunk(&mut self, data: &mut R) -> Result where R: Read + Seek, @@ -52,7 +57,10 @@ impl Chunks { let mut value = vec![0; self.size as usize]; data.read_exact(&mut value)?; - let id3v2 = parse_id3v2(&mut &*value)?; + let reader = &mut &*value; + + let header = read_id3v2_header(reader)?; + let id3v2 = parse_id3v2(reader, header)?; // Skip over the footer if id3v2.flags().footer { @@ -62,6 +70,27 @@ impl Chunks { Ok(id3v2) } + #[cfg(not(feature = "id3v2"))] + #[allow(clippy::similar_names)] + pub fn id3_chunk(&mut self, data: &mut R) -> Result<()> + where + R: Read + Seek, + { + let mut value = vec![0; self.size as usize]; + data.read_exact(&mut value)?; + + let mut reader = &mut &*value; + + let header = read_id3v2_header(reader)?; + + // Skip over the footer + if header.flags.footer { + data.seek(SeekFrom::Current(10))?; + } + + Ok(()) + } + pub fn correct_position(&mut self, data: &mut R) -> Result<()> where R: Read + Seek, diff --git a/src/logic/iff/wav/mod.rs b/src/logic/iff/wav/mod.rs index ae4cc957..034047ff 100644 --- a/src/logic/iff/wav/mod.rs +++ b/src/logic/iff/wav/mod.rs @@ -5,12 +5,14 @@ pub(crate) mod tag; pub(in crate::logic) mod write; use crate::error::Result; +#[cfg(feature = "id3v2")] use crate::logic::id3::v2::tag::Id3v2Tag; use crate::logic::tag_methods; use crate::types::file::{AudioFile, FileType, TaggedFile}; use crate::types::properties::FileProperties; -use crate::types::tag::TagType; +use crate::types::tag::{Tag, TagType}; use properties::WavProperties; +#[cfg(feature = "riff_info_list")] use tag::RiffInfoList; use std::io::{Read, Seek}; @@ -29,16 +31,17 @@ pub struct WavFile { impl From for TaggedFile { fn from(input: WavFile) -> Self { + let mut tags = Vec::>::with_capacity(3); + + #[cfg(feature = "riff_info_list")] + tags.push(input.riff_info.map(Into::into)); + #[cfg(feature = "id3v2")] + tags.push(input.id3v2_tag.map(Into::into)); + Self { ty: FileType::WAV, properties: FileProperties::from(input.properties), - tags: vec![ - input.riff_info.map(Into::into), - input.id3v2_tag.map(Into::into), - ] - .into_iter() - .flatten() - .collect(), + tags: tags.into_iter().flatten().collect(), } } } @@ -58,19 +61,33 @@ impl AudioFile for WavFile { &self.properties } + #[allow(unreachable_code)] fn contains_tag(&self) -> bool { - self.id3v2_tag.is_some() || self.riff_info.is_some() + #[cfg(feature = "id3v2")] + return self.id3v2_tag.is_some(); + + #[cfg(feature = "riff_info_list")] + return self.riff_info.is_some(); + + false } fn contains_tag_type(&self, tag_type: &TagType) -> bool { match tag_type { + #[cfg(feature = "id3v2")] TagType::Id3v2 => self.id3v2_tag.is_some(), + #[cfg(feature = "riff_info_list")] TagType::RiffInfo => self.riff_info.is_some(), _ => false, } } } -tag_methods! { - WavFile => ID3v2, id3v2_tag, Id3v2Tag; RIFF_INFO, riff_info, RiffInfoList +impl WavFile { + tag_methods! { + #[cfg(feature = "id3v2")]; + ID3v2, id3v2_tag, Id3v2Tag; + #[cfg(feature = "riff_info_list")]; + RIFF_INFO, riff_info, RiffInfoList + } } diff --git a/src/logic/iff/wav/read.rs b/src/logic/iff/wav/read.rs index 49eedec4..7919ffe0 100644 --- a/src/logic/iff/wav/read.rs +++ b/src/logic/iff/wav/read.rs @@ -2,6 +2,7 @@ use super::tag::RiffInfoList; use super::WavFile; use crate::error::{LoftyError, Result}; +#[cfg(feature = "id3v2")] use crate::logic::id3::v2::tag::Id3v2Tag; use crate::logic::iff::chunk::Chunks; @@ -39,6 +40,7 @@ where #[cfg(feature = "riff_info_list")] let mut riff_info = RiffInfoList::default(); + #[cfg(feature = "id3v2")] let mut id3v2_tag: Option = None; let mut chunks = Chunks::::new(); @@ -78,11 +80,13 @@ where #[cfg(not(feature = "riff_info_list"))] { - data.seek(SeekFrom::Current(i64::from(size)))?; + data.seek(SeekFrom::Current(i64::from(chunks.size)))?; } }, #[cfg(feature = "id3v2")] b"ID3 " | b"id3 " => id3v2_tag = Some(chunks.id3_chunk(data)?), + #[cfg(not(feature = "id3v2"))] + b"ID3 " | b"id3 " => chunks.id3_chunk(data)?, _ => { data.seek(SeekFrom::Current(i64::from(chunks.size)))?; }, diff --git a/src/logic/iff/wav/write.rs b/src/logic/iff/wav/write.rs index af69e3c7..99f87276 100644 --- a/src/logic/iff/wav/write.rs +++ b/src/logic/iff/wav/write.rs @@ -1,16 +1,19 @@ use crate::error::{LoftyError, Result}; +#[cfg(feature = "id3v2")] use crate::logic::id3::v2::tag::Id3v2TagRef; +#[cfg(feature = "riff_info_list")] use crate::logic::iff::wav::tag::RiffInfoListRef; +#[allow(unused_imports)] use crate::types::tag::{Tag, TagType}; use std::fs::File; -use byteorder::LittleEndian; - pub(crate) fn write_to(data: &mut File, tag: &Tag) -> Result<()> { match tag.tag_type() { + #[cfg(feature = "riff_info_list")] TagType::RiffInfo => Into::::into(tag).write_to(data), - TagType::Id3v2 => Into::::into(tag).write_to_chunk_file::(data), + #[cfg(feature = "id3v2")] + TagType::Id3v2 => Into::::into(tag).write_to(data), _ => Err(LoftyError::UnsupportedTag), } } diff --git a/src/logic/mod.rs b/src/logic/mod.rs index dc399c49..b66e82b1 100644 --- a/src/logic/mod.rs +++ b/src/logic/mod.rs @@ -5,8 +5,10 @@ pub(crate) mod mp3; pub(crate) mod mp4; pub(crate) mod ogg; -use crate::error::Result; +use crate::error::{LoftyError, Result}; +#[cfg(feature = "mp4_ilst")] use crate::logic::mp4::ilst::IlstRef; +#[cfg(feature = "vorbis_comments")] use crate::logic::ogg::tag::VorbisCommentsRef; use crate::types::file::FileType; use crate::types::tag::Tag; @@ -14,37 +16,50 @@ use ogg::constants::{OPUSTAGS, VORBIS_COMMENT_HEAD}; use std::fs::File; +#[allow(unreachable_patterns)] pub(crate) fn write_tag(tag: &Tag, file: &mut File, file_type: FileType) -> Result<()> { match file_type { FileType::AIFF => iff::aiff::write::write_to(file, tag), FileType::APE => ape::write::write_to(file, tag), - FileType::FLAC => { - ogg::flac::write::write_to(file, &mut Into::::into(tag)) - }, + #[cfg(feature = "vorbis_comments")] + FileType::FLAC => ogg::flac::write::write_to(file, &mut Into::::into(tag)), FileType::MP3 => mp3::write::write_to(file, tag), + #[cfg(feature = "mp4_ilst")] FileType::MP4 => mp4::ilst::write::write_to(file, &mut Into::::into(tag)), FileType::Opus => ogg::write::write_to(file, tag, OPUSTAGS), FileType::Vorbis => ogg::write::write_to(file, tag, VORBIS_COMMENT_HEAD), FileType::WAV => iff::wav::write::write_to(file, tag), + _ => Err(LoftyError::UnsupportedTag), } } macro_rules! tag_methods { - ($impl_for:ident => $($display_name:tt, $name:ident, $ty:ty);*) => { - impl $impl_for { - paste::paste! { - $( - #[doc = "Gets the " $display_name "tag if it exists"] - pub fn $name(&self) -> Option<&$ty> { - self.$name.as_ref() - } + ($( + $(#[$attr:meta])?; + $display_name:tt, + $name:ident, + $ty:ty);* + ) => { + paste::paste! { + $( + $(#[$attr])? + #[doc = "Gets the " $display_name "tag if it exists"] + pub fn $name(&self) -> Option<&$ty> { + self.$name.as_ref() + } - #[doc = "Sets the " $display_name] - pub fn [](&mut self, tag: $ty) { - self.$name = Some(tag) - } - )* - } + $(#[$attr])? + #[doc = "Sets the " $display_name] + pub fn [](&mut self, tag: $ty) { + self.$name = Some(tag) + } + + $(#[$attr])? + #[doc = "Removes the " $display_name] + pub fn [](&mut self) { + self.$name = None + } + )* } } } diff --git a/src/logic/mp3/mod.rs b/src/logic/mp3/mod.rs index 0f8ade94..183b4cf4 100644 --- a/src/logic/mp3/mod.rs +++ b/src/logic/mp3/mod.rs @@ -3,12 +3,17 @@ pub(crate) mod header; pub(crate) mod read; pub(in crate::logic) mod write; -use crate::logic::ape::tag::ApeTag; +use crate::error::Result; +#[cfg(feature = "ape")] +use crate::logic::ape::tag::ape_tag::ApeTag; +#[cfg(feature = "id3v1")] use crate::logic::id3::v1::tag::Id3v1Tag; +#[cfg(feature = "id3v2")] use crate::logic::id3::v2::tag::Id3v2Tag; use crate::logic::tag_methods; use crate::types::file::{AudioFile, FileType, TaggedFile}; -use crate::{FileProperties, Result, TagType}; +use crate::types::properties::FileProperties; +use crate::types::tag::{Tag, TagType}; use header::{ChannelMode, Layer, MpegVersion}; use std::io::{Read, Seek}; @@ -120,18 +125,21 @@ pub struct Mp3File { } impl From for TaggedFile { + #[allow(clippy::vec_init_then_push)] fn from(input: Mp3File) -> Self { + let mut tags = Vec::>::with_capacity(3); + + #[cfg(feature = "id3v2")] + tags.push(input.id3v2_tag.map(Into::into)); + #[cfg(feature = "id3v1")] + tags.push(input.id3v1_tag.map(Into::into)); + #[cfg(feature = "ape")] + tags.push(input.ape_tag.map(Into::into)); + Self { ty: FileType::MP3, properties: FileProperties::from(input.properties), - tags: vec![ - input.id3v2_tag.map(Into::into), - input.id3v1_tag.map(Into::into), - input.ape_tag.map(Into::into), - ] - .into_iter() - .flatten() - .collect(), + tags: tags.into_iter().flatten().collect(), } } } @@ -150,20 +158,38 @@ impl AudioFile for Mp3File { &self.properties } + #[allow(unreachable_code)] fn contains_tag(&self) -> bool { - self.id3v2_tag.is_some() || self.id3v1_tag.is_some() || self.ape_tag.is_some() + #[cfg(feature = "id3v2")] + return self.id3v2_tag.is_some(); + #[cfg(feature = "id3v1")] + return self.id3v1_tag.is_some(); + #[cfg(feature = "ape")] + return self.ape_tag.is_some(); + + false } fn contains_tag_type(&self, tag_type: &TagType) -> bool { match tag_type { + #[cfg(feature = "ape")] TagType::Ape => self.ape_tag.is_some(), + #[cfg(feature = "id3v2")] TagType::Id3v2 => self.id3v2_tag.is_some(), + #[cfg(feature = "id3v1")] TagType::Id3v1 => self.id3v1_tag.is_some(), _ => false, } } } -tag_methods! { - Mp3File => ID3v2, id3v2_tag, Id3v2Tag; ID3v1, id3v1_tag, Id3v1Tag; APE, ape_tag, ApeTag +impl Mp3File { + tag_methods! { + #[cfg(feature = "id3v2")]; + ID3v2, id3v2_tag, Id3v2Tag; + #[cfg(feature = "id3v1")]; + ID3v1, id3v1_tag, Id3v1Tag; + #[cfg(feature = "ape")]; + APE, ape_tag, ApeTag + } } diff --git a/src/logic/mp3/read.rs b/src/logic/mp3/read.rs index 444a173d..385e25e4 100644 --- a/src/logic/mp3/read.rs +++ b/src/logic/mp3/read.rs @@ -1,16 +1,21 @@ use super::header::{verify_frame_sync, Header, XingHeader}; use super::{Mp3File, Mp3Properties}; use crate::error::{LoftyError, Result}; +#[cfg(feature = "id3v2")] use crate::id3::v2::Id3v2Tag; -use crate::logic::ape::tag::ApeTag; -use crate::logic::id3::unsynch_u32; +#[cfg(feature = "ape")] +use crate::logic::ape::tag::ape_tag::ApeTag; +use crate::logic::ape::tag::read_ape_header; +#[cfg(feature = "id3v1")] use crate::logic::id3::v1::tag::Id3v1Tag; +#[cfg(feature = "id3v2")] use crate::logic::id3::v2::read::parse_id3v2; +use crate::logic::id3::v2::read_id3v2_header; use std::io::{Read, Seek, SeekFrom}; use std::time::Duration; -use byteorder::{BigEndian, ByteOrder, ReadBytesExt}; +use byteorder::ReadBytesExt; fn read_properties( first_frame: (Header, u64), @@ -67,8 +72,11 @@ pub(crate) fn read_from(data: &mut R) -> Result where R: Read + Seek, { + #[cfg(feature = "id3v2")] let mut id3v2_tag: Option = None; + #[cfg(feature = "id3v1")] let mut id3v1_tag: Option = None; + #[cfg(feature = "ape")] let mut ape_tag: Option = None; let mut first_mpeg_frame = (None, 0); @@ -99,20 +107,21 @@ where let mut remaining_header = [0; 6]; data.read_exact(&mut remaining_header)?; - let size = (unsynch_u32(BigEndian::read_u32(&remaining_header[2..])) + 10) as usize; - data.seek(SeekFrom::Current(-10))?; + let header = read_id3v2_header( + &mut &*[header.as_slice(), remaining_header.as_slice()].concat(), + )?; + let skip_footer = header.flags.footer; - let mut id3v2_read = vec![0; size]; - data.read_exact(&mut id3v2_read)?; - - let id3v2 = parse_id3v2(&mut &*id3v2_read)?; - - // Skip over the footer - if id3v2.flags().footer { - data.seek(SeekFrom::Current(10))?; + #[cfg(feature = "id3v2")] + { + let id3v2 = parse_id3v2(data, header)?; + id3v2_tag = Some(id3v2); } - id3v2_tag = Some(id3v2); + // Skip over the footer + if skip_footer { + data.seek(SeekFrom::Current(10))?; + } continue; }, @@ -122,7 +131,11 @@ where let mut id3v1_read = [0; 128]; data.read_exact(&mut id3v1_read)?; - id3v1_tag = Some(crate::logic::id3::v1::read::parse_id3v1(id3v1_read)); + #[cfg(feature = "id3v1")] + { + id3v1_tag = Some(crate::logic::id3::v1::read::parse_id3v1(id3v1_read)); + } + continue; }, [b'A', b'P', b'E', b'T'] => { @@ -130,7 +143,21 @@ where data.read_exact(&mut header_remaining)?; if &header_remaining == b"AGEX" { - ape_tag = Some(crate::logic::ape::tag::read::read_ape_tag(data, false)?.0); + let ape_header = read_ape_header(data, false)?; + + #[cfg(not(feature = "ape"))] + { + let size = ape_header.size; + data.seek(SeekFrom::Current(size as i64))?; + } + + #[cfg(feature = "ape")] + { + ape_tag = Some(crate::logic::ape::tag::read::read_ape_tag( + data, ape_header, + )?); + } + continue; } }, diff --git a/src/logic/mp3/write.rs b/src/logic/mp3/write.rs index a8590eef..b2a35991 100644 --- a/src/logic/mp3/write.rs +++ b/src/logic/mp3/write.rs @@ -1,15 +1,22 @@ use crate::error::{LoftyError, Result}; -use crate::logic::ape::tag::ApeTagRef; +#[cfg(feature = "ape")] +use crate::logic::ape::tag::ape_tag::ApeTagRef; +#[cfg(feature = "id3v1")] use crate::logic::id3::v1::tag::Id3v1TagRef; +#[cfg(feature = "id3v2")] use crate::logic::id3::v2::tag::Id3v2TagRef; +#[allow(unused_imports)] use crate::types::tag::{Tag, TagType}; use std::fs::File; pub(crate) fn write_to(data: &mut File, tag: &Tag) -> Result<()> { match tag.tag_type() { + #[cfg(feature = "ape")] TagType::Ape => Into::::into(tag).write_to(data), + #[cfg(feature = "id3v1")] TagType::Id3v1 => Into::::into(tag).write_to(data), + #[cfg(feature = "id3v2")] TagType::Id3v2 => Into::::into(tag).write_to(data), _ => Err(LoftyError::UnsupportedTag), } diff --git a/src/logic/mp4/mod.rs b/src/logic/mp4/mod.rs index 95548164..1be25165 100644 --- a/src/logic/mp4/mod.rs +++ b/src/logic/mp4/mod.rs @@ -1,4 +1,5 @@ mod atom_info; +#[cfg(feature = "mp4_ilst")] pub(crate) mod ilst; mod moov; mod properties; @@ -115,9 +116,15 @@ impl From for TaggedFile { Self { ty: FileType::MP4, properties: FileProperties::from(input.properties), - tags: if let Some(ilst) = input.ilst { - vec![ilst.into()] - } else { + tags: { + #[cfg(feature = "mp4_ilst")] + if let Some(ilst) = input.ilst { + vec![ilst.into()] + } else { + Vec::new() + } + + #[cfg(not(feature = "mp4_ilst"))] Vec::new() }, } @@ -138,12 +145,20 @@ impl AudioFile for Mp4File { &self.properties } + #[allow(unreachable_code)] fn contains_tag(&self) -> bool { - self.ilst.is_some() + #[cfg(feature = "mp4_ilst")] + return self.ilst.is_some(); + + false } + #[allow(unreachable_code)] fn contains_tag_type(&self, tag_type: &TagType) -> bool { - tag_type == &TagType::Mp4Ilst && self.ilst.is_some() + #[cfg(feature = "mp4_ilst")] + return tag_type == &TagType::Mp4Ilst && self.ilst.is_some(); + + false } } @@ -154,6 +169,9 @@ impl Mp4File { } } -tag_methods! { - Mp4File => ilst, ilst, Ilst +impl Mp4File { + tag_methods! { + #[cfg(feature = "mp4_ilst")]; + ilst, ilst, Ilst + } } diff --git a/src/logic/mp4/moov.rs b/src/logic/mp4/moov.rs index 720d8bf6..aaeae574 100644 --- a/src/logic/mp4/moov.rs +++ b/src/logic/mp4/moov.rs @@ -1,17 +1,18 @@ -use super::atom_info::AtomInfo; +use super::atom_info::{AtomIdent, AtomInfo}; #[cfg(feature = "mp4_ilst")] use super::ilst::{read::parse_ilst, Ilst}; use super::read::skip_unneeded; use super::trak::Trak; -use super::AtomIdent; use crate::error::{LoftyError, Result}; use std::io::{Read, Seek}; +#[cfg(feature = "mp4_ilst")] use byteorder::{BigEndian, ReadBytesExt}; pub(crate) struct Moov { pub(crate) traks: Vec, + #[cfg(feature = "mp4_ilst")] // Represents a parsed moov.udta.meta.ilst since we don't need anything else pub(crate) meta: Option, } @@ -44,12 +45,14 @@ impl Moov { R: Read + Seek, { let mut traks = Vec::new(); + #[cfg(feature = "mp4_ilst")] let mut meta = None; while let Ok(atom) = AtomInfo::read(data) { if let AtomIdent::Fourcc(fourcc) = atom.ident { match &fourcc { b"trak" => traks.push(Trak::parse(data, &atom)?), + #[cfg(feature = "mp4_ilst")] b"udta" => { meta = meta_from_udta(data, atom.len - 8)?; }, @@ -62,10 +65,15 @@ impl Moov { skip_unneeded(data, atom.extended, atom.len)? } - Ok(Self { traks, meta }) + Ok(Self { + traks, + #[cfg(feature = "mp4_ilst")] + meta, + }) } } +#[cfg(feature = "mp4_ilst")] fn meta_from_udta(data: &mut R, len: u64) -> Result> where R: Read + Seek, @@ -109,7 +117,6 @@ where skip_unneeded(data, atom.extended, atom.len)?; } - #[cfg(feature = "mp4_ilst")] if islt.0 { return parse_ilst(data, islt.1 - 8).map(Some); } diff --git a/src/logic/mp4/properties.rs b/src/logic/mp4/properties.rs index b5a3612c..15d45781 100644 --- a/src/logic/mp4/properties.rs +++ b/src/logic/mp4/properties.rs @@ -1,8 +1,7 @@ -use super::atom_info::AtomInfo; +use super::atom_info::{AtomIdent, AtomInfo}; use super::read::nested_atom; use super::read::skip_unneeded; use super::trak::Trak; -use super::AtomIdent; use super::{Mp4Codec, Mp4Properties}; use crate::error::{LoftyError, Result}; diff --git a/src/logic/mp4/read.rs b/src/logic/mp4/read.rs index f71107c8..6efc2667 100644 --- a/src/logic/mp4/read.rs +++ b/src/logic/mp4/read.rs @@ -1,9 +1,8 @@ -use super::atom_info::AtomInfo; +use super::atom_info::{AtomIdent, AtomInfo}; use super::moov::Moov; use super::properties::read_properties; use super::Mp4File; use crate::error::{LoftyError, Result}; -use crate::mp4::AtomIdent; use std::io::{Read, Seek, SeekFrom}; @@ -40,6 +39,7 @@ where Ok(Mp4File { ftyp, + #[cfg(feature = "mp4_ilst")] ilst: moov.meta, properties: read_properties(data, &moov.traks, file_length)?, }) diff --git a/src/logic/mp4/trak.rs b/src/logic/mp4/trak.rs index a3338cf5..9b141516 100644 --- a/src/logic/mp4/trak.rs +++ b/src/logic/mp4/trak.rs @@ -1,6 +1,5 @@ -use super::atom_info::AtomInfo; +use super::atom_info::{AtomIdent, AtomInfo}; use super::read::skip_unneeded; -use super::AtomIdent; use crate::error::Result; use std::io::{Read, Seek, SeekFrom}; diff --git a/src/logic/ogg/flac/mod.rs b/src/logic/ogg/flac/mod.rs index b415a71f..0788fa1f 100644 --- a/src/logic/ogg/flac/mod.rs +++ b/src/logic/ogg/flac/mod.rs @@ -57,6 +57,9 @@ impl AudioFile for FlacFile { } } -tag_methods! { - FlacFile => Vorbis_Comments, vorbis_comments, VorbisComments +impl FlacFile { + tag_methods! { + #[cfg(feature = "vorbis_comments")]; + Vorbis_Comments, vorbis_comments, VorbisComments + } } diff --git a/src/types/file.rs b/src/types/file.rs index 71b2598c..8c27fd05 100644 --- a/src/types/file.rs +++ b/src/types/file.rs @@ -44,6 +44,15 @@ pub struct TaggedFile { pub(crate) tags: Vec, } +#[cfg(any( + feature = "id3v1", + feature = "riff_info_list", + feature = "aiff_text_chunks", + feature = "vorbis_comments", + feature = "id3v2", + feature = "mp4_ilst", + feature = "ape" +))] impl TaggedFile { /// Gets the file's "Primary tag", or the one most likely to be used in the target format /// @@ -69,13 +78,20 @@ impl TaggedFile { /// See [`primary_tag`](Self::primary_tag) for an explanation pub fn primary_tag_type(&self) -> TagType { match self.ty { - #[cfg(feature = "id3v2")] + #[cfg(all(not(feature = "id3v2"), feature = "aiff_text_chunks"))] + FileType::AIFF => TagType::AiffText, + #[cfg(all(not(feature = "id3v2"), feature = "riff_info_list"))] + FileType::WAV => TagType::RiffInfo, + #[cfg(all(not(feature = "id3v2"), feature = "id3v1"))] + FileType::MP3 => TagType::Id3v1, + #[cfg(all(not(feature = "id3v2"), not(feature = "id3v1"), feature = "ape"))] + FileType::MP3 => TagType::Ape, FileType::AIFF | FileType::MP3 | FileType::WAV => TagType::Id3v2, - #[cfg(feature = "ape")] + #[cfg(all(not(feature = "ape"), feature = "id3v1"))] + FileType::MP3 => TagType::Id3v1, FileType::APE => TagType::Ape, #[cfg(feature = "vorbis_comments")] FileType::FLAC | FileType::Opus | FileType::Vorbis => TagType::VorbisComments, - #[cfg(feature = "mp4_ilst")] FileType::MP4 => TagType::Mp4Ilst, } } @@ -136,16 +152,6 @@ impl TaggedFile { .map(|pos| self.tags.remove(pos)) } - /// Returns the file's [`FileType`] - pub fn file_type(&self) -> &FileType { - &self.ty - } - - /// Returns a reference to the file's [`FileProperties`] - pub fn properties(&self) -> &FileProperties { - &self.properties - } - /// Attempts to write all tags to a path /// /// # Errors @@ -169,6 +175,18 @@ impl TaggedFile { } } +impl TaggedFile { + /// Returns the file's [`FileType`] + pub fn file_type(&self) -> &FileType { + &self.ty + } + + /// Returns a reference to the file's [`FileProperties`] + pub fn properties(&self) -> &FileProperties { + &self.properties + } +} + #[derive(PartialEq, Copy, Clone, Debug)] #[allow(missing_docs)] /// The type of file read @@ -187,22 +205,25 @@ impl FileType { /// Returns if the target FileType supports a [`TagType`] pub fn supports_tag_type(&self, tag_type: &TagType) -> bool { match self { - FileType::AIFF => tag_type == &TagType::Id3v2 || tag_type == &TagType::AiffText, - FileType::APE => { - tag_type == &TagType::Ape - || tag_type == &TagType::Id3v1 - || tag_type == &TagType::Id3v2 - }, - FileType::MP3 => { - tag_type == &TagType::Id3v2 - || tag_type == &TagType::Ape - || tag_type == &TagType::Id3v1 - }, - FileType::Opus | FileType::FLAC | FileType::Vorbis => { - tag_type == &TagType::VorbisComments + #[cfg(feature = "id3v2")] + FileType::AIFF | FileType::APE | FileType::MP3 | FileType::WAV + if tag_type == &TagType::Id3v2 => + { + true }, + #[cfg(feature = "aiff_text_chunks")] + FileType::AIFF if tag_type == &TagType::AiffText => true, + #[cfg(feature = "id3v1")] + FileType::APE | FileType::MP3 if tag_type == &TagType::Id3v1 => true, + #[cfg(feature = "ape")] + FileType::APE | FileType::MP3 if tag_type == &TagType::Ape => true, + #[cfg(feature = "vorbis_comments")] + FileType::Opus | FileType::FLAC | FileType::Vorbis => tag_type == &TagType::VorbisComments, + #[cfg(feature = "mp4_ilst")] FileType::MP4 => tag_type == &TagType::Mp4Ilst, - FileType::WAV => tag_type == &TagType::Id3v2 || tag_type == &TagType::RiffInfo, + #[cfg(feature = "riff_info_list")] + FileType::WAV => tag_type == &TagType::RiffInfo, + _ => false, } } @@ -262,7 +283,7 @@ impl FileType { } pub(crate) fn from_buffer_inner(buf: &[u8]) -> Result<(Option, u32)> { - use crate::logic::id3::unsynch_u32; + use crate::logic::id3::v2::unsynch_u32; if buf.is_empty() { return Err(LoftyError::EmptyFile); diff --git a/src/types/item.rs b/src/types/item.rs index 01b9c25f..fe8b04b6 100644 --- a/src/types/item.rs +++ b/src/types/item.rs @@ -1,5 +1,6 @@ -use crate::logic::id3::v1::constants::VALID_ITEMKEYS; -use crate::TagType; +use crate::types::tag::TagType; + +use std::collections::HashMap; macro_rules! first_key { ($key:tt $(| $remaining:expr)*) => { @@ -9,16 +10,332 @@ macro_rules! first_key { pub(crate) use first_key; -// This is used to create the ItemKey enum and its to and from key conversions +// This is used to create the key/ItemKey maps // -// First comes the ItemKey variant as an ident (ex. Artist), then a collection of the appropriate mappings. -// Ex. Artist => [TagType::Ape => "Artist"] +// First comes the feature attribute, followed by the name of the map. +// Ex: +// +// #[cfg(feature = "ape")] +// APE_MAP; +// +// This is followed by the key value pairs separated by `=>`, with the key being the +// format-specific key and the value being the appropriate ItemKey variant. +// Ex. "Artist" => Artist // // Some formats have multiple keys that map to the same ItemKey variant, which can be added with '|'. // The standard key(s) **must** come before any popular non-standard keys. // Keys should appear in order of popularity. -macro_rules! item_keys { - ($($variant:ident => [$($($tag_type:pat)|* => $($key:tt)|+),+]),+) => { +macro_rules! gen_map { + ($(#[$meta:meta])? $NAME:ident; $($($key:literal)|+ => $variant:ident),+) => { + $(#[$meta])? + lazy_static::lazy_static! { + static ref $NAME: HashMap<&'static str, ItemKey> = { + let mut map = HashMap::new(); + $( + $( + map.insert($key, ItemKey::$variant); + )+ + )+ + map + }; + } + + $(#[$meta])? + impl $NAME { + pub(crate) fn get_item_key(&self, key: &str) -> Option { + self.iter().find(|(k, _)| k.eq_ignore_ascii_case(key)).map(|(_, v)| v.clone()) + } + + pub(crate) fn get_key(&self, item_key: &ItemKey) -> Option<&str> { + match item_key { + $( + ItemKey::$variant => Some(first_key!($($key)|*)), + )+ + _ => None + } + } + } + } +} + +gen_map!( + #[cfg(feature = "aiff_text_chunks")] + AIFF_TEXT_MAP; + + "NAME" => TrackTitle, + "AUTH" => TrackArtist, + "(c) " => CopyrightMessage +); + +gen_map!( + #[cfg(feature = "ape")] + APE_MAP; + + "Album" => AlbumTitle, + "DiscSubtitle" => SetSubtitle, + "Grouping" => ContentGroup, + "Title" => TrackTitle, + "Subtitle" => TrackSubtitle, + "ALBUMSORT" => AlbumTitleSortOrder, + "ALBUMARTISTSORT" => AlbumArtistSortOrder, + "TITLESORT" => TrackTitleSortOrder, + "ARTISTSORT" => TrackArtistSortOrder, + "Album Artist" | "ALBUMARTIST" => AlbumArtist, + "Artist" => TrackArtist, + "Arranger" => Arranger, + "Writer" => Writer, + "Composer" => Composer, + "Conductor" => Conductor, + "Engineer" => Engineer, + "Lyricist" => Lyricist, + "DjMixer" => MixDj, + "Mixer" => MixEngineer, + "Performer" => Performer, + "Producer" => Producer, + "Label" => Label, + "MixArtist" => Remixer, + "Disc" => DiscNumber, + "Disc" => DiscTotal, + "Track" => TrackNumber, + "Track" => TrackTotal, + "Year" => Year, + "ISRC" => ISRC, + "Barcode" => Barcode, + "CatalogNumber" => CatalogNumber, + "Compilation" => FlagCompilation, + "Media" => OriginalMediaType, + "EncodedBy" => EncodedBy, + "Genre" => Genre, + "Mood" => Mood, + "Copyright" => CopyrightMessage, + "Comment" => Comment, + "language" => Language, + "Script" => Script, + "Lyrics" => Lyrics +); + +gen_map! ( + #[cfg(feature = "id3v2")] + ID3V2_MAP; + + "TALB" => AlbumTitle, + "TSST" => SetSubtitle, + "TIT1" | "GRP1" => ContentGroup, + "TIT2" => TrackTitle, + "TIT3" => TrackSubtitle, + "TOAL" => OriginalAlbumTitle, + "TOPE" => OriginalArtist, + "TOLY" => OriginalLyricist, + "TSOA" => AlbumTitleSortOrder, + "TSO2" => AlbumArtistSortOrder, + "TSOT" => TrackTitleSortOrder, + "TSOP" => TrackArtistSortOrder, + "TSOC" => ComposerSortOrder, + "TPE2" => AlbumArtist, + "TPE1" => TrackArtist, + "TEXT" => Writer, + "TCOM" => Composer, + "TPE3" => Conductor, + "TIPL" => InvolvedPeople, + "TEXT" => Lyricist, + "TMCL" => MusicianCredits, + "IPRO" => Producer, + "TPUB" => Publisher, + "TPUB" => Label, + "TRSN" => InternetRadioStationName, + "TRSO" => InternetRadioStationOwner, + "TPE4" => Remixer, + "TPOS" => DiscNumber, + "TPOS" => DiscTotal, + "TRCK" => TrackNumber, + "TRCK" => TrackTotal, + "POPM" => Popularimeter, + "TDRC" => RecordingDate, + "TDOR" => OriginalReleaseDate, + "TSRC" => ISRC, + "MVNM" => Movement, + "MVIN" => MovementIndex, + "TCMP" => FlagCompilation, + "PCST" => FlagPodcast, + "TFLT" => FileType, + "TOWN" => FileOwner, + "TDTG" => TaggingTime, + "TLEN" => Length, + "TOFN" => OriginalFileName, + "TMED" => OriginalMediaType, + "TENC" => EncodedBy, + "TSSE" => EncoderSoftware, + "TSSE" => EncoderSettings, + "TDEN" => EncodingTime, + "WOAF" => AudioFileURL, + "WOAS" => AudioSourceURL, + "WCOM" => CommercialInformationURL, + "WCOP" => CopyrightURL, + "WOAR" => TrackArtistURL, + "WORS" => RadioStationURL, + "WPAY" => PaymentURL, + "WPUB" => PublisherURL, + "TCON" => Genre, + "TLEY" => InitialKey, + "TMOO" => Mood, + "TBPM" => BPM, + "TCOP" => CopyrightMessage, + "TDES" => PodcastDescription, + "TCAT" => PodcastSeriesCategory, + "WFED" => PodcastURL, + "TDRL" => PodcastReleaseDate, + "TGID" => PodcastGlobalUniqueID, + "TKWD" => PodcastKeywords, + "COMM" => Comment, + "TLAN" => Language, + "USLT" => Lyrics +); + +gen_map! ( + #[cfg(feature = "mp4_ilst")] + ILST_MAP; + + "\u{a9}alb" => AlbumTitle, + "----:com.apple.iTunes:DISCSUBTITLE" => SetSubtitle, + "tvsh" => ShowName, + "\u{a9}grp" => ContentGroup, + "\u{a9}nam" => TrackTitle, + "----:com.apple.iTunes:SUBTITLE" => TrackSubtitle, + "soal" => AlbumTitleSortOrder, + "soaa" => AlbumArtistSortOrder, + "sonm" => TrackTitleSortOrder, + "soar" => TrackArtistSortOrder, + "sosn" => ShowNameSortOrder, + "soco" => ComposerSortOrder, + "aART" => AlbumArtist, + "\u{a9}ART" => TrackArtist, + "\u{a9}wrt" => Composer, + "----:com.apple.iTunes:CONDUCTOR" => Conductor, + "----:com.apple.iTunes:ENGINEER" => Engineer, + "----:com.apple.iTunes:LYRICIST" => Lyricist, + "----:com.apple.iTunes:DJMIXER" => MixDj, + "----:com.apple.iTunes:MIXER" => MixEngineer, + "----:com.apple.iTunes:PRODUCER" => Producer, + "----:com.apple.iTunes:LABEL" => Label, + "----:com.apple.iTunes:REMIXER" => Remixer, + "disk" => DiscNumber, + "disk" => DiscTotal, + "trkn" => TrackNumber, + "trkn" => TrackTotal, + "rate" => LawRating, + "\u{a9}day" => RecordingDate, + "----:com.apple.iTunes:ISRC" => ISRC, + "----:com.apple.iTunes:BARCODE" => Barcode, + "----:com.apple.iTunes:CATALOGNUMBER" => CatalogNumber, + "cpil" => FlagCompilation, + "pcst" => FlagPodcast, + "----:com.apple.iTunes:MEDIA" => OriginalMediaType, + "\u{a9}too" => EncoderSoftware, + "\u{a9}gen" => Genre, + "----:com.apple.iTunes:MOOD" => Mood, + "tmpo" => BPM, + "cprt" => CopyrightMessage, + "----:com.apple.iTunes:LICENSE" => License, + "ldes" => PodcastDescription, + "catg" => PodcastSeriesCategory, + "purl" => PodcastURL, + "egid" => PodcastGlobalUniqueID, + "keyw" => PodcastKeywords, + "\u{a9}cmt" => Comment, + "desc" => Description, + "----:com.apple.iTunes:LANGUAGE" => Language, + "----:com.apple.iTunes:SCRIPT" => Script, + "\u{a9}lyr" => Lyrics +); + +gen_map! ( + #[cfg(feature = "riff_info_list")] + RIFF_INFO_MAP; + + "IPRD" => AlbumTitle, + "INAM" => TrackTitle, + "IART" => TrackArtist, + "IWRI" => Writer, + "IMUS" => Composer, + "IPRO" => Producer, + "IPRT" | "ITRK" => TrackNumber, + "IFRM" => TrackTotal, + "IRTD" => LawRating, + "ICRD" => RecordingDate, + "ISRF" => OriginalMediaType, + "ITCH" => EncodedBy, + "ISFT" => EncoderSoftware, + "IGNR" => Genre, + "ICOP" => CopyrightMessage, + "ICMT" => Comment, + "ILNG" => Language +); + +gen_map!( + #[cfg(feature = "vorbis_comments")] + VORBIS_MAP; + + "ALBUM" => AlbumTitle, + "DISCSUBTITLE" => SetSubtitle, + "GROUPING" => ContentGroup, + "TITLE" => TrackTitle, + "SUBTITLE" => TrackSubtitle, + "ALBUMSORT" => AlbumTitleSortOrder, + "ALBUMARTISTSORT" => AlbumArtistSortOrder, + "TITLESORT" => TrackTitleSortOrder, + "ARTISTSORT" => TrackArtistSortOrder, + "ALBUMARTIST" => AlbumArtist, + "ARTIST" => TrackArtist, + "ARRANGER" => Arranger, + "AUTHOR" | "WRITER" => Writer, + "COMPOSER" => Composer, + "CONDUCTOR" => Conductor, + "ENGINEER" => Engineer, + "LYRICIST" => Lyricist, + "DJMIXER" => MixDj, + "MIXER" => MixEngineer, + "PERFORMER" => Performer, + "PRODUCER" => Producer, + "PUBLISHER" => Publisher, + "LABEL" => Label, + "REMIXER" => Remixer, + "DISCNUMBER" => DiscNumber, + "DISCTOTAL" | "TOTALDISCS" => DiscTotal, + "TRACKNUMBER" => TrackNumber, + "TRACKTOTAL" | "TOTALTRACKS" => TrackTotal, + "DATE" => RecordingDate, + "YEAR" => Year, + "ORIGINALDATE" => OriginalReleaseDate, + "ISRC" => ISRC, + "CATALOGNUMBER" => CatalogNumber, + "COMPILATION" => FlagCompilation, + "MEDIA" => OriginalMediaType, + "ENCODED-BY" => EncodedBy, + "ENCODER" => EncoderSoftware, + "ENCODING" | "ENCODERSETTINGS" => EncoderSettings, + "GENRE" => Genre, + "MOOD" => Mood, + "BPM" => BPM, + "COPYRIGHT" => CopyrightMessage, + "LICENSE" => License, + "COMMENT" => Comment, + "LANGUAGE" => Language, + "SCRIPT" => Script, + "LYRICS" => Lyrics +); + +macro_rules! gen_item_keys { + ( + MAPS => [ + $( + $(#[$feat:meta])? + [$tag_type:pat, $MAP:ident] + ),+ + ]; + KEYS => [ + $($variant:ident),+ $(,)? + ] + ) => { #[derive(PartialEq, Clone, Debug, Eq, Hash)] #[allow(missing_docs)] #[non_exhaustive] @@ -41,372 +358,175 @@ macro_rules! item_keys { pub fn from_key(tag_type: TagType, key: &str) -> Self { match tag_type { $( - $( - $($tag_type)|* if $(key.eq_ignore_ascii_case($key))||* => ItemKey::$variant, - )+ + $(#[$feat])? + $tag_type => $MAP.get_item_key(key).unwrap_or_else(|| Self::Unknown(key.to_string())), )+ - _ => Self::Unknown(key.to_string()), + _ => Self::Unknown(key.to_string()) } } - /// Maps the variant to a format-specific key /// /// Use `allow_unknown` to include [`ItemKey::Unknown`]. It is up to the caller /// to determine if the unknown key actually fits the format's specifications. pub fn map_key(&self, tag_type: TagType, allow_unknown: bool) -> Option<&str> { - match (tag_type, self) { + match tag_type { $( - $( - ($($tag_type)|*, ItemKey::$variant) => Some(first_key!($($key)|*)), - )+ + $(#[$feat])? + $tag_type => if let Some(key) = $MAP.get_key(self) { + return Some(key) + }, )+ - (_, ItemKey::Unknown(unknown)) if allow_unknown => Some(&*unknown), - _ => None, + _ => {} } + + if let ItemKey::Unknown(ref unknown) = self { + if allow_unknown { + return Some(unknown) + } + } + + None } } - }; + } } -item_keys!( - // Titles - AlbumTitle => [ - TagType::Id3v2 => "TALB", TagType::Mp4Ilst => "\u{a9}alb", - TagType::VorbisComments => "ALBUM", TagType::Ape => "Album", - TagType::RiffInfo => "IPRD" - ], - SetSubtitle => [ - TagType::Id3v2 => "TSST", TagType::Mp4Ilst => "----:com.apple.iTunes:DISCSUBTITLE", - TagType::VorbisComments => "DISCSUBTITLE", TagType::Ape => "DiscSubtitle" - ], - ShowName => [ - TagType::Mp4Ilst => "tvsh" - ], - ContentGroup => [ - TagType::Id3v2 => "TIT1" | "GRP1", TagType::Mp4Ilst => "\u{a9}grp", - TagType::VorbisComments => "GROUPING", TagType::Ape => "Grouping" - ], - TrackTitle => [ - TagType::Id3v2 => "TIT2", TagType::Mp4Ilst => "\u{a9}nam", - TagType::VorbisComments => "TITLE", TagType::Ape => "Title", - TagType::RiffInfo => "INAM", TagType::AiffText => "NAME" - ], - TrackSubtitle => [ - TagType::Id3v2 => "TIT3", TagType::Mp4Ilst => "----:com.apple.iTunes:SUBTITLE", - TagType::VorbisComments => "SUBTITLE", TagType::Ape => "Subtitle" - ], +gen_item_keys!( + MAPS => [ + #[cfg(feature = "aiff_text_chunks")] + [TagType::AiffText, AIFF_TEXT_MAP], - // Original names - OriginalAlbumTitle => [ - TagType::Id3v2 => "TOAL" - ], - OriginalArtist => [ - TagType::Id3v2 => "TOPE" - ], - OriginalLyricist => [ - TagType::Id3v2 => "TOLY" - ], + #[cfg(feature = "ape")] + [TagType::Ape, APE_MAP], - // Sorting - AlbumTitleSortOrder => [ - TagType::Id3v2 => "TSOA", TagType::Mp4Ilst => "soal", - TagType::VorbisComments | TagType::Ape => "ALBUMSORT" - ], - AlbumArtistSortOrder => [ - TagType::Id3v2 => "TSO2", TagType::Mp4Ilst => "soaa", - TagType::VorbisComments | TagType::Ape => "ALBUMARTISTSORT" - ], - TrackTitleSortOrder => [ - TagType::Id3v2 => "TSOT", TagType::Mp4Ilst => "sonm", - TagType::VorbisComments | TagType::Ape => "TITLESORT" - ], - TrackArtistSortOrder => [ - TagType::Id3v2 => "TSOP", TagType::Mp4Ilst => "soar", - TagType::VorbisComments | TagType::Ape => "ARTISTSORT" - ], - ShowNameSortOrder => [ - TagType::Mp4Ilst => "sosn" - ], - ComposerSortOrder => [ - TagType::Id3v2 => "TSOC", TagType::Mp4Ilst => "soco" - ], + #[cfg(feature = "id3v2")] + [TagType::Id3v2, ID3V2_MAP], + #[cfg(feature = "mp4_ilst")] + [TagType::Mp4Ilst, ILST_MAP], - // People & Organizations - AlbumArtist => [ - TagType::Id3v2 => "TPE2", TagType::Mp4Ilst => "aART", - TagType::VorbisComments => "ALBUMARTIST", TagType::Ape => "Album Artist" | "ALBUMARTIST" - ], - TrackArtist => [ - TagType::Id3v2 => "TPE1", TagType::Mp4Ilst => "\u{a9}ART", - TagType::VorbisComments => "ARTIST", TagType::Ape => "Artist", - TagType::RiffInfo => "IART", TagType::AiffText => "AUTH" - ], - Arranger => [ - TagType::VorbisComments => "ARRANGER", TagType::Ape => "Arranger" - ], - Writer => [ - TagType::Id3v2 => "TEXT", - TagType::VorbisComments => "AUTHOR" | "WRITER", TagType::Ape => "Writer", - TagType::RiffInfo => "IWRI" - ], - Composer => [ - TagType::Id3v2 => "TCOM", TagType::Mp4Ilst => "\u{a9}wrt", - TagType::VorbisComments => "COMPOSER", TagType::Ape => "Composer", - TagType::RiffInfo => "IMUS" - ], - Conductor => [ - TagType::Id3v2 => "TPE3", TagType::Mp4Ilst => "----:com.apple.iTunes:CONDUCTOR", - TagType::VorbisComments => "CONDUCTOR", TagType::Ape => "Conductor" - ], - Engineer => [ - TagType::Mp4Ilst => "----:com.apple.iTunes:ENGINEER", TagType::VorbisComments => "ENGINEER", - TagType::Ape => "Engineer" - ], - InvolvedPeople => [ - TagType::Id3v2 => "TIPL" - ], - Lyricist => [ - TagType::Id3v2 => "TEXT", TagType::Mp4Ilst => "----:com.apple.iTunes:LYRICIST", - TagType::VorbisComments => "LYRICIST", TagType::Ape => "Lyricist" - ], - MixDj => [ - TagType::Mp4Ilst => "----:com.apple.iTunes:DJMIXER", TagType::VorbisComments => "DJMIXER", - TagType::Ape => "DjMixer" - ], - MixEngineer => [ - TagType::Mp4Ilst => "----:com.apple.iTunes:MIXER", TagType::VorbisComments => "MIXER", - TagType::Ape => "Mixer" - ], - MusicianCredits => [ - TagType::Id3v2 => "TMCL" - ], - Performer => [ - TagType::VorbisComments => "PERFORMER", TagType::Ape => "Performer" - ], - Producer => [ - TagType::Mp4Ilst => "----:com.apple.iTunes:PRODUCER", TagType::VorbisComments => "PRODUCER", - TagType::Ape => "Producer", TagType::RiffInfo => "IPRO" - ], - Publisher => [ - TagType::Id3v2 => "TPUB", TagType::VorbisComments => "PUBLISHER" - ], - Label => [ - TagType::Id3v2 => "TPUB", TagType::Mp4Ilst => "----:com.apple.iTunes:LABEL", - TagType::VorbisComments => "LABEL", TagType::Ape => "Label" - ], - InternetRadioStationName => [ - TagType::Id3v2 => "TRSN" - ], - InternetRadioStationOwner => [ - TagType::Id3v2 => "TRSO" - ], - Remixer => [ - TagType::Id3v2 => "TPE4", TagType::Mp4Ilst => "----:com.apple.iTunes:REMIXER", - TagType::VorbisComments => "REMIXER", TagType::Ape => "MixArtist" - ], + #[cfg(feature = "riff_info_list")] + [TagType::RiffInfo, RIFF_INFO_MAP], - // Counts & Indexes - DiscNumber => [ - TagType::Id3v2 => "TPOS", TagType::Mp4Ilst => "disk", - TagType::VorbisComments => "DISCNUMBER", TagType::Ape => "Disc" - ], - DiscTotal => [ - TagType::Id3v2 => "TPOS", TagType::Mp4Ilst => "disk", - TagType::VorbisComments => "DISCTOTAL" | "TOTALDISCS", TagType::Ape => "Disc" - ], - TrackNumber => [ - TagType::Id3v2 => "TRCK", TagType::Mp4Ilst => "trkn", - TagType::VorbisComments => "TRACKNUMBER", TagType::Ape => "Track", - TagType::RiffInfo => "IPRT" | "ITRK" - ], - TrackTotal => [ - TagType::Id3v2 => "TRCK", TagType::Mp4Ilst => "trkn", - TagType::VorbisComments => "TRACKTOTAL" | "TOTALTRACKS", TagType::Ape => "Track", - TagType::RiffInfo => "IFRM" - ], - Popularimeter => [ - TagType::Id3v2 => "POPM" - ], - LawRating => [ - TagType::Mp4Ilst => "rate", TagType::RiffInfo => "IRTD" - ], + #[cfg(feature = "vorbis_comments")] + [TagType::VorbisComments, VORBIS_MAP] + ]; - // Dates - RecordingDate => [ - TagType::Id3v2 => "TDRC", TagType::Mp4Ilst => "\u{a9}day", - TagType::VorbisComments => "DATE", TagType::RiffInfo => "ICRD" - ], - Year => [ - TagType::Id3v2 => "TDRC", TagType::VorbisComments => "DATE" | "YEAR", - TagType::Ape => "Year" - ], - OriginalReleaseDate => [ - TagType::Id3v2 => "TDOR", TagType::VorbisComments => "ORIGINALDATE" - ], + KEYS => [ + // Titles + AlbumTitle, + SetSubtitle, + ShowName, + ContentGroup, + TrackTitle, + TrackSubtitle, - // Identifiers - ISRC => [ - TagType::Id3v2 => "TSRC", TagType::Mp4Ilst => "----:com.apple.iTunes:ISRC", - TagType::VorbisComments => "ISRC", TagType::Ape => "ISRC" - ], - Barcode => [ - TagType::Mp4Ilst => "----:com.apple.iTunes:BARCODE", TagType::Ape => "Barcode" - ], - CatalogNumber => [ - TagType::Mp4Ilst => "----:com.apple.iTunes:CATALOGNUMBER", TagType::VorbisComments => "CATALOGNUMBER", - TagType::Ape => "CatalogNumber" - ], - Movement => [ - TagType::Id3v2 => "MVNM" - ], - MovementIndex => [ - TagType::Id3v2 => "MVIN" - ], + // Original names + OriginalAlbumTitle, + OriginalArtist, + OriginalLyricist, - // Flags - FlagCompilation => [ - TagType::Id3v2 => "TCMP", TagType::Mp4Ilst => "cpil", - TagType::VorbisComments => "COMPILATION", TagType::Ape => "Compilation" - ], - FlagPodcast => [ - TagType::Id3v2 => "PCST", TagType::Mp4Ilst => "pcst" - ], + // Sorting + AlbumTitleSortOrder, + AlbumArtistSortOrder, + TrackTitleSortOrder, + TrackArtistSortOrder, + ShowNameSortOrder, + ComposerSortOrder, - // File information - FileType => [ - TagType::Id3v2 => "TFLT" - ], - FileOwner => [ - TagType::Id3v2 => "TOWN" - ], - TaggingTime => [ - TagType::Id3v2 => "TDTG" - ], - Length => [ - TagType::Id3v2 => "TLEN" - ], - OriginalFileName => [ - TagType::Id3v2 => "TOFN" - ], - OriginalMediaType => [ - TagType::Id3v2 => "TMED", TagType::Mp4Ilst => "----:com.apple.iTunes:MEDIA", - TagType::VorbisComments => "MEDIA", TagType::Ape => "Media", - TagType::RiffInfo => "ISRF" - ], + // People & Organizations + AlbumArtist, + TrackArtist, + Arranger, + Writer, + Composer, + Conductor, + Engineer, + InvolvedPeople, + Lyricist, + MixDj, + MixEngineer, + MusicianCredits, + Performer, + Producer, + Publisher, + Label, + InternetRadioStationName, + InternetRadioStationOwner, + Remixer, - // Encoder information - EncodedBy => [ - TagType::Id3v2 => "TENC", TagType::VorbisComments => "ENCODED-BY", - TagType::Ape => "EncodedBy", TagType::RiffInfo => "ITCH" - ], - EncoderSoftware => [ - TagType::Id3v2 => "TSSE", TagType::Mp4Ilst => "\u{a9}too", - TagType::VorbisComments => "ENCODER", TagType::RiffInfo => "ISFT" - ], - EncoderSettings => [ - TagType::Id3v2 => "TSSE", TagType::VorbisComments => "ENCODING" | "ENCODERSETTINGS" - ], - EncodingTime => [ - TagType::Id3v2 => "TDEN" - ], + // Counts & Indexes + DiscNumber, + DiscTotal, + TrackNumber, + TrackTotal, + Popularimeter, + LawRating, - // URLs - AudioFileURL => [ - TagType::Id3v2 => "WOAF" - ], - AudioSourceURL => [ - TagType::Id3v2 => "WOAS" - ], - CommercialInformationURL => [ - TagType::Id3v2 => "WCOM" - ], - CopyrightURL => [ - TagType::Id3v2 => "WCOP" - ], - TrackArtistURL => [ - TagType::Id3v2 => "WOAR" - ], - RadioStationURL => [ - TagType::Id3v2 => "WORS" - ], - PaymentURL => [ - TagType::Id3v2 => "WPAY" - ], - PublisherURL => [ - TagType::Id3v2 => "WPUB" - ], + // Dates + RecordingDate, + Year, + OriginalReleaseDate, + // Identifiers + ISRC, + Barcode, + CatalogNumber, + Movement, + MovementIndex, - // Style - Genre => [ - TagType::Id3v2 => "TCON", TagType::Mp4Ilst => "\u{a9}gen", - TagType::VorbisComments => "GENRE", TagType::RiffInfo => "IGNR", - TagType::Ape => "Genre" - ], - InitialKey => [ - TagType::Id3v2 => "TKEY" - ], - Mood => [ - TagType::Id3v2 => "TMOO", TagType::Mp4Ilst => "----:com.apple.iTunes:MOOD", - TagType::VorbisComments => "MOOD", TagType::Ape => "Mood" - ], - BPM => [ - TagType::Id3v2 => "TBPM", TagType::Mp4Ilst => "tmpo", - TagType::VorbisComments => "BPM" - ], + // Flags + FlagCompilation, + FlagPodcast, - // Legal - CopyrightMessage => [ - TagType::Id3v2 => "TCOP", TagType::Mp4Ilst => "cprt", - TagType::VorbisComments => "COPYRIGHT", TagType::Ape => "Copyright", - TagType::RiffInfo => "ICOP", TagType::AiffText => "(c) " - ], - License => [ - TagType::Mp4Ilst => "----:com.apple.iTunes:LICENSE", TagType::VorbisComments => "LICENSE" - ], + // File Information + FileType, + FileOwner, + TaggingTime, + Length, + OriginalFileName, + OriginalMediaType, - // Podcast - PodcastDescription => [ - TagType::Id3v2 => "TDES", TagType::Mp4Ilst => "ldes" - ], - PodcastSeriesCategory => [ - TagType::Id3v2 => "TCAT", TagType::Mp4Ilst => "catg" - ], - PodcastURL => [ - TagType::Id3v2 => "WFED", TagType::Mp4Ilst => "purl" - ], - PodcastReleaseDate => [ - TagType::Id3v2 => "TDRL" - ], - PodcastGlobalUniqueID => [ - TagType::Id3v2 => "TGID", TagType::Mp4Ilst => "egid" - ], - PodcastKeywords => [ - TagType::Id3v2 => "TKWD", TagType::Mp4Ilst => "keyw" - ], + // Encoder information + EncodedBy, + EncoderSoftware, + EncoderSettings, + EncodingTime, - // Miscellaneous - Comment => [ - TagType::Id3v2 => "COMM", TagType::Mp4Ilst => "\u{a9}cmt", - TagType::VorbisComments => "COMMENT", TagType::Ape => "Comment", - TagType::RiffInfo => "ICMT" - ], - Description => [ - TagType::Mp4Ilst => "desc" - ], - Language => [ - TagType::Id3v2 => "TLAN", TagType::Mp4Ilst => "----:com.apple.iTunes:LANGUAGE", - TagType::VorbisComments => "LANGUAGE", TagType::Ape => "language", - TagType::RiffInfo => "ILNG" - ], - Script => [ - TagType::Mp4Ilst => "----:com.apple.iTunes:SCRIPT", TagType::VorbisComments => "SCRIPT", - TagType::Ape => "Script" - ], - Lyrics => [ - TagType::Id3v2 => "USLT", TagType::Mp4Ilst => "\u{a9}lyr", - TagType::VorbisComments => "LYRICS", TagType::Ape => "Lyrics" + // URLs + AudioFileURL, + AudioSourceURL, + CommercialInformationURL, + CopyrightURL, + TrackArtistURL, + RadioStationURL, + PaymentURL, + PublisherURL, + + // Style + Genre, + InitialKey, + Mood, + BPM, + + // Legal + CopyrightMessage, + License, + + // Podcast + PodcastDescription, + PodcastSeriesCategory, + PodcastURL, + PodcastReleaseDate, + PodcastGlobalUniqueID, + PodcastKeywords, + + // Miscellaneous + Comment, + Description, + Language, + Script, + Lyrics, ] ); @@ -485,7 +605,10 @@ impl TagItem { } pub(crate) fn re_map(&self, tag_type: TagType) -> Option<()> { + #[cfg(feature = "id3v1")] if tag_type == TagType::Id3v1 { + use crate::logic::id3::v1::constants::VALID_ITEMKEYS; + return VALID_ITEMKEYS.contains(&self.item_key).then(|| ()); } diff --git a/src/types/picture.rs b/src/types/picture.rs index 1b8c0500..64d9c094 100644 --- a/src/types/picture.rs +++ b/src/types/picture.rs @@ -3,9 +3,12 @@ use crate::{LoftyError, Result}; use {crate::logic::id3::v2::util::text_utils::TextEncoding, crate::logic::id3::v2::Id3v2Version}; use std::borrow::Cow; +#[cfg(feature = "id3v2")] +use std::io::Write; use std::io::{Cursor, Read}; -use std::io::{Seek, SeekFrom, Write}; +use std::io::{Seek, SeekFrom}; +#[cfg(feature = "id3v2")] use byteorder::WriteBytesExt; #[cfg(any(feature = "vorbis_comments", feature = "id3v2",))] use byteorder::{BigEndian, ReadBytesExt}; diff --git a/src/types/tag.rs b/src/types/tag.rs index fdcf4823..c76ef276 100644 --- a/src/types/tag.rs +++ b/src/types/tag.rs @@ -312,25 +312,18 @@ impl Tag { /// The tag's format #[derive(Copy, Clone, Debug, PartialEq)] pub enum TagType { - #[cfg(feature = "ape")] /// This covers both APEv1 and APEv2 as it doesn't matter much Ape, - #[cfg(feature = "id3v1")] /// Represents an ID3v1 tag Id3v1, - #[cfg(feature = "id3v2")] /// This covers all ID3v2 versions since they all get upgraded to ID3v2.4 Id3v2, - #[cfg(feature = "mp4_ilst")] /// Represents an MP4 ILST atom Mp4Ilst, - #[cfg(feature = "vorbis_comments")] /// Represents vorbis comments VorbisComments, - #[cfg(feature = "riff_info_list")] /// Represents a RIFF INFO LIST RiffInfo, - #[cfg(feature = "aiff_text_chunks")] /// Represents AIFF text chunks AiffText, }