Put ID3v2 tag restrictions behind a feature, cleanup

This commit is contained in:
Serial 2021-08-28 20:11:37 -04:00
parent 98ab6d4bce
commit 575d7af692
18 changed files with 137 additions and 103 deletions

View file

@ -18,6 +18,7 @@ filepath = { version = "0.1.1", optional = true } # wav/aiff only supports paths
ogg_pager = { version = "0.1.7", optional = true }
# Mp4
mp4ameta = {version = "0.11.0", optional = true}
simdutf8 = { version = "0.1.3", optional = true }
# Case insensitive keys (APE/FLAC/Opus/Vorbis)
unicase = { version = "2.6.0"}
@ -33,11 +34,12 @@ cfg-if = "1.0.0"
[features]
default = ["mp4_atoms", "vorbis_comments", "ape", "id3v1", "id3v2", "aiff_text_chunks", "riff_info_list", "quick_tag_accessors"]
mp4_atoms = []
mp4_atoms = ["simdutf8"]
vorbis_comments = ["ogg_pager"]
ape = []
id3v1 = []
id3v2 = ["flate2"]
id3v2_restrictions = []
aiff_text_chunks = []
riff_info_list = []
quick_tag_accessors = ["paste"]

View file

@ -16,7 +16,7 @@ pub enum LoftyError {
/// Provided an empty file
#[error("File contains no data")]
EmptyFile,
/// Attempting to write an abnormally large amount of data
/// Attempting to read/write an abnormally large amount of data
#[error("An abnormally large amount of data was provided, and an overflow occurred")]
TooMuchData,
@ -64,6 +64,9 @@ pub enum LoftyError {
/// Arises when a tag is expected (Ex. found an "ID3 " chunk in a WAV file), but isn't found
#[error("Reading: Expected a tag, found invalid data")]
FakeTag,
/// Arises when an atom contains invalid data
#[error("MP4 Atom: {0}")]
BadAtom(&'static str),
/// Errors that arise while reading/writing to WAV files
#[error("Riff: {0}")]
Wav(&'static str),
@ -82,9 +85,12 @@ pub enum LoftyError {
/// Errors that arise while reading/writing to OGG files
#[error("OGG: {0}")]
Ogg(&'static str),
/// Errors that arise while reading/writing to MPEG files
#[error("MPEG: {0}")]
Mpeg(&'static str),
/// Errors that arise while reading/writing to MP3 files
#[error("MP3: {0}")]
Mp3(&'static str),
/// Errors that arise while reading/writing to MP4 files
#[error("MP4: {0}")]
Mp4(&'static str),
/// Errors that arise while reading/writing to APE files
#[error("APE: {0}")]
Ape(&'static str),

View file

@ -88,8 +88,6 @@
//!
//! # Features
//!
//! NOTE: All of these are enabled by default
//!
//! ## QOL
//! * `quick_tag_accessors` - Adds easier getters/setters for string values (Ex. [`Tag::artist`]), adds an extra dependency
//!
@ -104,6 +102,9 @@
//! * `riff_info_list`
//! * `vorbis_comments`
//!
//! ## Utilities
//! * `id3v2_restrictions` - Parses ID3v2 extended headers and exposes flags for fine grained control
//!
//! # Notes on ID3v2
//!
//! See [`id3`](crate::id3) for important warnings and notes on reading tags.
@ -181,6 +182,7 @@ pub mod id3 {
//! The solution is to use [`ItemKey::Id3v2Specific`](crate::ItemKey::Id3v2Specific) alongside [`Id3v2Frame`](crate::id3::Id3v2Frame).
//!
//! NOTE: Unlike the above issue, this one does not require unchecked insertion.
#[cfg(feature = "id3v2_restrictions")]
pub use crate::logic::id3::v2::restrictions::*;
pub use crate::logic::id3::v2::util::encapsulated_object::{
GEOBInformation, GeneralEncapsulatedObject,

View file

@ -50,15 +50,18 @@ impl AudioFile for ApeFile {
}
impl ApeFile {
fn id3v2_tag(&self) -> Option<&Tag> {
/// Returns a reference to the ID3v2 tag if it exists
pub fn id3v2_tag(&self) -> Option<&Tag> {
self.id3v2.as_ref()
}
fn id3v1_tag(&self) -> Option<&Tag> {
/// Returns a reference to the ID3v1 tag if it exists
pub fn id3v1_tag(&self) -> Option<&Tag> {
self.id3v1.as_ref()
}
fn ape_tag(&self) -> Option<&Tag> {
/// Returns a reference to the APEv1/2 tag if it exists
pub fn ape_tag(&self) -> Option<&Tag> {
self.ape.as_ref()
}
}

View file

@ -126,17 +126,17 @@ fn parse_text(id: &str, content: &mut &[u8]) -> Result<TagItem> {
let text = decode_text(content, encoding, false)?.unwrap_or_else(String::new);
Ok(TagItem::new(
ItemKey::Id3v2Specific(Id3v2Frame::Text(id.to_string(), encoding)),
ItemValue::Text(text),
))
let key = ItemKey::from_key(&TagType::Id3v2(Id3v2Version::V4), id)
.unwrap_or_else(|| ItemKey::Id3v2Specific(Id3v2Frame::Text(id.to_string(), encoding)));
Ok(TagItem::new(key, ItemValue::Text(text)))
}
fn parse_link(id: &str, content: &mut &[u8]) -> Result<TagItem> {
let link = decode_text(content, TextEncoding::Latin1, false)?.unwrap_or_else(String::new);
Ok(TagItem::new(
ItemKey::Id3v2Specific(Id3v2Frame::URL(id.to_string())),
ItemValue::Locator(link),
))
let key = ItemKey::from_key(&TagType::Id3v2(Id3v2Version::V4), id)
.unwrap_or_else(|| ItemKey::Id3v2Specific(Id3v2Frame::URL(id.to_string())));
Ok(TagItem::new(key, ItemValue::Locator(link)))
}

View file

@ -9,6 +9,7 @@ use byteorder::{BigEndian, ByteOrder};
mod frame;
pub(crate) mod read;
#[cfg(feature = "id3v2_restrictions")]
pub(crate) mod restrictions;
pub(crate) mod util;

View file

@ -1,7 +1,9 @@
use crate::error::Result;
#[cfg(feature = "id3v2_restrictions")]
use crate::logic::id3::decode_u32;
use crate::logic::id3::v2::frame::content::FrameContent;
use crate::logic::id3::v2::frame::Frame;
#[cfg(feature = "id3v2_restrictions")]
use crate::logic::id3::v2::restrictions::{
ImageSizeRestrictions, TagRestrictions, TagSizeRestrictions, TextSizeRestrictions,
};
@ -11,6 +13,7 @@ use crate::{LoftyError, TagType};
use std::io::Read;
#[cfg(feature = "id3v2_restrictions")]
use byteorder::{BigEndian, ReadBytesExt};
pub(crate) fn parse_id3v2(bytes: &mut &[u8]) -> Result<Tag> {
@ -46,9 +49,11 @@ pub(crate) fn parse_id3v2(bytes: &mut &[u8]) -> Result<Tag> {
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
};
#[cfg(feature = "id3v2_restrictions")]
if flags_parsed.extended_header {
let extended_size = decode_u32(bytes.read_u32::<BigEndian>()?);
@ -79,8 +84,17 @@ pub(crate) fn parse_id3v2(bytes: &mut &[u8]) -> Result<Tag> {
}
}
#[cfg(not(feature = "id3v2_restrictions"))]
let mut tag = Tag::new(TagType::Id3v2(Id3v2Version::V4));
#[cfg(feature = "id3v2_restrictions")]
let mut tag = {
let mut tag = Tag::new(TagType::Id3v2(Id3v2Version::V4));
tag.set_flags(flags_parsed);
tag
};
loop {
match Frame::read(bytes, version)? {
None => break,
@ -97,6 +111,7 @@ pub(crate) fn parse_id3v2(bytes: &mut &[u8]) -> Result<Tag> {
Ok(tag)
}
#[cfg(feature = "id3v2_restrictions")]
fn parse_restrictions(bytes: &mut &[u8]) -> Result<TagRestrictions> {
// We don't care about the length byte
let _data_length = bytes.read_u8()?;
@ -110,7 +125,7 @@ fn parse_restrictions(bytes: &mut &[u8]) -> Result<TagRestrictions> {
restriction_flags & 0x80 == 0x80,
restriction_flags & 0x40 == 0x40,
) {
(false, false) => {},
(false, false) => {}, // default
(false, true) => restrictions.size = TagSizeRestrictions::S_64F_128K,
(true, false) => restrictions.size = TagSizeRestrictions::S_32F_40K,
(true, true) => restrictions.size = TagSizeRestrictions::S_32F_4K,
@ -126,7 +141,7 @@ fn parse_restrictions(bytes: &mut &[u8]) -> Result<TagRestrictions> {
restriction_flags & 0x10 == 0x10,
restriction_flags & 0x08 == 0x08,
) {
(false, false) => {},
(false, false) => {}, // default
(false, true) => restrictions.text_fields_size = TextSizeRestrictions::C_1024,
(true, false) => restrictions.text_fields_size = TextSizeRestrictions::C_128,
(true, true) => restrictions.text_fields_size = TextSizeRestrictions::C_30,
@ -142,7 +157,7 @@ fn parse_restrictions(bytes: &mut &[u8]) -> Result<TagRestrictions> {
restriction_flags & 0x02 == 0x02,
restriction_flags & 0x01 == 0x01,
) {
(false, false) => {},
(false, false) => {}, // default
(false, true) => restrictions.image_size = ImageSizeRestrictions::P_256,
(true, false) => restrictions.image_size = ImageSizeRestrictions::P_64,
(true, true) => restrictions.image_size = ImageSizeRestrictions::P_64_64,

View file

@ -6,7 +6,7 @@ use crate::{ItemKey, ItemValue, Tag, TagItem};
use std::io::{Read, Seek, SeekFrom};
use std::time::Duration;
use byteorder::{BigEndian, LittleEndian, ReadBytesExt};
use byteorder::{BigEndian, ReadBytesExt};
/// An AIFF file
pub struct AiffFile {
@ -45,11 +45,13 @@ impl AudioFile for AiffFile {
}
impl AiffFile {
fn id3v2_tag(&self) -> Option<&Tag> {
/// Returns a reference to the ID3v2 tag if it exists
pub fn id3v2_tag(&self) -> Option<&Tag> {
self.id3v2.as_ref()
}
fn text_chunks(&self) -> Option<&Tag> {
/// Returns a reference to the text chunks tag if it exists
pub fn text_chunks(&self) -> Option<&Tag> {
self.text_chunks.as_ref()
}
}

View file

@ -1,12 +1,9 @@
use crate::types::file::AudioFile;
use crate::{
FileProperties, FileType, ItemKey, ItemValue, LoftyError, Result, Tag, TagItem, TagType,
TaggedFile,
FileProperties, ItemKey, ItemValue, LoftyError, Result, Tag, TagItem, TagType
};
use std::collections::HashMap;
use std::fs::File;
use std::io::{Read, Seek, SeekFrom, Write};
use std::io::{Read, Seek, SeekFrom};
use std::time::Duration;
use crate::logic::id3::v2::read::parse_id3v2;
@ -53,11 +50,13 @@ impl AudioFile for WavFile {
}
impl WavFile {
fn id3v2_tag(&self) -> Option<&Tag> {
/// Returns a reference to the ID3v2 tag if it exists
pub fn id3v2_tag(&self) -> Option<&Tag> {
self.id3v2.as_ref()
}
fn riff_info(&self) -> Option<&Tag> {
/// Returns a reference to the RIFF INFO tag if it exists
pub fn riff_info(&self) -> Option<&Tag> {
self.riff_info.as_ref()
}
}
@ -276,33 +275,6 @@ where
})
}
fn find_info_list<T>(data: &mut T) -> Result<()>
where
T: Read + Seek,
{
loop {
let mut chunk_name = [0; 4];
data.read_exact(&mut chunk_name)?;
if &chunk_name == b"LIST" {
data.seek(SeekFrom::Current(4))?;
let mut list_type = [0; 4];
data.read_exact(&mut list_type)?;
if &list_type == b"INFO" {
data.seek(SeekFrom::Current(-8))?;
return Ok(());
}
data.seek(SeekFrom::Current(-8))?;
}
let size = data.read_u32::<LittleEndian>()?;
data.seek(SeekFrom::Current(i64::from(size)))?;
}
}
cfg_if::cfg_if! {
if #[cfg(feature = "format-riff")] {
pub(crate) fn write_to(data: &mut File, metadata: HashMap<String, String>) -> Result<()> {

View file

@ -1,5 +1,6 @@
pub(crate) mod ape;
pub(crate) mod iff;
pub(crate) mod mp4;
pub(crate) mod mpeg;
pub(crate) mod ogg;

View file

@ -49,21 +49,21 @@ impl Header {
0 => MpegVersion::V2_5,
2 => MpegVersion::V2,
3 => MpegVersion::V1,
_ => return Err(LoftyError::Mpeg("Frame header has an invalid version")),
_ => return Err(LoftyError::Mp3("Frame header has an invalid version")),
};
let layer = match (header >> 17) & 0b11 {
1 => Layer::Layer3,
2 => Layer::Layer2,
3 => Layer::Layer1,
_ => return Err(LoftyError::Mpeg("Frame header uses a reserved layer")),
_ => return Err(LoftyError::Mp3("Frame header uses a reserved layer")),
};
let bitrate = (header >> 12) & 0b1111;
let sample_rate = (header >> 10) & 0b11;
if sample_rate == 0 {
return Err(LoftyError::Mpeg("Frame header has a sample rate of 0"));
return Err(LoftyError::Mp3("Frame header has a sample rate of 0"));
}
let mode = match (header >> 6) & 0b11 {
@ -71,7 +71,7 @@ impl Header {
1 => Mode::JointStereo,
2 => Mode::DualChannel,
3 => Mode::SingleChannel,
_ => return Err(LoftyError::Mpeg("Unreachable error")),
_ => return Err(LoftyError::Mp3("Unreachable error")),
};
let layer_index = (layer as usize).saturating_sub(1);
@ -121,14 +121,14 @@ impl XingHeader {
match &header {
b"Xing" | b"Info" => {
if reader_len < 16 {
return Err(LoftyError::Mpeg("Xing header has an invalid size (< 16)"));
return Err(LoftyError::Mp3("Xing header has an invalid size (< 16)"));
}
let mut flags = [0; 4];
reader.read_exact(&mut flags)?;
if flags[3] & 0x03 != 0x03 {
return Err(LoftyError::Mpeg(
return Err(LoftyError::Mp3(
"Xing header doesn't have required flags set (0x0001 and 0x0002)",
));
}
@ -140,7 +140,7 @@ impl XingHeader {
},
b"VBRI" => {
if reader_len < 32 {
return Err(LoftyError::Mpeg("VBRI header has an invalid size (< 32)"));
return Err(LoftyError::Mp3("VBRI header has an invalid size (< 32)"));
}
// Skip 6 bytes
@ -154,7 +154,7 @@ impl XingHeader {
Ok(Self { frames, size })
},
_ => Err(LoftyError::Mpeg("No Xing, LAME, or VBRI header located")),
_ => Err(LoftyError::Mp3("No Xing, LAME, or VBRI header located")),
}
}
}

View file

@ -47,15 +47,18 @@ impl AudioFile for MpegFile {
}
impl MpegFile {
fn id3v2_tag(&self) -> Option<&Tag> {
/// Returns a reference to the ID3v2 tag if it exists
pub fn id3v2_tag(&self) -> Option<&Tag> {
self.id3v2.as_ref()
}
fn id3v1_tag(&self) -> Option<&Tag> {
/// Returns a reference to the ID3v1 tag if it exists
pub fn id3v1_tag(&self) -> Option<&Tag> {
self.id3v1.as_ref()
}
fn ape_tag(&self) -> Option<&Tag> {
/// Returns a reference to the APEv1/2 tag if it exists
pub fn ape_tag(&self) -> Option<&Tag> {
self.ape.as_ref()
}
}

View file

@ -127,12 +127,12 @@ where
continue;
}
},
_ => return Err(LoftyError::Mpeg("File contains an invalid frame")),
_ => return Err(LoftyError::Mp3("File contains an invalid frame")),
}
}
if first_mpeg_frame.0.is_none() {
return Err(LoftyError::Mpeg("Unable to find an MPEG frame"));
return Err(LoftyError::Mp3("Unable to find an MPEG frame"));
}
let first_mpeg_frame = (first_mpeg_frame.0.unwrap(), first_mpeg_frame.1);

View file

@ -6,9 +6,6 @@ use crate::types::file::AudioFile;
use crate::types::properties::FileProperties;
use crate::types::tag::{Tag, TagType};
use std::borrow::Cow;
use std::collections::HashMap;
use std::fs::File;
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
use std::time::Duration;

View file

@ -1,17 +1,19 @@
use crate::logic::id3::v2::Id3v2Version;
use crate::{FileProperties, LoftyError, Result, Tag, TagType};
use crate::logic::ape::ApeFile;
use crate::logic::iff::aiff::AiffFile;
use crate::logic::iff::wav::WavFile;
use crate::logic::mp4::Mp4File;
use crate::logic::mpeg::MpegFile;
use crate::logic::ogg::flac::FlacFile;
use crate::logic::ogg::opus::OpusFile;
use crate::logic::ogg::vorbis::VorbisFile;
use byteorder::ReadBytesExt;
use std::convert::TryInto;
use std::io::{Read, Seek, SeekFrom};
use byteorder::ReadBytesExt;
/// Provides various methods for interaction with a file
pub trait AudioFile {
/// Read a file from a reader
@ -180,6 +182,20 @@ impl From<MpegFile> for TaggedFile {
}
}
impl From<Mp4File> for TaggedFile {
fn from(input: Mp4File) -> Self {
Self {
ty: FileType::MP4,
properties: input.properties,
tags: if let Some(ilst) = input.ilst {
vec![ilst]
} else {
Vec::new()
},
}
}
}
impl From<ApeFile> for TaggedFile {
fn from(input: ApeFile) -> Self {
Self {

View file

@ -83,7 +83,7 @@ macro_rules! item_keys {
}
item_keys!(
ALLOWED_UNKNOWN => [TagType::Ape, TagType::VorbisComments];
ALLOWED_UNKNOWN => [TagType::Ape, TagType::VorbisComments, TagType::Mp4Atom];
// Titles
AlbumTitle => [
TagType::Id3v2(_) => "TALB", TagType::Mp4Atom => "\u{a9}alb",

View file

@ -9,31 +9,6 @@ use byteorder::WriteBytesExt;
#[cfg(any(feature = "vorbis_comments", feature = "id3v2",))]
use byteorder::{BigEndian, ReadBytesExt};
#[cfg(feature = "ape")]
pub const APE_PICTYPES: [&str; 21] = [
"Other",
"Png Icon",
"Icon",
"Front",
"Back",
"Leaflet",
"Media",
"Lead Artist",
"Artist",
"Conductor",
"Band",
"Composer",
"Lyricist",
"Recording Location",
"During Recording",
"During Performance",
"Video Capture",
"Fish",
"Illustration",
"Band Logotype",
"Publisher Logotype",
];
/// Mime types for covers.
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum MimeType {

View file

@ -1,6 +1,8 @@
use super::item::ItemKey;
use super::picture::{Picture, PictureType};
#[cfg(feature = "id3v2_restrictions")]
use crate::logic::id3::v2::restrictions::TagRestrictions;
#[cfg(feature = "id3v2")]
use crate::logic::id3::v2::Id3v2Version;
#[cfg(feature = "quick_tag_accessors")]
@ -36,6 +38,7 @@ macro_rules! common_items {
}
}
#[cfg(any(feature = "id3v2", feature = "ape"))]
#[derive(Clone, Debug)]
#[allow(clippy::struct_excessive_bools)]
/// **(ID3v2/APEv2 ONLY)** Various flags to describe the content of an item
@ -43,21 +46,27 @@ macro_rules! common_items {
/// It is not an error to attempt to write flags to a format that doesn't support them.
/// They will just be ignored.
pub struct TagItemFlags {
#[cfg(feature = "id3v2")]
/// **(ID3v2 ONLY)** Preserve frame on tag edit
pub tag_alter_preservation: bool,
#[cfg(feature = "id3v2")]
/// **(ID3v2 ONLY)** Preserve frame on file edit
pub file_alter_preservation: bool,
#[cfg(any(feature = "id3v2", feature = "ape"))]
/// **(ID3v2/APEv2 ONLY)** Item cannot be written to
pub read_only: bool,
#[cfg(feature = "id3v2")]
/// **(ID3v2 ONLY)** Frame belongs in a group
///
/// In addition to setting this flag, a group identifier byte must be added.
/// All frames with the same group identifier byte belong to the same group.
pub grouping_identity: (bool, u8),
#[cfg(feature = "id3v2")]
/// **(ID3v2 ONLY)** Frame is zlib compressed
///
/// It is **required** `data_length_indicator` be set if this is set.
pub compression: bool,
#[cfg(feature = "id3v2")]
/// **(ID3v2 ONLY)** Frame is encrypted
///
/// NOTE: Since the encryption method is unknown, lofty cannot do anything with these frames
@ -65,12 +74,14 @@ pub struct TagItemFlags {
/// In addition to setting this flag, an encryption method symbol must be added.
/// The method symbol **must** be > 0x80.
pub encryption: (bool, u8),
#[cfg(feature = "id3v2")]
/// **(ID3v2 ONLY)** Frame is unsynchronised
///
/// In short, this makes all "0xFF 0x00" combinations into "0xFF 0x00 0x00" to avoid confusion
/// with the MPEG frame header, which is often identified by its "frame sync" (11 set bits).
/// It is preferred an ID3v2 tag is either *completely* unsynchronised or not unsynchronised at all.
pub unsynchronisation: bool,
#[cfg(feature = "id3v2")]
/// **(ID3v2 ONLY)** Frame has a data length indicator
///
/// The data length indicator is the size of the frame if the flags were all zeroed out.
@ -170,16 +181,34 @@ pub enum ItemValue {
Text(String),
/// **(APE/ID3v2 ONLY)** Any UTF-8 encoded locator of external information
Locator(String),
/// **(APE/ID3v2 ONLY)** Binary information
/// **(APE/ID3v2/MP4 ONLY)** Binary information
///
/// In the case of ID3v2, this is the type of a [`Id3v2Frame::EncapsulatedObject`](crate::id3::Id3v2Frame::EncapsulatedObject) **and** any unknown frame.
///
/// For APEv2, no uses of this item type are documented, there's no telling what it could be.
Binary(Vec<u8>),
/// Any 32 bit unsigned integer
///
/// This is most commonly used for items such as track and disc numbers
UInt(u32),
/// **(MP4 ONLY)** Any 64 bit unsigned integer
///
/// There are no common [`ItemKey`]s that use this
UInt64(u64),
/// Any 32 bit signed integer
///
/// There are no common [`ItemKey`]s that use this
Int(i32),
/// **(MP4 ONLY)** Any 64 bit signed integer
///
/// There are no common [`ItemKey`]s that use this
Int64(i64),
#[cfg(feature = "id3v2")]
/// **(ID3v2 ONLY)** The content of a synchronized text frame, see [`SynchronizedText`](crate::id3::SynchronizedText)
SynchronizedText(Vec<(u32, String)>),
}
#[cfg(feature = "id3v2")]
#[derive(Default, Copy, Clone)]
#[allow(clippy::struct_excessive_bools)]
/// **(ID3v2 ONLY)** Flags that apply to the entire tag
@ -198,6 +227,7 @@ pub struct TagFlags {
///
/// This is calculated if the tag is written
pub crc: bool,
#[cfg(feature = "id3v2_restrictions")]
/// Restrictions on the tag
///
/// NOTE: This **requires** `extended_header` to be set. Otherwise, it will be ignored.
@ -214,6 +244,7 @@ pub struct Tag {
tag_type: TagType,
pictures: Vec<Picture>,
items: Vec<TagItem>,
#[cfg(feature = "id3v2")]
flags: TagFlags,
}
@ -263,6 +294,14 @@ impl Tag {
flags: TagFlags::default(),
}
}
#[cfg(feature = "id3v2")]
/// **(ID3v2 ONLY)** Restrict the tag's flags
pub fn set_flags(&mut self, flags: TagFlags) {
if let TagType::Id3v2(_) = self.tag_type {
self.flags = flags
}
}
}
impl Tag {