Add format specific tag structs

This allows for the use of format-specific elements, such as ID3v2 frame headers
This commit is contained in:
Serial 2021-11-21 15:18:19 -05:00
parent e84731c375
commit bc0c246dbf
83 changed files with 3218 additions and 2247 deletions

View file

@ -17,14 +17,12 @@ ogg_pager = "0.1.7"
# Mp4
simdutf8 = { version = "0.1.3", optional = true }
# Quick string accessor methods for Tag
paste = { version = "1.0.5", optional = true }
paste = "1.0.5"
base64 = "0.13.0"
byteorder = "1.4.3"
[features]
default = ["mp4_atoms", "vorbis_comments", "ape", "id3v1", "id3v2", "aiff_text_chunks", "riff_info_list", "quick_tag_accessors"]
default = ["mp4_atoms", "vorbis_comments", "ape", "id3v1", "id3v2", "aiff_text_chunks", "riff_info_list"]
mp4_atoms = ["simdutf8"]
vorbis_comments = []
ape = []
@ -33,7 +31,6 @@ id3v2 = ["flate2"]
id3v2_restrictions = []
aiff_text_chunks = []
riff_info_list = []
quick_tag_accessors = ["paste"]
[dev-dependencies]
criterion = { version = "0.3.5", features = ["html_reports"] }

View file

@ -10,7 +10,7 @@ Parse, convert, and write metadata to various audio formats.
| File Format | Extensions | Read | Write | Metadata Format(s) |
|-------------|-------------------------------------------------|------|-------|----------------------------------------------------|
| Ape | `ape` |**X** |**X** |`APEv2`, `APEv1`, `ID3v2` (Not officially), `ID3v1` |
| Ape | `ape` |**X** |**X** |`APEv2`, `APEv1`, `ID3v2` (Read only), `ID3v1` |
| AIFF | `aiff`, `aif` |**X** |**X** |`ID3v2`, `Text Chunks` |
| FLAC | `flac` |**X** |**X** |`Vorbis Comments` |
| MP3 | `mp3` |**X** |**X** |`ID3v2`, `ID3v1`, `APEv2`, `APEv1` |

View file

@ -1,10 +1,10 @@
use ogg_pager::PageError;
use std::fmt::{Display, Formatter};
/// Result of tag operations.
/// Result of tag operations
pub type Result<T> = std::result::Result<T, LoftyError>;
/// Errors that could occur within Lofty.
/// Errors that could occur within Lofty
#[derive(Debug)]
pub enum LoftyError {
// File extension/format related errors
@ -75,7 +75,6 @@ pub enum LoftyError {
// Conversions for external errors
/// Errors that arise while parsing OGG pages
#[cfg(feature = "vorbis_comments")]
OggPage(ogg_pager::PageError),
/// Unable to convert bytes to a String
FromUtf8(std::string::FromUtf8Error),
@ -87,7 +86,6 @@ impl Display for LoftyError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
// Conversions
#[cfg(feature = "vorbis_comments")]
LoftyError::OggPage(ref err) => write!(f, "{}", err),
LoftyError::FromUtf8(ref err) => write!(f, "{}", err),
LoftyError::Io(ref err) => write!(f, "{}", err),

View file

@ -112,7 +112,8 @@
//!
//! See [`id3`](crate::id3) for important warnings and notes on reading tags.
#![deny(clippy::pedantic, clippy::all, missing_docs)]
#![deny(clippy::pedantic, clippy::all)]
// TODO missing_docs
#![allow(
clippy::too_many_lines,
clippy::cast_precision_loss,
@ -127,7 +128,9 @@
clippy::semicolon_if_nothing_returned,
clippy::used_underscore_binding,
clippy::new_without_default,
clippy::unused_self
clippy::unused_self,
clippy::from_over_into,
clippy::upper_case_acronyms
)]
pub use crate::error::{LoftyError, Result};
@ -141,12 +144,6 @@ pub use crate::types::{
tag::{Tag, TagType},
};
#[cfg(any(feature = "id3v2", feature = "ape"))]
pub use crate::types::item::TagItemFlags;
#[cfg(feature = "id3v2")]
pub use crate::types::tag::TagFlags;
mod types;
/// Various concrete file types, used when inference is unnecessary
@ -154,7 +151,10 @@ pub mod files {
pub use crate::logic::ape::{ApeFile, ApeProperties};
pub use crate::logic::iff::{
aiff::AiffFile,
wav::{WavFile, WavFormat, WavProperties},
wav::{
properties::{WavFormat, WavProperties},
WavFile,
},
};
pub use crate::logic::mp3::{
header::{ChannelMode, Layer, MpegVersion},
@ -163,12 +163,19 @@ pub mod files {
pub use crate::logic::mp4::{Mp4Codec, Mp4File, Mp4Properties};
pub use crate::logic::ogg::{
flac::FlacFile,
opus::{OpusFile, OpusProperties},
vorbis::{VorbisFile, VorbisProperties},
opus::{properties::OpusProperties, OpusFile},
vorbis::{properties::VorbisProperties, VorbisFile},
};
pub use crate::types::file::AudioFile;
}
/// Various concrete tag types, used when format-specific features are necessary
pub mod tags {
pub use crate::logic::id3::v1::tag::Id3v1Tag;
pub use crate::logic::iff::{aiff::tag::AiffTextChunks, wav::tag::RiffInfoList};
pub use crate::logic::ogg::tag::VorbisComments;
}
#[cfg(any(feature = "id3v1", feature = "id3v2"))]
/// ID3v1/v2 specific items
pub mod id3 {
@ -180,48 +187,19 @@ pub mod id3 {
//! ID3v2 items and utilities
//!
//! # ID3v2 notes and warnings
//!
//! ID3v2 does things differently than other formats.
//!
//! ## Unknown Keys
//!
//! ID3v2 **does not** support [`ItemKey::Unknown`](crate::ItemKey::Unknown) and they will be ignored.
//! Instead, [`ItemKey::Id3v2Specific`](crate::ItemKey::Id3v2Specific) with an [`Id3v2Frame`](crate::id3::v2::Id3v2Frame) variant must be used.
//!
//! ## Frame ID mappings
//!
//! Certain [`ItemKey`](crate::ItemKey)s are unable to map to an ID3v2 frame, as they are a part of a larger
//! collection (such as `TIPL` and `TMCL`).
//!
//! For example, if the key is `Arranger` (part of `TIPL`), there is no mapping available.
//!
//! In this case, the caller is expected to build these lists. If these [`ItemKey`](crate::ItemKey)s are inserted
//! using [`Tag::insert_item_unchecked`](crate::Tag::insert_item_unchecked), they will simply be ignored.
//!
//! ## Special frames
//!
//! ID3v2 has multiple frames that have no equivalent in other formats:
//!
//! * COMM - Comments (Unlike comments in other formats)
//! * USLT - Unsynchronized text (Unlike lyrics/text in other formats)
//! * TXXX - User defined text
//! * WXXX - User defined URL
//! * SYLT - Synchronized text
//! * GEOB - Encapsulated object (file)
//!
//! These frames all require different amounts of information, so they cannot be mapped to a traditional [`ItemKey`](crate::ItemKey) variant.
//! The solution is to use [`ItemKey::Id3v2Specific`](crate::ItemKey::Id3v2Specific) alongside [`Id3v2Frame`](crate::id3::v2::Id3v2Frame).
//!
//! NOTE: Unlike the above issue, this one does not require unchecked insertion.
// TODO
pub use {
crate::logic::id3::v2::frame::{Id3v2Frame, LanguageSpecificFrame},
crate::logic::id3::v2::frame::{
EncodedTextFrame, Frame, FrameFlags, FrameID, FrameValue, LanguageFrame,
},
crate::logic::id3::v2::items::encapsulated_object::{
GEOBInformation, GeneralEncapsulatedObject,
},
crate::logic::id3::v2::items::sync_text::{
SyncTextContentType, SyncTextInformation, SynchronizedText, TimestampFormat,
},
crate::logic::id3::v2::tag::{Id3v2Tag, Id3v2TagFlags},
crate::logic::id3::v2::util::text_utils::TextEncoding,
crate::logic::id3::v2::util::upgrade::{upgrade_v2, upgrade_v3},
crate::logic::id3::v2::Id3v2Version,
@ -253,6 +231,11 @@ pub mod id3 {
}
}
/// MP4 specific items
pub mod mp4 {
pub use crate::logic::mp4::ilst::{Atom, AtomData, AtomIdent};
}
/// Various items related to [`Picture`](crate::picture::Picture)s
pub mod picture {
pub use crate::types::picture::{MimeType, Picture, PictureInformation, PictureType};

View file

@ -1,11 +1,18 @@
mod constants;
mod properties;
pub(crate) mod read;
#[cfg(feature = "ape")]
pub(crate) mod tag;
pub(crate) mod write;
#[cfg(feature = "id3v1")]
use crate::logic::id3::v1::tag::Id3v1Tag;
use crate::logic::id3::v2::tag::Id3v2Tag;
use crate::logic::tag_methods;
use crate::types::file::{AudioFile, FileType, TaggedFile};
use crate::{FileProperties, Result, Tag, TagType};
use crate::{FileProperties, Result, TagType};
use tag::ApeTag;
use std::io::{Read, Seek};
use std::time::Duration;
@ -61,13 +68,13 @@ impl ApeProperties {
pub struct ApeFile {
#[cfg(feature = "id3v1")]
/// An ID3v1 tag
pub(crate) id3v1: Option<Tag>,
pub(crate) id3v1_tag: Option<Id3v1Tag>,
#[cfg(feature = "id3v2")]
/// An ID3v2 tag (Not officially supported)
pub(crate) id3v2: Option<Tag>,
pub(crate) id3v2_tag: Option<Id3v2Tag>,
#[cfg(feature = "ape")]
/// An APEv1/v2 tag
pub(crate) ape: Option<Tag>,
pub(crate) ape_tag: Option<ApeTag>,
/// The file's audio properties
pub(crate) properties: ApeProperties,
}
@ -77,10 +84,14 @@ impl From<ApeFile> for TaggedFile {
Self {
ty: FileType::APE,
properties: FileProperties::from(input.properties),
tags: vec![input.id3v1, input.id3v2, input.ape]
.into_iter()
.flatten()
.collect(),
tags: vec![
input.ape_tag.map(|at| at.into()),
input.id3v1_tag.map(|id3v1| id3v1.into()),
input.id3v2_tag.map(|id3v2| id3v2.into()),
]
.into_iter()
.flatten()
.collect(),
}
}
}
@ -100,54 +111,38 @@ impl AudioFile for ApeFile {
&self.properties
}
#[allow(clippy::match_same_arms)]
fn contains_tag(&self) -> bool {
self.ape.is_some() || self.id3v1.is_some() || self.id3v2.is_some()
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,
}
}
fn contains_tag_type(&self, tag_type: &TagType) -> bool {
match tag_type {
TagType::Ape => self.ape.is_some(),
TagType::Id3v1 => self.id3v1.is_some(),
TagType::Id3v2 => self.id3v2.is_some(),
#[cfg(feature = "ape")]
TagType::Ape => self.ape_tag.is_some(),
#[cfg(feature = "id3v1")]
TagType::Id3v1 => self.id3v1_tag.is_some(),
#[cfg(feature = "id3v2")]
TagType::Id3v2 => self.id3v2_tag.is_some(),
_ => false,
}
}
}
impl ApeFile {
#[cfg(feature = "id3v2")]
/// Returns a reference to the ID3v2 tag if it exists
pub fn id3v2_tag(&self) -> Option<&Tag> {
self.id3v2.as_ref()
}
#[cfg(feature = "id3v2")]
/// Returns a mutable reference to the ID3v2 tag if it exists
pub fn id3v2_tag_mut(&mut self) -> Option<&mut Tag> {
self.id3v2.as_mut()
}
#[cfg(feature = "id3v1")]
/// Returns a reference to the ID3v1 tag if it exists
pub fn id3v1_tag(&self) -> Option<&Tag> {
self.id3v1.as_ref()
}
#[cfg(feature = "id3v1")]
/// Returns a mutable reference to the ID3v1 tag if it exists
pub fn id3v1_tag_mut(&mut self) -> Option<&mut Tag> {
self.id3v1.as_mut()
}
#[cfg(feature = "ape")]
/// Returns a reference to the APEv1/2 tag if it exists
pub fn ape_tag(&self) -> Option<&Tag> {
self.ape.as_ref()
}
#[cfg(feature = "ape")]
/// Returns a mutable reference to the APEv1/2 tag if it exists
pub fn ape_tag_mut(&mut self) -> Option<&mut Tag> {
self.ape.as_mut()
}
tag_methods! {
ApeFile => ID3v2, id3v2_tag, Id3v2Tag; ID3v1, id3v1_tag, Id3v1Tag; APE, ape_tag, ApeTag
}

View file

@ -3,14 +3,17 @@ use super::properties::{properties_gt_3980, properties_lt_3980};
use super::tag::read::read_ape_tag;
use super::{ApeFile, ApeProperties};
use crate::error::{LoftyError, Result};
use crate::logic::id3::find_lyrics3v2;
use crate::logic::id3::v1::find_id3v1;
use crate::logic::id3::v2::find_id3v2;
use crate::logic::id3::v2::read::parse_id3v2;
use crate::types::tag::Tag;
#[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 std::io::{Read, Seek, SeekFrom};
use crate::id3::v2::Id3v2Tag;
use crate::logic::ape::tag::ApeTag;
use byteorder::{LittleEndian, ReadBytesExt};
fn read_properties<R>(data: &mut R, stream_len: u64) -> Result<ApeProperties>
@ -40,22 +43,22 @@ where
let mut stream_len = end - start;
let mut id3v2: Option<Tag> = None;
let mut id3v1: Option<Tag> = None;
let mut ape: Option<Tag> = None;
let mut id3v2_tag: Option<Id3v2Tag> = None;
let mut id3v1_tag: Option<Id3v1Tag> = None;
let mut ape_tag: Option<ApeTag> = 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;
let id3v2_tag = parse_id3v2(&mut &*id3v2_read)?;
let id3v2 = parse_id3v2(&mut &*id3v2_read)?;
// Skip over the footer
if id3v2_tag.flags().footer {
if id3v2.flags().footer {
data.seek(SeekFrom::Current(10))?;
}
id3v2 = Some(id3v2_tag)
id3v2_tag = Some(id3v2)
}
let mut found_mac = false;
@ -86,10 +89,10 @@ where
return Err(LoftyError::Ape("Found incomplete APE tag"));
}
let (ape_tag, size) = read_ape_tag(data, false)?;
let (ape, size) = read_ape_tag(data, false)?;
stream_len -= u64::from(size);
ape = Some(ape_tag)
ape_tag = Some(ape)
}
_ => {
return Err(LoftyError::Ape(
@ -104,11 +107,11 @@ where
//
// Starts with ['T', 'A', 'G']
// Exactly 128 bytes long (including the identifier)
let (found_id3v1, id3v1_tag) = find_id3v1(data, true)?;
let (found_id3v1, id3v1) = find_id3v1(data, true)?;
if found_id3v1 {
stream_len -= 128;
id3v1 = id3v1_tag;
id3v1_tag = id3v1;
}
// Next, check for a Lyrics3v2 tag, and skip over it, as it's no use to us
@ -129,19 +132,22 @@ where
data.read_exact(&mut ape_preamble)?;
if &ape_preamble == APE_PREAMBLE {
let (ape_tag, size) = read_ape_tag(data, true)?;
let (ape, size) = read_ape_tag(data, true)?;
stream_len -= u64::from(size);
ape = Some(ape_tag)
ape_tag = Some(ape)
}
// Go back to the MAC header to read properties
data.seek(SeekFrom::Start(mac_start))?;
Ok(ApeFile {
id3v1,
id3v2,
ape,
#[cfg(feature = "id3v1")]
id3v1_tag,
#[cfg(feature = "id3v2")]
id3v2_tag,
#[cfg(feature = "ape")]
ape_tag,
properties: read_properties(data, stream_len)?,
})
}

65
src/logic/ape/tag/item.rs Normal file
View file

@ -0,0 +1,65 @@
use crate::error::{LoftyError, Result};
use crate::logic::ape::constants::INVALID_KEYS;
use crate::types::item::{ItemValue, ItemValueRef, TagItem};
use crate::types::tag::TagType;
use std::convert::TryFrom;
pub struct ApeItem {
pub read_only: bool,
pub(crate) key: String,
pub(crate) value: ItemValue,
}
impl ApeItem {
pub fn new(key: String, value: ItemValue) -> Result<Self> {
if INVALID_KEYS.contains(&&*key.to_uppercase()) {
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"));
}
Ok(Self {
read_only: false,
key,
value,
})
}
pub fn set_read_only(&mut self) {
self.read_only = true
}
}
impl TryFrom<TagItem> for ApeItem {
type Error = LoftyError;
fn try_from(value: TagItem) -> std::prelude::rust_2015::Result<Self, Self::Error> {
Self::new(
value
.item_key
.map_key(&TagType::Ape, false)
.ok_or(LoftyError::Ape(
"Attempted to convert an unsupported item key",
))?
.to_string(),
value.item_value,
)
}
}
pub(in crate::logic) struct ApeItemRef<'a> {
pub read_only: bool,
pub value: ItemValueRef<'a>,
}
impl<'a> Into<ApeItemRef<'a>> for &'a ApeItem {
fn into(self) -> ApeItemRef<'a> {
ApeItemRef {
read_only: self.read_only,
value: (&self.value).into(),
}
}
}

View file

@ -1,2 +1,128 @@
mod item;
pub(in crate::logic) mod read;
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::{Tag, TagType};
use std::collections::HashMap;
use std::convert::TryInto;
use std::fs::File;
#[derive(Default)]
/// An APE tag
pub struct ApeTag {
pub read_only: bool,
pub(super) items: HashMap<String, ApeItem>,
}
impl ApeTag {
pub fn get_key(&self, key: &str) -> Option<&ApeItem> {
self.items.get(key)
}
pub fn push_item(&mut self, value: ApeItem) {
let _ = self.items.insert(value.key.clone(), value);
}
pub fn remove_key(&mut self, key: &str) {
let _ = self.items.remove(key);
}
}
impl ApeTag {
pub fn write_to(&self, file: &mut File) -> Result<()> {
Into::<ApeTagRef>::into(self).write_to(file)
}
}
impl From<ApeTag> 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<Tag> 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.push_item(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.push_item(item)
}
}
}
ape_tag
}
}
pub(in crate::logic) struct ApeTagRef<'a> {
read_only: bool,
pub(super) items: HashMap<&'a str, ApeItemRef<'a>>,
}
impl<'a> ApeTagRef<'a> {
pub(crate) fn write_to(&self, file: &mut File) -> Result<()> {
write::write_to(file, self)
}
}
impl<'a> Into<ApeTagRef<'a>> for &'a Tag {
fn into(self) -> ApeTagRef<'a> {
let mut items = HashMap::<&'a str, ApeItemRef<'a>>::new();
for item in &self.items {
let key = item.key().map_key(&TagType::Ape, true).unwrap();
items.insert(
key,
ApeItemRef {
read_only: false,
value: (&item.item_value).into(),
},
);
}
ApeTagRef {
read_only: false,
items,
}
}
}
impl<'a> Into<ApeTagRef<'a>> for &'a ApeTag {
fn into(self) -> ApeTagRef<'a> {
ApeTagRef {
read_only: self.read_only,
items: {
let mut items = HashMap::<&str, ApeItemRef<'a>>::new();
for (k, v) in &self.items {
items.insert(k.as_str(), v.into());
}
items
},
}
}
}

View file

@ -1,14 +1,14 @@
use super::{ApeItem, ApeTag};
use crate::error::{LoftyError, Result};
use crate::logic::ape::constants::INVALID_KEYS;
use crate::types::item::{ItemKey, ItemValue, TagItem, TagItemFlags};
use crate::types::tag::{Tag, TagType};
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<R>(data: &mut R, footer: bool) -> Result<(Tag, u32)>
pub(crate) fn read_ape_tag<R>(data: &mut R, footer: bool) -> Result<(ApeTag, u32)>
where
R: Read + Seek,
{
@ -34,7 +34,7 @@ where
data.seek(SeekFrom::Current(12))?;
}
let mut tag = Tag::new(TagType::Ape);
let mut tag = ApeTag::default();
for _ in 0..item_count {
let value_size = data.read_u32::<LittleEndian>()?;
@ -64,10 +64,7 @@ where
return Err(LoftyError::Ape("Tag item contains a non ASCII key"));
}
let item_flags = TagItemFlags {
read_only: (flags & 1) == 1,
..TagItemFlags::default()
};
let read_only = (flags & 1) == 1;
let item_type = (flags & 6) >> 1;
@ -85,13 +82,13 @@ where
_ => return Err(LoftyError::Ape("Tag item contains an invalid item type")),
};
let mut item = TagItem::new(
ItemKey::from_key(&TagType::Ape, &*key).unwrap(),
parsed_value,
);
let mut item = ApeItem::new(key, parsed_value)?;
item.set_flags(item_flags);
tag.insert_item(item);
if read_only {
item.set_read_only()
}
tag.push_item(item);
}
// Version 1 doesn't include a header

View file

@ -1,19 +1,26 @@
use super::read::read_ape_tag;
use crate::error::{LoftyError, Result};
use crate::logic::ape::constants::APE_PREAMBLE;
use crate::logic::id3::find_lyrics3v2;
use crate::logic::id3::v1::find_id3v1;
use crate::logic::ape::tag::item::ApeItemRef;
use crate::logic::ape::tag::ApeTagRef;
use crate::logic::id3::v2::find_id3v2;
use crate::types::item::{ItemValue, TagItem};
use crate::types::picture::Picture;
use crate::types::tag::{Tag, TagType};
use crate::logic::id3::{find_id3v1, find_lyrics3v2};
use crate::probe::Probe;
use crate::types::file::FileType;
use crate::types::item::ItemValueRef;
use std::collections::HashMap;
use std::fs::File;
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
use byteorder::{LittleEndian, WriteBytesExt};
pub(crate) fn write_to(data: &mut File, tag: &Tag) -> Result<()> {
pub(in crate::logic) fn write_to(data: &mut File, tag: &ApeTagRef) -> Result<()> {
match Probe::new().file_type(data) {
Some(ft) if ft == FileType::APE || ft == FileType::MP3 => {}
_ => return Err(LoftyError::UnsupportedTag),
}
// We don't actually need the ID3v2 tag, but reading it will seek to the end of it if it exists
find_id3v2(data, false)?;
@ -34,9 +41,9 @@ pub(crate) fn write_to(data: &mut File, tag: &Tag) -> Result<()> {
let (mut existing, size) = read_ape_tag(data, false)?;
// Only keep metadata around that's marked read only
existing.retain(|i| i.flags().read_only);
existing.items.retain(|_i, v| v.read_only);
if existing.item_count() > 0 {
if !existing.items.is_empty() {
read_only = Some(existing)
}
@ -65,9 +72,9 @@ pub(crate) fn write_to(data: &mut File, tag: &Tag) -> Result<()> {
let (mut existing, size) = read_ape_tag(data, true)?;
existing.retain(|i| i.flags().read_only);
existing.items.retain(|_, v| v.read_only);
if existing.item_count() > 0 {
if !existing.items.is_empty() {
read_only = Some(existing)
}
@ -80,18 +87,10 @@ pub(crate) fn write_to(data: &mut File, tag: &Tag) -> Result<()> {
}
// Preserve any metadata marked as read only
// If there is any read only metadata, we will have to clone the TagItems
let tag = if let Some(read_only) = read_only {
use std::collections::HashSet;
let mut items = [read_only.items(), tag.items()].concat();
let mut unique_items = HashSet::new();
items.retain(|i| unique_items.insert(i.clone()));
create_ape_tag(&items, tag.pictures())?
create_ape_tag(&Into::<ApeTagRef>::into(&read_only).items)?
} else {
create_ape_tag(tag.items(), tag.pictures())?
create_ape_tag(&tag.items)?
};
data.seek(SeekFrom::Start(0))?;
@ -118,69 +117,44 @@ pub(crate) fn write_to(data: &mut File, tag: &Tag) -> Result<()> {
Ok(())
}
fn create_ape_tag(items: &[TagItem], pictures: &[Picture]) -> Result<Vec<u8>> {
fn create_ape_tag(items: &HashMap<&str, ApeItemRef>) -> Result<Vec<u8>> {
// Unnecessary to write anything if there's no metadata
if items.is_empty() && pictures.is_empty() {
if items.is_empty() {
Ok(Vec::<u8>::new())
} else {
let mut tag = Cursor::new(Vec::<u8>::new());
let item_count = (items.len() + pictures.len()) as u32;
let item_count = items.len() as u32;
for item in items {
let (size, flags, value) = match item.value() {
ItemValue::Binary(value) => {
let mut flags = 1_u32 << 1;
for (k, v) in items {
let (mut flags, value) = match v.value {
ItemValueRef::Binary(value) => {
tag.write_u32::<LittleEndian>(value.len() as u32)?;
if item.flags().read_only {
flags |= 1_u32
}
(value.len() as u32, flags, value.as_slice())
(1_u32 << 1, value)
}
ItemValue::Text(value) => {
let value = value.as_bytes();
ItemValueRef::Text(value) => {
tag.write_u32::<LittleEndian>(value.len() as u32)?;
let mut flags = 0_u32;
if item.flags().read_only {
flags |= 1_u32
}
(value.len() as u32, flags, value)
(0_u32, value.as_bytes())
}
ItemValue::Locator(value) => {
let mut flags = 2_u32 << 1;
ItemValueRef::Locator(value) => {
tag.write_u32::<LittleEndian>(value.len() as u32)?;
if item.flags().read_only {
flags |= 1_u32
}
(value.len() as u32, flags, value.as_bytes())
(2_u32 << 1, value.as_bytes())
}
_ => continue,
};
tag.write_u32::<LittleEndian>(size)?;
if v.read_only {
flags |= 1_u32
}
tag.write_u32::<LittleEndian>(flags)?;
tag.write_all(item.key().map_key(&TagType::Ape).unwrap().as_bytes())?;
tag.write_all(k.as_bytes())?;
tag.write_u8(0)?;
tag.write_all(value)?;
}
for pic in pictures {
let key = pic.pic_type.as_ape_key();
let bytes = pic.as_ape_bytes();
// Binary item
let flags = 1_u32 << 1;
tag.write_u32::<LittleEndian>(bytes.len() as u32)?;
tag.write_u32::<LittleEndian>(flags)?;
tag.write_all(key.as_bytes())?;
tag.write_u8(0)?;
tag.write_all(&bytes)?;
}
let size = tag.get_ref().len();
if size as u64 + 32 > u64::from(u32::MAX) {

View file

@ -1,12 +1,14 @@
use crate::error::{LoftyError, Result};
use crate::logic::ape::tag::ApeTagRef;
use crate::logic::id3::v1::tag::Id3v1TagRef;
use crate::types::tag::{Tag, TagType};
use std::fs::File;
pub(crate) fn write_to(data: &mut File, tag: &Tag) -> Result<()> {
pub(in crate::logic) fn write_to(data: &mut File, tag: &Tag) -> Result<()> {
match tag.tag_type() {
TagType::Ape => super::tag::write::write_to(data, tag),
TagType::Id3v1 => crate::logic::id3::v1::write::write_id3v1(data, tag),
TagType::Ape => Into::<ApeTagRef>::into(tag).write_to(data),
TagType::Id3v1 => Into::<Id3v1TagRef>::into(tag).write_to(data),
_ => Err(LoftyError::UnsupportedTag),
}
}

View file

@ -53,3 +53,64 @@ where
Ok((exists, size))
}
#[cfg(feature = "id3v1")]
pub(in crate::logic) fn find_id3v1<R>(
data: &mut R,
read: bool,
) -> Result<(bool, Option<v1::tag::Id3v1Tag>)>
where
R: Read + Seek,
{
let mut id3v1 = None;
let mut exists = false;
data.seek(SeekFrom::End(-128))?;
let mut id3v1_header = [0; 3];
data.read_exact(&mut id3v1_header)?;
data.seek(SeekFrom::Current(-3))?;
if &id3v1_header == b"TAG" {
exists = true;
if read {
let mut id3v1_tag = [0; 128];
data.read_exact(&mut id3v1_tag)?;
data.seek(SeekFrom::End(-128))?;
id3v1 = Some(v1::read::parse_id3v1(id3v1_tag))
}
} else {
// No ID3v1 tag found
data.seek(SeekFrom::End(0))?;
}
Ok((exists, id3v1))
}
#[cfg(not(feature = "id3v1"))]
pub(in crate::logic) fn find_id3v1<R>(data: &mut R, read: bool) -> Result<(bool, Option<()>)>
where
R: Read + Seek,
{
let mut exists = false;
data.seek(SeekFrom::End(-128))?;
let mut id3v1_header = [0; 3];
data.read_exact(&mut id3v1_header)?;
data.seek(SeekFrom::Current(-3))?;
if &id3v1_header == b"TAG" {
exists = true;
} else {
// No ID3v1 tag found
data.seek(SeekFrom::End(0))?;
}
Ok((exists, None))
}

View file

@ -1,3 +1,5 @@
use crate::types::item::ItemKey;
/// All possible genres for ID3v1
pub const GENRES: [&str; 192] = [
"Blues",
@ -193,3 +195,11 @@ pub const GENRES: [&str; 192] = [
"Garage Rock",
"Psybient",
];
pub const VALID_ITEMKEYS: [ItemKey; 5] = [
ItemKey::TrackTitle,
ItemKey::TrackArtist,
ItemKey::AlbumTitle,
ItemKey::Year,
ItemKey::Comment,
];

View file

@ -1,41 +1,4 @@
pub(crate) mod constants;
pub(in crate::logic) mod read;
pub(crate) mod tag;
pub(in crate::logic) mod write;
use crate::error::Result;
use crate::types::tag::Tag;
use std::io::{Read, Seek, SeekFrom};
pub(in crate::logic) fn find_id3v1<R>(data: &mut R, read: bool) -> Result<(bool, Option<Tag>)>
where
R: Read + Seek,
{
let mut id3v1 = None;
let mut exists = false;
data.seek(SeekFrom::End(-128))?;
let mut id3v1_header = [0; 3];
data.read_exact(&mut id3v1_header)?;
data.seek(SeekFrom::Current(-3))?;
if &id3v1_header == b"TAG" {
exists = true;
if read {
let mut id3v1_tag = [0; 128];
data.read_exact(&mut id3v1_tag)?;
data.seek(SeekFrom::End(-128))?;
id3v1 = Some(read::parse_id3v1(id3v1_tag))
}
} else {
// No ID3v1 tag found
data.seek(SeekFrom::End(0))?;
}
Ok((exists, id3v1))
}

View file

@ -1,54 +1,42 @@
use super::constants::GENRES;
use crate::types::item::{ItemKey, ItemValue, TagItem};
use crate::types::tag::{Tag, TagType};
use super::tag::Id3v1Tag;
pub fn parse_id3v1(reader: [u8; 128]) -> Tag {
let mut tag = Tag::new(TagType::Id3v1);
pub fn parse_id3v1(reader: [u8; 128]) -> Id3v1Tag {
let mut tag = Id3v1Tag {
title: None,
artist: None,
album: None,
year: None,
comment: None,
track_number: None,
genre: None,
};
let reader = &reader[3..];
if let Some(title) = decode_text(ItemKey::TrackTitle, &reader[..30]) {
tag.insert_item_unchecked(title);
}
if let Some(artist) = decode_text(ItemKey::TrackArtist, &reader[30..60]) {
tag.insert_item_unchecked(artist);
}
if let Some(album) = decode_text(ItemKey::AlbumTitle, &reader[60..90]) {
tag.insert_item_unchecked(album);
}
if let Some(year) = decode_text(ItemKey::Year, &reader[90..94]) {
tag.insert_item_unchecked(year);
}
tag.title = decode_text(&reader[..30]);
tag.artist = decode_text(&reader[30..60]);
tag.album = decode_text(&reader[60..90]);
tag.year = decode_text(&reader[90..94]);
let range = if reader[119] == 0 && reader[122] != 0 {
tag.insert_item_unchecked(TagItem::new(
ItemKey::TrackNumber,
ItemValue::UInt(u32::from(reader[122])),
));
tag.track_number = Some(reader[122]);
94_usize..123
} else {
94..124
};
if let Some(comment) = decode_text(ItemKey::Comment, &reader[range]) {
tag.insert_item_unchecked(comment);
}
tag.comment = decode_text(&reader[range]);
if reader[124] < GENRES.len() as u8 {
tag.insert_item_unchecked(TagItem::new(
ItemKey::Genre,
ItemValue::Text(GENRES[reader[125] as usize].to_string()),
));
tag.genre = Some(reader[124]);
}
tag
}
fn decode_text(key: ItemKey, data: &[u8]) -> Option<TagItem> {
fn decode_text(data: &[u8]) -> Option<String> {
let read = data
.iter()
.filter(|c| **c != 0)
@ -58,6 +46,6 @@ fn decode_text(key: ItemKey, data: &[u8]) -> Option<TagItem> {
if read.is_empty() {
None
} else {
Some(TagItem::new(key, ItemValue::Text(read)))
Some(read)
}
}

179
src/logic/id3/v1/tag.rs Normal file
View file

@ -0,0 +1,179 @@
use crate::error::Result;
use crate::logic::id3::v1::constants::GENRES;
use crate::types::item::{ItemKey, ItemValue, TagItem};
use crate::types::tag::{Tag, TagType};
use std::fs::File;
#[derive(Default, Debug)]
/// An ID3v1 tag
///
/// ID3v1 is a severely limited format, with each field
/// being incredibly small in size. All fields have been
/// commented with their maximum sizes and any other additional
/// restrictions.
///
/// Attempting to write a field greater than the maximum size
/// will **not** error, it will just be shrunk.
pub struct Id3v1Tag {
/// Track title, 30 bytes max
pub title: Option<String>,
/// Track artist, 30 bytes max
pub artist: Option<String>,
/// Album title, 30 bytes max
pub album: Option<String>,
/// Release year, 4 bytes max
pub year: Option<String>,
/// A short comment
///
/// The number of bytes differs between versions, but not much.
/// A V1 tag may have been read, which limits this field to 30 bytes.
/// A V1.1 tag, however, only has 28 bytes available.
///
/// Lofty will *always* write a V1.1 tag.
pub comment: Option<String>,
/// The track number, 1 byte max
///
/// Issues:
///
/// * The track number **cannot** be 0. Many readers, including Lofty,
/// look for a zeroed byte at the end of the comment to differentiate
/// between V1 and V1.1.
/// * A V1 tag may have been read, which does *not* have a track number.
pub track_number: Option<u8>,
/// The track's genre, 1 byte max
///
/// ID3v1 has a predefined set of genres, see [`GENRES`](crate::id3::v1::GENRES).
/// This byte should be an index to a genre.
pub genre: Option<u8>,
}
impl Id3v1Tag {
pub fn is_empty(&self) -> bool {
self.title.is_none()
&& self.artist.is_none()
&& self.album.is_none()
&& self.year.is_none()
&& self.comment.is_none()
&& self.track_number.is_none()
&& self.genre.is_none()
}
pub fn write_to(&self, file: &mut File) -> Result<()> {
Into::<Id3v1TagRef>::into(self).write_to(file)
}
}
impl From<Id3v1Tag> for Tag {
fn from(input: Id3v1Tag) -> Self {
let mut tag = Self::new(TagType::Id3v1);
input.title.map(|t| tag.insert_text(ItemKey::TrackTitle, t));
input
.artist
.map(|a| tag.insert_text(ItemKey::TrackArtist, a));
input.album.map(|a| tag.insert_text(ItemKey::AlbumTitle, a));
input.year.map(|y| tag.insert_text(ItemKey::Year, y));
input.comment.map(|c| tag.insert_text(ItemKey::Comment, c));
if let Some(t) = input.track_number {
tag.insert_item_unchecked(TagItem::new(
ItemKey::TrackNumber,
ItemValue::Text(t.to_string()),
))
}
if let Some(genre_index) = input.genre {
if let Some(genre) = GENRES.get(genre_index as usize) {
tag.insert_text(ItemKey::Genre, (*genre).to_string());
}
}
tag
}
}
impl From<Tag> for Id3v1Tag {
fn from(input: Tag) -> Self {
Self {
title: input.get_string(&ItemKey::TrackTitle).map(str::to_owned),
artist: input.get_string(&ItemKey::TrackArtist).map(str::to_owned),
album: input.get_string(&ItemKey::AlbumTitle).map(str::to_owned),
year: input.get_string(&ItemKey::Year).map(str::to_owned),
comment: input.get_string(&ItemKey::Comment).map(str::to_owned),
track_number: if let Some(Ok(track_number)) = input
.get_string(&ItemKey::TrackNumber)
.map(str::parse::<u8>)
{
Some(track_number)
} else {
None
},
genre: input
.get_string(&ItemKey::Genre)
.and_then(|genre| GENRES.iter().position(|v| v == &genre).map(|pos| pos as u8)),
}
}
}
pub(crate) struct Id3v1TagRef<'a> {
pub title: Option<&'a str>,
pub artist: Option<&'a str>,
pub album: Option<&'a str>,
pub year: Option<&'a str>,
pub comment: Option<&'a str>,
pub track_number: Option<u8>,
pub genre: Option<u8>,
}
impl<'a> Into<Id3v1TagRef<'a>> for &'a Id3v1Tag {
fn into(self) -> Id3v1TagRef<'a> {
Id3v1TagRef {
title: self.title.as_deref(),
artist: self.artist.as_deref(),
album: self.album.as_deref(),
year: self.year.as_deref(),
comment: self.comment.as_deref(),
track_number: self.track_number,
genre: self.genre,
}
}
}
impl<'a> Into<Id3v1TagRef<'a>> for &'a Tag {
fn into(self) -> Id3v1TagRef<'a> {
Id3v1TagRef {
title: self.get_string(&ItemKey::TrackTitle),
artist: self.get_string(&ItemKey::TrackArtist),
album: self.get_string(&ItemKey::AlbumTitle),
year: self.get_string(&ItemKey::Year),
comment: self.get_string(&ItemKey::Comment),
track_number: if let Some(Ok(track_number)) =
self.get_string(&ItemKey::TrackNumber).map(str::parse::<u8>)
{
Some(track_number)
} else {
None
},
genre: self
.get_string(&ItemKey::Genre)
.and_then(|genre| GENRES.iter().position(|v| v == &genre).map(|pos| pos as u8)),
}
}
}
impl<'a> Id3v1TagRef<'a> {
pub(crate) fn write_to(&self, file: &mut File) -> Result<()> {
super::write::write_id3v1(file, self)
}
pub(super) fn is_empty(&self) -> bool {
self.title.is_none()
&& self.artist.is_none()
&& self.album.is_none()
&& self.year.is_none()
&& self.comment.is_none()
&& self.track_number.is_none()
&& self.genre.is_none()
}
}

View file

@ -1,17 +1,24 @@
use crate::error::Result;
use crate::types::item::{ItemKey, ItemValue, TagItem};
use crate::types::tag::Tag;
use super::tag::Id3v1TagRef;
use crate::error::{LoftyError, Result};
use crate::logic::id3::find_id3v1;
use crate::probe::Probe;
use crate::types::file::FileType;
use std::fs::File;
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
use byteorder::WriteBytesExt;
pub fn write_id3v1(writer: &mut File, tag: &Tag) -> Result<()> {
// This will seek us to the writing position
let (exists, _) = super::find_id3v1(writer, false)?;
pub(in crate::logic) fn write_id3v1(writer: &mut File, tag: &Id3v1TagRef) -> Result<()> {
match Probe::new().file_type(writer) {
Some(ft) if ft == FileType::APE || ft == FileType::MP3 => {}
_ => return Err(LoftyError::UnsupportedTag),
}
if tag.item_count() == 0 && exists {
// This will seek us to the writing position
let (exists, _) = find_id3v1(writer, false)?;
if tag.is_empty() && exists {
writer.seek(SeekFrom::Start(0))?;
let mut file_bytes = Vec::new();
@ -19,7 +26,7 @@ pub fn write_id3v1(writer: &mut File, tag: &Tag) -> Result<()> {
writer.seek(SeekFrom::Start(0))?;
writer.set_len(0)?;
writer.write_all(&file_bytes[..file_bytes.len() - 129])?;
writer.write_all(&file_bytes[..file_bytes.len() - 128])?;
return Ok(());
}
@ -31,16 +38,16 @@ pub fn write_id3v1(writer: &mut File, tag: &Tag) -> Result<()> {
Ok(())
}
fn encode(tag: &Tag) -> Result<Vec<u8>> {
fn resize_string(item: Option<&TagItem>, size: usize) -> Result<Vec<u8>> {
fn encode(tag: &Id3v1TagRef) -> Result<Vec<u8>> {
fn resize_string(value: Option<&str>, size: usize) -> Result<Vec<u8>> {
let mut cursor = Cursor::new(vec![0; size]);
cursor.seek(SeekFrom::Start(0))?;
if let Some(ItemValue::Text(text)) = item.map(TagItem::value) {
if text.len() > size {
cursor.write_all(text.split_at(size).0.as_bytes())?;
if let Some(val) = value {
if val.len() > size {
cursor.write_all(val.split_at(size).0.as_bytes())?;
} else {
cursor.write_all(text.as_bytes())?;
cursor.write_all(val.as_bytes())?;
}
}
@ -51,72 +58,25 @@ fn encode(tag: &Tag) -> Result<Vec<u8>> {
writer.write_all(&[b'T', b'A', b'G'])?;
let title = resize_string(tag.get_item_ref(&ItemKey::TrackTitle), 30)?;
let title = resize_string(tag.title, 30)?;
writer.write_all(&*title)?;
let artist = resize_string(tag.get_item_ref(&ItemKey::TrackArtist), 30)?;
let artist = resize_string(tag.artist, 30)?;
writer.write_all(&*artist)?;
let album = resize_string(tag.get_item_ref(&ItemKey::AlbumTitle), 30)?;
let album = resize_string(tag.album, 30)?;
writer.write_all(&*album)?;
let year = resize_string(tag.get_item_ref(&ItemKey::Year), 4)?;
let year = resize_string(tag.year, 4)?;
writer.write_all(&*year)?;
let comment = resize_string(tag.get_item_ref(&ItemKey::Comment), 28)?;
let comment = resize_string(tag.comment, 28)?;
writer.write_all(&*comment)?;
writer.write_u8(0)?;
let item_to_byte = |key: &ItemKey, max: u8, empty: u8| {
if let Some(track_number) = tag.get_item_ref(key) {
match track_number.value() {
ItemValue::Text(text) => {
if let Ok(parsed) = text.parse::<u8>() {
if parsed <= max {
return parsed;
}
}
empty
}
ItemValue::UInt(i) => {
if *i <= u32::from(max) {
*i as u8
} else {
empty
}
}
ItemValue::UInt64(i) => {
if *i <= u64::from(max) {
*i as u8
} else {
empty
}
}
ItemValue::Int(i) => {
if i.is_positive() && *i <= i32::from(max) {
*i as u8
} else {
empty
}
}
ItemValue::Int64(i) => {
if i.is_positive() && *i <= i64::from(max) {
*i as u8
} else {
empty
}
}
_ => empty,
}
} else {
empty
}
};
writer.write_u8(item_to_byte(&ItemKey::TrackNumber, 255, 0))?;
writer.write_u8(item_to_byte(&ItemKey::Genre, 191, 255))?;
writer.write_u8(tag.track_number.unwrap_or(0))?;
writer.write_u8(tag.genre.unwrap_or(255))?;
Ok(writer)
}

View file

@ -1,55 +1,39 @@
use super::{Id3v2Frame, LanguageSpecificFrame};
use crate::error::Result;
use crate::error::{LoftyError, Result};
use crate::logic::id3::v2::frame::{EncodedTextFrame, FrameID, FrameValue, LanguageFrame};
use crate::logic::id3::v2::util::text_utils::{decode_text, TextEncoding};
use crate::logic::id3::v2::Id3v2Version;
use crate::types::picture::Picture;
use crate::{ItemKey, ItemValue, LoftyError, TagItem, TagType};
use std::io::Read;
use byteorder::ReadBytesExt;
pub(crate) enum FrameContent {
Picture(Picture),
// For values that only apply to an Id3v2Frame
Item(TagItem),
}
pub(crate) fn parse_content(
content: &mut &[u8],
id: &str,
version: Id3v2Version,
) -> Result<FrameContent> {
) -> Result<(FrameID, FrameValue)> {
Ok(match id {
// The ID was previously upgraded, but the content remains unchanged, so version is necessary
"APIC" => FrameContent::Picture(Picture::from_apic_bytes(content, version)?),
"TXXX" => FrameContent::Item(parse_user_defined(content, false)?),
"WXXX" => FrameContent::Item(parse_user_defined(content, true)?),
"COMM" | "USLT" => FrameContent::Item(parse_text_language(id, content)?),
"SYLT" => FrameContent::Item({
TagItem::new(
ItemKey::Id3v2Specific(Id3v2Frame::SyncText),
ItemValue::Binary(content.to_vec()),
)
}),
"GEOB" => FrameContent::Item({
TagItem::new(
ItemKey::Id3v2Specific(Id3v2Frame::EncapsulatedObject),
ItemValue::Binary(content.to_vec()),
)
}),
_ if id.starts_with('T') => FrameContent::Item(parse_text(id, content)?),
_ if id.starts_with('W') => FrameContent::Item(parse_link(id, content)?),
_ => FrameContent::Item(TagItem::new(
ItemKey::from_key(&TagType::Id3v2, id)
.unwrap_or_else(|| ItemKey::Unknown(id.to_string())),
ItemValue::Binary(content.to_vec()),
)),
"APIC" => (
FrameID::Valid(String::from("APIC")),
FrameValue::Picture(Picture::from_apic_bytes(content, version)?),
),
"TXXX" => parse_user_defined(content, false)?,
"WXXX" => parse_user_defined(content, true)?,
"COMM" | "USLT" => parse_text_language(id, content)?,
_ if id.starts_with('T') => parse_text(id, content)?,
_ if id.starts_with('W') => parse_link(id, content)?,
// SYLT, GEOB, and any unknown frames
_ => (
FrameID::Valid(String::from(id)),
FrameValue::Binary(content.to_vec()),
),
})
}
// There are 2 possibilities for the frame's content: text or link.
fn parse_user_defined(content: &mut &[u8], link: bool) -> Result<TagItem> {
fn parse_user_defined(content: &mut &[u8], link: bool) -> Result<(FrameID, FrameValue)> {
if content.len() < 2 {
return Err(LoftyError::BadFrameLength);
}
@ -65,21 +49,29 @@ fn parse_user_defined(content: &mut &[u8], link: bool) -> Result<TagItem> {
let content =
decode_text(content, TextEncoding::Latin1, false)?.unwrap_or_else(String::new);
TagItem::new(
ItemKey::Id3v2Specific(Id3v2Frame::UserURL(encoding, description)),
ItemValue::Locator(content),
(
FrameID::Valid(String::from("WXXX")),
FrameValue::UserURL(EncodedTextFrame {
encoding,
description,
content,
}),
)
} else {
let content = decode_text(content, encoding, false)?.unwrap_or_else(String::new);
TagItem::new(
ItemKey::Id3v2Specific(Id3v2Frame::UserText(encoding, description)),
ItemValue::Text(content),
(
FrameID::Valid(String::from("TXXX")),
FrameValue::UserText(EncodedTextFrame {
encoding,
description,
content,
}),
)
})
}
fn parse_text_language(id: &str, content: &mut &[u8]) -> Result<TagItem> {
fn parse_text_language(id: &str, content: &mut &[u8]) -> Result<(FrameID, FrameValue)> {
if content.len() < 5 {
return Err(LoftyError::BadFrameLength);
}
@ -98,22 +90,25 @@ fn parse_text_language(id: &str, content: &mut &[u8]) -> Result<TagItem> {
let description = decode_text(content, encoding, true)?;
let content = decode_text(content, encoding, false)?.unwrap_or_else(String::new);
let information = LanguageSpecificFrame {
let information = LanguageFrame {
encoding,
language: lang.to_string(),
description,
description: description.unwrap_or_else(|| String::from("")),
content,
};
let item_key = match id {
"COMM" => ItemKey::Id3v2Specific(Id3v2Frame::Comment(information)),
"USLT" => ItemKey::Id3v2Specific(Id3v2Frame::UnSyncText(information)),
let value = match id {
"COMM" => FrameValue::Comment(information),
"USLT" => FrameValue::UnSyncText(information),
_ => unreachable!(),
};
Ok(TagItem::new(item_key, ItemValue::Text(content)))
let id = FrameID::Valid(String::from(id));
Ok((id, value))
}
fn parse_text(id: &str, content: &mut &[u8]) -> Result<TagItem> {
fn parse_text(id: &str, content: &mut &[u8]) -> Result<(FrameID, FrameValue)> {
let encoding = match TextEncoding::from_u8(content.read_u8()?) {
None => return Err(LoftyError::TextDecode("Found invalid encoding")),
Some(e) => e,
@ -121,17 +116,17 @@ fn parse_text(id: &str, content: &mut &[u8]) -> Result<TagItem> {
let text = decode_text(content, encoding, false)?.unwrap_or_else(String::new);
let key = ItemKey::from_key(&TagType::Id3v2, id)
.unwrap_or_else(|| ItemKey::Id3v2Specific(Id3v2Frame::Text(id.to_string(), encoding)));
Ok(TagItem::new(key, ItemValue::Text(text)))
Ok((
FrameID::Valid(String::from(id)),
FrameValue::Text {
encoding,
value: text,
},
))
}
fn parse_link(id: &str, content: &mut &[u8]) -> Result<TagItem> {
fn parse_link(id: &str, content: &mut &[u8]) -> Result<(FrameID, FrameValue)> {
let link = decode_text(content, TextEncoding::Latin1, false)?.unwrap_or_else(String::new);
let key = ItemKey::from_key(&TagType::Id3v2, id)
.unwrap_or_else(|| ItemKey::Id3v2Specific(Id3v2Frame::URL(id.to_string())));
Ok(TagItem::new(key, ItemValue::Locator(link)))
Ok((FrameID::Valid(String::from(id)), FrameValue::URL(link)))
}

View file

@ -1,10 +1,10 @@
use super::FrameFlags;
use crate::error::{LoftyError, Result};
use crate::logic::id3::v2::util::upgrade::{upgrade_v2, upgrade_v3};
use crate::types::item::TagItemFlags;
use std::io::Read;
pub(crate) fn parse_v2_header<R>(reader: &mut R) -> Result<Option<(String, u32, TagItemFlags)>>
pub(crate) fn parse_v2_header<R>(reader: &mut R) -> Result<Option<(String, u32, FrameFlags)>>
where
R: Read,
{
@ -26,13 +26,13 @@ where
let size = u32::from_be_bytes([0, frame_header[3], frame_header[4], frame_header[5]]);
// V2 doesn't store flags
Ok(Some((id.to_string(), size, TagItemFlags::default())))
Ok(Some((id.to_string(), size, FrameFlags::default())))
}
pub(crate) fn parse_header<R>(
reader: &mut R,
synchsafe: bool,
) -> Result<Option<(String, u32, TagItemFlags)>>
) -> Result<Option<(String, u32, FrameFlags)>>
where
R: Read,
{
@ -77,8 +77,8 @@ where
Ok(Some((id.to_string(), size, flags)))
}
pub(crate) fn parse_flags(flags: u16, v4: bool) -> TagItemFlags {
TagItemFlags {
pub(crate) fn parse_flags(flags: u16, v4: bool) -> FrameFlags {
FrameFlags {
tag_alter_preservation: if v4 {
flags & 0x4000 == 0x4000
} else {

View file

@ -3,39 +3,175 @@ mod header;
pub(in crate::logic::id3::v2) mod read;
use super::util::text_utils::TextEncoding;
use crate::error::{LoftyError, Result};
use crate::logic::id3::v2::util::text_utils::encode_text;
use crate::logic::id3::v2::util::upgrade::{upgrade_v2, upgrade_v3};
use crate::types::item::{ItemKey, ItemValue, TagItem};
use crate::types::picture::Picture;
use crate::types::tag::TagType;
use std::convert::TryFrom;
#[derive(PartialEq, Clone, Debug, Eq, Hash)]
pub struct Frame {
pub(super) id: FrameID,
pub(super) value: FrameValue,
pub(super) flags: FrameFlags,
}
impl Frame {
pub fn new(id: &str, value: FrameValue, flags: FrameFlags) -> Result<Self> {
let id = match id.len() {
// An ID with a length of 4 could be either V3 or V4.
4 => match upgrade_v3(id) {
None => FrameID::Valid(id.to_string()),
Some(id) => FrameID::Valid(id.to_string()),
},
3 => match upgrade_v2(id) {
None => FrameID::Outdated(id.to_string()),
Some(upgraded) => FrameID::Valid(upgraded.to_string()),
},
_ => {
return Err(LoftyError::Id3v2(
"Frame ID has a bad length (!= 3 || != 4)",
))
}
};
match id {
FrameID::Valid(id) | FrameID::Outdated(id) if !id.is_ascii() => {
return Err(LoftyError::Id3v2("Frame ID contains non-ascii characters"))
}
_ => {}
}
Ok(Self { id, value, flags })
}
pub fn id_str(&self) -> &str {
match &self.id {
FrameID::Valid(id) | FrameID::Outdated(id) => id.as_str(),
}
}
pub fn content(&self) -> &FrameValue {
&self.value
}
/// Returns a reference to the [`FrameFlags`]
pub fn flags(&self) -> &FrameFlags {
&self.flags
}
/// Set the item's flags
pub fn set_flags(&mut self, flags: FrameFlags) {
self.flags = flags
}
}
#[derive(PartialEq, Clone, Debug, Eq, Hash)]
/// Information about an ID3v2 frame that requires a language
pub struct LanguageSpecificFrame {
pub struct LanguageFrame {
/// The encoding of the description and comment text
pub encoding: TextEncoding,
/// ISO-639-2 language code (3 bytes)
pub language: String,
/// Unique content description
pub description: Option<String>,
pub description: String,
/// The actual frame content
pub content: String,
}
impl LanguageFrame {
pub fn as_bytes(&self) -> Result<Vec<u8>> {
let mut bytes = vec![self.encoding as u8];
if self.language.len() != 3 || !self.language.is_ascii() {
return Err(LoftyError::Id3v2(
"Invalid frame language found (expected 3 ascii characters)",
));
}
bytes.extend(self.language.as_bytes().iter());
bytes.extend(encode_text(&*self.description, self.encoding, true).iter());
bytes.extend(encode_text(&*self.content, self.encoding, false));
Ok(bytes)
}
}
#[derive(PartialEq, Clone, Debug, Eq, Hash)]
pub struct EncodedTextFrame {
/// The encoding of the description and comment text
pub encoding: TextEncoding,
/// Unique content description
pub description: String,
/// The actual frame content
pub content: String,
}
impl EncodedTextFrame {
pub fn as_bytes(&self) -> Vec<u8> {
let mut bytes = vec![self.encoding as u8];
bytes.extend(encode_text(&*self.description, self.encoding, true).iter());
bytes.extend(encode_text(&*self.content, self.encoding, false));
bytes
}
}
#[derive(PartialEq, Clone, Debug, Eq, Hash)]
/// Different types of ID3v2 frames that require varying amounts of information
pub enum Id3v2Frame {
pub enum FrameID {
Valid(String),
/// When an ID3v2.2 key couldn't be upgraded
///
/// This **will not** be written. It is up to the user to upgrade and store the key as [`Id3v2Frame::Valid`](Self::Valid).
///
/// The entire frame is stored as [`ItemValue::Binary`](crate::ItemValue::Binary).
Outdated(String),
}
impl TryFrom<ItemKey> for FrameID {
type Error = LoftyError;
fn try_from(value: ItemKey) -> std::prelude::rust_2015::Result<Self, Self::Error> {
match value {
ItemKey::Unknown(unknown) if unknown.len() == 4 && unknown.is_ascii() => {
Ok(Self::Valid(unknown.to_ascii_uppercase()))
}
k => k.map_key(&TagType::Id3v2, false).map_or(
Err(LoftyError::Id3v2(
"ItemKey does not meet the requirements to be a FrameID",
)),
|id| Ok(Self::Valid(id.to_string())),
),
}
}
}
#[derive(PartialEq, Clone, Debug, Eq, Hash)]
pub enum FrameValue {
/// Represents a "COMM" frame
///
/// Due to the amount of information needed, it is contained in a separate struct, [`LanguageSpecificFrame`]
Comment(LanguageSpecificFrame),
/// Due to the amount of information needed, it is contained in a separate struct, [`LanguageFrame`]
Comment(LanguageFrame),
/// Represents a "USLT" frame
///
/// Due to the amount of information needed, it is contained in a separate struct, [`LanguageSpecificFrame`]
UnSyncText(LanguageSpecificFrame),
/// Due to the amount of information needed, it is contained in a separate struct, [`LanguageFrame`]
UnSyncText(LanguageFrame),
/// Represents a "T..." (excluding TXXX) frame
///
/// NOTE: Text frame names **must** be unique
///
/// This can be thought of as Text(name, encoding)
Text(String, TextEncoding),
Text {
encoding: TextEncoding,
value: String,
},
/// Represents a "TXXX" frame
///
/// This can be thought of as TXXX(encoding, description), as TXXX frames are often identified by descriptions.
UserText(TextEncoding, String),
/// Due to the amount of information needed, it is contained in a separate struct, [`EncodedTextFrame`]
UserText(EncodedTextFrame),
/// Represents a "W..." (excluding WXXX) frame
///
/// NOTES:
@ -47,18 +183,161 @@ pub enum Id3v2Frame {
URL(String),
/// Represents a "WXXX" frame
///
/// This can be thought of as WXXX(encoding, description), as WXXX frames are often identified by descriptions.
UserURL(TextEncoding, String),
/// Represents a "SYLT" frame
/// Due to the amount of information needed, it is contained in a separate struct, [`EncodedTextFrame`]
UserURL(EncodedTextFrame),
/// Represents an "APIC" or "PIC" frame
Picture(Picture),
/// Binary data
///
/// Nothing is required here, the entire frame is stored as [`ItemValue::Binary`](crate::ItemValue::Binary). For parsing see [`SynchronizedText::parse`](crate::id3::v2::SynchronizedText::parse)
SyncText,
/// Represents a "GEOB" frame
/// NOTES:
///
/// Nothing is required here, the entire frame is stored as [`ItemValue::Binary`](crate::ItemValue::Binary). For parsing see [`GeneralEncapsulatedObject::parse`](crate::id3::v2::GeneralEncapsulatedObject::parse)
EncapsulatedObject,
/// When an ID3v2.2 key couldn't be upgraded
///
/// This **will not** be written. It is up to the user to upgrade and store the key as another variant.
Outdated(String),
/// * This is used for "GEOB" and "SYLT" frames, see
/// [`GeneralEncapsulatedObject::parse`](crate::id3::v2::GeneralEncapsulatedObject::parse) and [`SynchronizedText::parse`](crate::id3::v2::SynchronizedText::parse) respectively
/// * This is used for **all** frames with an ID of [`FrameID::Outdated`]
/// * This is used for unknown frames
Binary(Vec<u8>),
}
impl From<ItemValue> for FrameValue {
fn from(input: ItemValue) -> Self {
match input {
ItemValue::Text(text) => FrameValue::Text {
encoding: TextEncoding::UTF8,
value: text,
},
ItemValue::Locator(locator) => FrameValue::URL(locator),
ItemValue::Binary(binary) => FrameValue::Binary(binary),
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)]
#[allow(clippy::struct_excessive_bools)]
/// Various flags to describe the content of an item
pub struct FrameFlags {
/// Preserve frame on tag edit
pub tag_alter_preservation: bool,
/// Preserve frame on file edit
pub file_alter_preservation: bool,
/// Item cannot be written to
pub read_only: bool,
/// 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),
/// Frame is zlib compressed
///
/// It is **required** `data_length_indicator` be set if this is set.
pub compression: bool,
/// Frame is encrypted
///
/// NOTE: Since the encryption method is unknown, lofty cannot do anything with these frames
///
/// In addition to setting this flag, an encryption method symbol must be added.
/// The method symbol **must** be > 0x80.
pub encryption: (bool, u8),
/// 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,
/// Frame has a data length indicator
///
/// The data length indicator is the size of the frame if the flags were all zeroed out.
/// This is usually used in combination with `compression` and `encryption` (depending on encryption method).
///
/// If using encryption, the final size must be added. It will be ignored if using compression.
pub data_length_indicator: (bool, u32),
}
pub(crate) struct FrameRef<'a> {
pub id: &'a str,
pub value: FrameValueRef<'a>,
pub flags: FrameFlags,
}
impl<'a> Frame {
pub(crate) fn as_opt_ref(&'a self) -> Option<FrameRef<'a>> {
if let FrameID::Valid(id) = &self.id {
Some(FrameRef {
id,
value: (&self.value).into(),
flags: self.flags,
})
} else {
None
}
}
}
impl<'a> TryFrom<&'a TagItem> for FrameRef<'a> {
type Error = LoftyError;
fn try_from(value: &'a TagItem) -> std::prelude::rust_2015::Result<Self, Self::Error> {
let id = match value.key() {
ItemKey::Unknown(unknown)
if unknown.len() == 4
&& unknown.is_ascii()
&& unknown.chars().all(|c| c.is_ascii_uppercase()) =>
{
Ok(unknown.as_str())
}
k => k.map_key(&TagType::Id3v2, false).ok_or(LoftyError::Id3v2(
"ItemKey does not meet the requirements to be a FrameID",
)),
}?;
Ok(FrameRef {
id,
value: Into::<FrameValueRef<'a>>::into(value.value()),
flags: FrameFlags::default(),
})
}
}
pub(crate) enum FrameValueRef<'a> {
Comment(&'a LanguageFrame),
UnSyncText(&'a LanguageFrame),
Text {
encoding: TextEncoding,
value: &'a str,
},
UserText(&'a EncodedTextFrame),
URL(&'a str),
UserURL(&'a EncodedTextFrame),
Picture(&'a Picture),
Binary(&'a [u8]),
}
impl<'a> Into<FrameValueRef<'a>> for &'a FrameValue {
fn into(self) -> FrameValueRef<'a> {
match self {
FrameValue::Comment(lf) => FrameValueRef::Comment(lf),
FrameValue::UnSyncText(lf) => FrameValueRef::UnSyncText(lf),
FrameValue::Text { encoding, value } => FrameValueRef::Text {
encoding: *encoding,
value: value.as_str(),
},
FrameValue::UserText(etf) => FrameValueRef::UserText(etf),
FrameValue::URL(url) => FrameValueRef::URL(url.as_str()),
FrameValue::UserURL(etf) => FrameValueRef::UserURL(etf),
FrameValue::Picture(pic) => FrameValueRef::Picture(pic),
FrameValue::Binary(bin) => FrameValueRef::Binary(bin.as_slice()),
}
}
}
impl<'a> Into<FrameValueRef<'a>> for &'a ItemValue {
fn into(self) -> FrameValueRef<'a> {
match self {
ItemValue::Text(text) => FrameValueRef::Text {
encoding: TextEncoding::UTF8,
value: text.as_str(),
},
ItemValue::Locator(locator) => FrameValueRef::URL(locator.as_str()),
ItemValue::Binary(binary) => FrameValueRef::Binary(binary.as_slice()),
}
}
}

View file

@ -1,18 +1,13 @@
use super::header::{parse_header, parse_v2_header};
use super::Frame;
use crate::error::{LoftyError, Result};
use crate::logic::id3::v2::frame::content::{parse_content, FrameContent};
use crate::logic::id3::v2::frame::content::parse_content;
use crate::logic::id3::v2::Id3v2Version;
use crate::types::item::TagItemFlags;
use std::io::Read;
use byteorder::{BigEndian, ReadBytesExt};
pub(crate) struct Frame {
pub flags: TagItemFlags,
pub content: FrameContent,
}
impl Frame {
pub(crate) fn read<R>(reader: &mut R, version: Id3v2Version) -> Result<Option<Self>>
where
@ -59,9 +54,8 @@ impl Frame {
flags.data_length_indicator.1 = content_reader.read_u32::<BigEndian>()?;
}
Ok(Some(Self {
flags,
content: parse_content(&mut content_reader, &*id, version)?,
}))
let (id, value) = parse_content(&mut content_reader, &*id, version)?;
Ok(Some(Self { id, value, flags }))
}
}

View file

@ -83,7 +83,7 @@ impl TagRestrictions {
/// Read a [`TagRestrictions`] from a byte
///
/// NOTE: See https://id3.org/id3v2.4.0-structure section 3.2, item d
pub fn parse(byte: u8) -> Self {
pub fn from_byte(byte: u8) -> Self {
let mut restrictions = TagRestrictions::default();
let restriction_flags = byte;
@ -134,7 +134,7 @@ impl TagRestrictions {
restrictions
}
/// Convert a [`TagRestrictions`] into a byte Vec
/// Convert a [`TagRestrictions`] into a `u8`
///
/// NOTE: This does not include a frame header
pub fn as_bytes(&self) -> u8 {

View file

@ -1,3 +1,10 @@
pub(crate) mod frame;
pub(crate) mod items;
pub(crate) mod read;
pub(crate) mod tag;
pub(crate) mod util;
pub(in crate::logic) mod write;
use crate::error::Result;
use crate::logic::id3::unsynch_u32;
@ -5,12 +12,6 @@ use std::io::{Read, Seek, SeekFrom};
use byteorder::{BigEndian, ByteOrder};
pub(crate) mod frame;
pub(crate) mod items;
pub(crate) mod read;
pub(crate) mod util;
pub(in crate::logic) mod write;
#[derive(PartialEq, Debug, Clone, Copy)]
/// The ID3v2 version
pub enum Id3v2Version {

View file

@ -1,18 +1,17 @@
use super::frame::read::Frame;
use crate::error::Result;
use super::frame::Frame;
use super::tag::Id3v2Tag;
use super::tag::Id3v2TagFlags;
use crate::error::{LoftyError, Result};
use crate::logic::id3::unsynch_u32;
use crate::logic::id3::v2::frame::content::FrameContent;
#[cfg(feature = "id3v2_restrictions")]
use crate::logic::id3::v2::items::restrictions::TagRestrictions;
use crate::logic::id3::v2::Id3v2Version;
use crate::types::tag::{Tag, TagFlags};
use crate::{LoftyError, TagType};
use std::io::Read;
use byteorder::{BigEndian, ReadBytesExt};
pub(crate) fn parse_id3v2(bytes: &mut &[u8]) -> Result<Tag> {
pub(crate) fn parse_id3v2(bytes: &mut &[u8]) -> Result<Id3v2Tag> {
let mut header = [0; 10];
bytes.read_exact(&mut header)?;
@ -37,7 +36,7 @@ pub(crate) fn parse_id3v2(bytes: &mut &[u8]) -> Result<Tag> {
return Err(LoftyError::Id3v2("Encountered a compressed ID3v2.2 tag"));
}
let mut flags_parsed = TagFlags {
let mut flags_parsed = Id3v2TagFlags {
unsynchronisation: flags & 0x80 == 0x80,
experimental: (version == Id3v2Version::V4 || version == Id3v2Version::V3)
&& flags & 0x20 == 0x20,
@ -82,27 +81,17 @@ pub(crate) fn parse_id3v2(bytes: &mut &[u8]) -> Result<Tag> {
// We don't care about the length byte, it is always 1
let _data_length = bytes.read_u8()?;
flags_parsed.restrictions.1 = TagRestrictions::parse(bytes.read_u8()?);
flags_parsed.restrictions.1 = TagRestrictions::from_byte(bytes.read_u8()?);
}
}
let mut tag = {
let mut tag = Tag::new(TagType::Id3v2);
tag.set_flags(flags_parsed);
tag
};
let mut tag = Id3v2Tag::default();
tag.set_flags(flags_parsed);
loop {
match Frame::read(bytes, version)? {
None => break,
Some(f) => match f.content {
FrameContent::Picture(pic) => tag.push_picture(pic),
FrameContent::Item(mut item) => {
item.set_flags(f.flags);
tag.insert_item_unchecked(item)
}
},
Some(f) => drop(tag.insert(f)),
}
}

199
src/logic/id3/v2/tag.rs Normal file
View file

@ -0,0 +1,199 @@
use super::frame::{EncodedTextFrame, LanguageFrame};
use super::frame::{Frame, FrameFlags, FrameValue};
#[cfg(feature = "id3v2_restrictions")]
use super::items::restrictions::TagRestrictions;
use super::Id3v2Version;
use crate::error::Result;
use crate::logic::id3::v2::frame::FrameRef;
use crate::types::item::{ItemKey, ItemValue, TagItem};
use crate::types::tag::{Tag, TagType};
use std::convert::TryInto;
use std::fs::File;
use byteorder::ByteOrder;
#[derive(Default)]
pub struct Id3v2Tag {
flags: Id3v2TagFlags,
frames: Vec<Frame>,
}
impl Id3v2Tag {
/// Returns the [`Id3v2TagFlags`]
pub fn flags(&self) -> &Id3v2TagFlags {
&self.flags
}
/// Restrict the tag's flags
pub fn set_flags(&mut self, flags: Id3v2TagFlags) {
self.flags = flags
}
}
impl Id3v2Tag {
pub fn iter(&self) -> impl Iterator<Item = &Frame> {
self.frames.iter()
}
pub fn len(&self) -> usize {
self.frames.len()
}
pub fn is_empty(&self) -> bool {
self.frames.is_empty()
}
pub fn get(&self, id: &str) -> Option<&Frame> {
self.frames.iter().find(|f| f.id_str() == id)
}
pub fn insert(&mut self, frame: Frame) -> Option<Frame> {
let replaced = self
.frames
.iter()
.position(|f| f == &frame)
.map(|pos| self.frames.remove(pos));
self.frames.push(frame);
replaced
}
pub fn remove(&mut self, id: &str) {
self.frames.retain(|f| f.id_str() != id)
}
}
impl Id3v2Tag {
pub fn write_to(&self, file: &mut File) -> Result<()> {
Into::<Id3v2TagRef>::into(self).write_to(file)
}
pub fn write_to_chunk_file<B: ByteOrder>(&self, file: &mut File) -> Result<()> {
Into::<Id3v2TagRef>::into(self).write_to_chunk_file::<B>(file)
}
}
impl IntoIterator for Id3v2Tag {
type Item = Frame;
type IntoIter = std::vec::IntoIter<Frame>;
fn into_iter(self) -> Self::IntoIter {
self.frames.into_iter()
}
}
impl From<Id3v2Tag> for Tag {
fn from(input: Id3v2Tag) -> Self {
let mut tag = Self::new(TagType::Id3v2);
for frame in input.frames {
let item_key = ItemKey::from_key(&TagType::Id3v2, frame.id_str());
let item_value = match frame.value {
FrameValue::Comment(LanguageFrame { content, .. })
| FrameValue::UnSyncText(LanguageFrame { content, .. })
| FrameValue::Text { value: content, .. }
| FrameValue::UserText(EncodedTextFrame { content, .. }) => ItemValue::Text(content),
FrameValue::URL(content)
| FrameValue::UserURL(EncodedTextFrame { content, .. }) => ItemValue::Locator(content),
FrameValue::Picture(pic) => {
ItemValue::Binary(if let Ok(bin) = pic.as_apic_bytes(Id3v2Version::V4) {
bin
} else {
continue;
})
}
FrameValue::Binary(binary) => ItemValue::Binary(binary),
};
tag.insert_item_unchecked(TagItem::new(item_key, item_value))
}
tag
}
}
impl From<Tag> for Id3v2Tag {
fn from(input: Tag) -> Self {
let mut id3v2_tag = Self::default();
for item in input.items {
let id = match item.item_key.try_into() {
Ok(id) => id,
Err(_) => continue,
};
let frame_value: FrameValue = item.item_value.into();
id3v2_tag.frames.push(Frame {
id,
value: frame_value,
flags: FrameFlags::default(),
});
}
id3v2_tag
}
}
#[derive(Default, Copy, Clone)]
#[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<dyn Iterator<Item = FrameRef<'a>> + 'a>,
}
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<B: ByteOrder>(
&mut self,
file: &mut File,
) -> Result<()> {
super::write::write_id3v2_to_chunk_file::<B>(file, self)
}
}
impl<'a> Into<Id3v2TagRef<'a>> for &'a Tag {
fn into(self) -> Id3v2TagRef<'a> {
Id3v2TagRef {
flags: Id3v2TagFlags::default(),
frames: Box::new(
self.items()
.iter()
.map(TryInto::<FrameRef>::try_into)
.filter_map(Result::ok),
),
}
}
}
impl<'a> Into<Id3v2TagRef<'a>> for &'a Id3v2Tag {
fn into(self) -> Id3v2TagRef<'a> {
Id3v2TagRef {
flags: self.flags,
frames: Box::new(self.frames.iter().filter_map(Frame::as_opt_ref)),
}
}
}

View file

@ -7,6 +7,7 @@ use byteorder::ReadBytesExt;
/// The text encoding for use in ID3v2 frames
#[derive(Debug, Clone, Eq, PartialEq, Copy, Hash)]
#[repr(u8)]
pub enum TextEncoding {
/// ISO-8859-1
Latin1 = 0,

View file

@ -9,6 +9,8 @@ pub(in crate::logic::id3::v2) fn write_to_chunk_file<B>(data: &mut File, tag: &[
where
B: ByteOrder,
{
data.seek(SeekFrom::Current(12))?;
let mut id3v2_chunk = (None, None);
let mut fourcc = [0; 4];
@ -22,9 +24,14 @@ where
data.seek(SeekFrom::Current(i64::from(size)))?;
}
if let (Some(chunk_start), Some(chunk_size)) = id3v2_chunk {
if let (Some(chunk_start), Some(mut chunk_size)) = id3v2_chunk {
data.seek(SeekFrom::Start(0))?;
// We need to remove the padding byte if it exists
if chunk_size % 2 != 0 {
chunk_size += 1;
}
let mut file_bytes = Vec::new();
data.read_to_end(&mut file_bytes)?;
@ -44,7 +51,14 @@ where
data.write_u32::<B>(tag.len() as u32)?;
data.write_all(tag)?;
// It is required an odd length chunk be padded with a 0
// The 0 isn't included in the chunk size, however
if tag.len() % 2 != 0 {
data.write_u8(0)?;
}
let total_size = data.seek(SeekFrom::Current(0))? - 8;
data.seek(SeekFrom::Start(4))?;
data.write_u32::<B>(total_size as u32)?;

View file

@ -1,137 +1,100 @@
use crate::error::{LoftyError, Result};
use crate::id3::v2::Id3v2Version;
use crate::logic::id3::synch_u32;
use crate::logic::id3::v2::frame::{Id3v2Frame, LanguageSpecificFrame};
use crate::logic::id3::v2::util::text_utils::{encode_text, TextEncoding};
use crate::types::item::{ItemKey, ItemValue, TagItem, TagItemFlags};
use crate::types::tag::TagType;
use crate::logic::id3::v2::frame::{FrameFlags, FrameRef, FrameValueRef};
use std::io::Write;
use byteorder::{BigEndian, WriteBytesExt};
enum FrameType<'a> {
EncodedText(TextEncoding),
LanguageDependent(&'a LanguageSpecificFrame),
UserDefined(TextEncoding, &'a str),
Other,
}
pub(in crate::logic::id3::v2) fn create_items<W>(writer: &mut W, items: &[TagItem]) -> Result<()>
pub(in crate::logic::id3::v2) fn create_items<'a, W>(
writer: &mut W,
frames: &mut dyn Iterator<Item = FrameRef<'a>>,
) -> Result<()>
where
W: Write,
{
// Get rid of any invalid keys
let items = items.iter().filter(|i| {
(match i.key() {
ItemKey::Id3v2Specific(Id3v2Frame::Text(name, _)) => {
name.starts_with('T') && name.is_ascii() && name.len() == 4
for frame in frames {
let value = match frame.value {
FrameValueRef::Comment(content) | FrameValueRef::UnSyncText(content) => {
content.as_bytes()?
}
ItemKey::Id3v2Specific(Id3v2Frame::URL(name)) => {
name.starts_with('W') && name.is_ascii() && name.len() == 4
}
ItemKey::Id3v2Specific(id3v2_frame) => {
std::mem::discriminant(&Id3v2Frame::Outdated(String::new()))
!= std::mem::discriminant(id3v2_frame)
}
ItemKey::Unknown(_) => false,
key => key.map_key(&TagType::Id3v2).is_some(),
}) && matches!(
i.value(),
ItemValue::Text(_) | ItemValue::Locator(_) | ItemValue::Binary(_)
)
});
FrameValueRef::Text { value, encoding } => {
let mut v = vec![encoding as u8];
// Get rid of any invalid keys
for item in items {
let value = match item.value() {
ItemValue::Text(text) => text.as_bytes(),
ItemValue::Locator(locator) => locator.as_bytes(),
ItemValue::Binary(binary) => binary,
_ => unreachable!(),
v.extend_from_slice(value.as_bytes());
v
}
FrameValueRef::UserText(content) | FrameValueRef::UserURL(content) => {
content.as_bytes()
}
FrameValueRef::URL(link) => link.as_bytes().to_vec(),
FrameValueRef::Picture(pic) => pic.as_apic_bytes(Id3v2Version::V4)?,
FrameValueRef::Binary(binary) => binary.to_vec(),
};
let flags = item.flags();
match item.key() {
ItemKey::Id3v2Specific(frame) => match frame {
Id3v2Frame::Comment(details) => write_frame(
writer,
&FrameType::LanguageDependent(details),
"COMM",
flags,
0,
value,
)?,
Id3v2Frame::UnSyncText(details) => write_frame(
writer,
&FrameType::LanguageDependent(details),
"USLT",
flags,
0,
value,
)?,
Id3v2Frame::Text(name, encoding) => write_frame(
writer,
&FrameType::EncodedText(*encoding),
name,
flags,
// Encoding
1,
value,
)?,
Id3v2Frame::UserText(encoding, descriptor) => write_frame(
writer,
&FrameType::UserDefined(*encoding, descriptor),
"TXXX",
flags,
// Encoding + descriptor + null terminator
2 + descriptor.len() as u32,
value,
)?,
Id3v2Frame::URL(name) => {
write_frame(writer, &FrameType::Other, name, flags, 0, value)?
}
Id3v2Frame::UserURL(encoding, descriptor) => write_frame(
writer,
&FrameType::UserDefined(*encoding, descriptor),
"WXXX",
flags,
// Encoding + descriptor + null terminator
2 + descriptor.len() as u32,
value,
)?,
Id3v2Frame::SyncText => {
write_frame(writer, &FrameType::Other, "SYLT", flags, 0, value)?
}
Id3v2Frame::EncapsulatedObject => {
write_frame(writer, &FrameType::Other, "GEOB", flags, 0, value)?
}
_ => {}
},
key => {
let key = key.map_key(&TagType::Id3v2).unwrap();
if key.starts_with('T') {
write_frame(
writer,
&FrameType::EncodedText(TextEncoding::UTF8),
key,
flags,
// Encoding
1,
value,
)?;
} else {
write_frame(writer, &FrameType::Other, key, flags, 0, value)?;
}
}
}
write_frame(writer, frame.id, frame.flags, &value)?;
}
Ok(())
}
fn write_frame_header<W>(writer: &mut W, name: &str, len: u32, flags: &TagItemFlags) -> Result<()>
fn write_frame<W>(writer: &mut W, name: &str, flags: FrameFlags, value: &[u8]) -> Result<()>
where
W: Write,
{
if flags.encryption.0 {
write_encrypted(writer, name, value, flags)?;
return Ok(());
}
let len = value.len() as u32;
let is_grouping_identity = flags.grouping_identity.0;
write_frame_header(
writer,
name,
if is_grouping_identity { len + 1 } else { len },
flags,
)?;
if is_grouping_identity {
writer.write_u8(flags.grouping_identity.1)?;
}
writer.write_all(value)?;
Ok(())
}
fn write_encrypted<W>(writer: &mut W, name: &str, value: &[u8], flags: FrameFlags) -> Result<()>
where
W: Write,
{
let method_symbol = flags.encryption.1;
let data_length_indicator = flags.data_length_indicator;
if method_symbol > 0x80 {
return Err(LoftyError::Id3v2(
"Attempted to write an encrypted frame with an invalid method symbol (> 0x80)",
));
}
if data_length_indicator.0 && data_length_indicator.1 > 0 {
write_frame_header(writer, name, (value.len() + 1) as u32, flags)?;
writer.write_u32::<BigEndian>(synch_u32(data_length_indicator.1)?)?;
writer.write_u8(method_symbol)?;
writer.write_all(value)?;
return Ok(());
}
Err(LoftyError::Id3v2(
"Attempted to write an encrypted frame without a data length indicator",
))
}
fn write_frame_header<W>(writer: &mut W, name: &str, len: u32, flags: FrameFlags) -> Result<()>
where
W: Write,
{
@ -142,10 +105,10 @@ where
Ok(())
}
fn get_flags(tag_flags: &TagItemFlags) -> u16 {
fn get_flags(tag_flags: FrameFlags) -> u16 {
let mut flags = 0;
if tag_flags == &TagItemFlags::default() {
if tag_flags == FrameFlags::default() {
return flags;
}
@ -183,97 +146,3 @@ fn get_flags(tag_flags: &TagItemFlags) -> u16 {
flags
}
fn write_frame<W>(
writer: &mut W,
frame_type: &FrameType,
name: &str,
flags: &TagItemFlags,
// Any additional bytes, such as encoding or language code
additional_len: u32,
value: &[u8],
) -> Result<()>
where
W: Write,
{
if flags.encryption.0 {
write_encrypted(writer, name, value, flags)?;
return Ok(());
}
let len = value.len() as u32 + additional_len;
let is_grouping_identity = flags.grouping_identity.0;
write_frame_header(
writer,
name,
if is_grouping_identity { len + 1 } else { len },
flags,
)?;
if is_grouping_identity {
writer.write_u8(flags.grouping_identity.1)?;
}
match frame_type {
FrameType::EncodedText(encoding) => {
writer.write_u8(*encoding as u8)?;
writer.write_all(value)?;
}
FrameType::LanguageDependent(details) => {
writer.write_u8(details.encoding as u8)?;
if details.language.len() == 3 {
writer.write_all(details.language.as_bytes())?;
} else {
return Err(LoftyError::Id3v2(
"Attempted to write a LanguageSpecificFrame with an invalid language String \
length (!= 3)",
));
}
if let Some(ref descriptor) = details.description {
writer.write_all(&encode_text(descriptor, details.encoding, true))?;
} else {
writer.write_u8(0)?;
}
writer.write_all(value)?;
}
FrameType::UserDefined(encoding, descriptor) => {
writer.write_u8(*encoding as u8)?;
writer.write_all(&encode_text(descriptor, *encoding, true))?;
writer.write_all(value)?;
}
FrameType::Other => writer.write_all(value)?,
}
Ok(())
}
fn write_encrypted<W>(writer: &mut W, name: &str, value: &[u8], flags: &TagItemFlags) -> Result<()>
where
W: Write,
{
let method_symbol = flags.encryption.1;
let data_length_indicator = flags.data_length_indicator;
if method_symbol > 0x80 {
return Err(LoftyError::Id3v2(
"Attempted to write an encrypted frame with an invalid method symbol (> 0x80)",
));
}
if data_length_indicator.0 && data_length_indicator.1 > 0 {
write_frame_header(writer, name, (value.len() + 1) as u32, flags)?;
writer.write_u32::<BigEndian>(synch_u32(data_length_indicator.1)?)?;
writer.write_u8(method_symbol)?;
writer.write_all(value)?;
return Ok(());
}
Err(LoftyError::Id3v2(
"Attempted to write an encrypted frame without a data length indicator",
))
}

View file

@ -2,29 +2,21 @@ mod chunk_file;
mod frame;
use super::find_id3v2;
use crate::error::Result;
use crate::error::{LoftyError, Result};
use crate::logic::id3::synch_u32;
use crate::types::tag::{Tag, TagFlags};
use crate::logic::id3::v2::tag::{Id3v2TagFlags, Id3v2TagRef};
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};
pub(in crate::logic) fn write_id3v2(data: &mut File, tag: &Tag) -> Result<()> {
if tag.item_count() == 0 {
find_id3v2(data, false)?;
if data.seek(SeekFrom::Current(0))? != 0 {
let mut file_bytes = Vec::new();
data.read_to_end(&mut file_bytes)?;
data.seek(SeekFrom::Start(0))?;
data.set_len(0)?;
data.write_all(&*file_bytes)?;
}
return Ok(());
pub(in crate::logic) fn write_id3v2(data: &mut File, tag: &mut Id3v2TagRef) -> Result<()> {
match Probe::new().file_type(data) {
Some(ft) if ft == FileType::APE || ft == FileType::MP3 => {}
_ => return Err(LoftyError::UnsupportedTag),
}
let id3v2 = create_tag(tag)?;
@ -45,26 +37,38 @@ pub(in crate::logic) fn write_id3v2(data: &mut File, tag: &Tag) -> Result<()> {
}
// Formats such as WAV and AIFF store the ID3v2 tag in an 'ID3 ' chunk rather than at the beginning of the file
pub(in crate::logic) fn write_id3v2_to_chunk_file<B>(data: &mut File, tag: &Tag) -> Result<()>
pub(in crate::logic) fn write_id3v2_to_chunk_file<B>(
data: &mut File,
tag: &mut Id3v2TagRef,
) -> Result<()>
where
B: ByteOrder,
{
let id3v2 = if tag.item_count() == 0 {
Vec::new()
} else {
create_tag(tag)?
};
match Probe::new().file_type(data) {
Some(ft) if ft == FileType::WAV || ft == FileType::AIFF => {}
_ => return Err(LoftyError::UnsupportedTag),
}
let id3v2 = create_tag(tag)?;
chunk_file::write_to_chunk_file::<B>(data, &id3v2)?;
Ok(())
}
fn create_tag(tag: &Tag) -> Result<Vec<u8>> {
let mut id3v2 = create_tag_header(tag.flags())?;
fn create_tag(tag: &mut Id3v2TagRef) -> Result<Vec<u8>> {
let frames = &mut tag.frames;
let mut peek = frames.peekable();
if peek.peek().is_none() {
return Ok(Vec::new());
}
let mut id3v2 = create_tag_header(tag.flags)?;
let header_len = id3v2.get_ref().len();
// Write the items
frame::create_items(&mut id3v2, tag.items())?;
frame::create_items(&mut id3v2, &mut peek)?;
let len = id3v2.get_ref().len() - header_len;
@ -76,7 +80,7 @@ fn create_tag(tag: &Tag) -> Result<Vec<u8>> {
}
#[allow(clippy::trivially_copy_pass_by_ref)]
fn create_tag_header(flags: &TagFlags) -> Result<Cursor<Vec<u8>>> {
fn create_tag_header(flags: Id3v2TagFlags) -> Result<Cursor<Vec<u8>>> {
let mut header = Cursor::new(Vec::new());
header.write_all(&[b'I', b'D', b'3'])?;

View file

@ -1,11 +1,16 @@
mod properties;
mod read;
mod tag;
#[cfg(feature = "aiff_text_chunks")]
pub(crate) mod tag;
pub(in crate::logic) mod write;
use crate::error::Result;
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::{Tag, TagType};
use crate::types::tag::TagType;
use tag::AiffTextChunks;
use std::io::{Read, Seek};
@ -13,10 +18,10 @@ use std::io::{Read, Seek};
pub struct AiffFile {
#[cfg(feature = "aiff_text_chunks")]
/// Any text chunks included in the file
pub(crate) text_chunks: Option<Tag>,
pub(crate) text_chunks: Option<AiffTextChunks>,
#[cfg(feature = "id3v2")]
/// An ID3v2 tag
pub(crate) id3v2: Option<Tag>,
pub(crate) id3v2_tag: Option<Id3v2Tag>,
/// The file's audio properties
pub(crate) properties: FileProperties,
}
@ -26,10 +31,13 @@ impl From<AiffFile> for TaggedFile {
Self {
ty: FileType::AIFF,
properties: input.properties,
tags: vec![input.text_chunks, input.id3v2]
.into_iter()
.flatten()
.collect(),
tags: vec![
input.text_chunks.map(|tc| tc.into()),
input.id3v2_tag.map(|id3| id3.into()),
]
.into_iter()
.flatten()
.collect(),
}
}
}
@ -50,40 +58,18 @@ impl AudioFile for AiffFile {
}
fn contains_tag(&self) -> bool {
self.id3v2.is_some() || self.text_chunks.is_some()
self.id3v2_tag.is_some() || self.text_chunks.is_some()
}
fn contains_tag_type(&self, tag_type: &TagType) -> bool {
match tag_type {
TagType::Id3v2 => self.id3v2.is_some(),
TagType::Id3v2 => self.id3v2_tag.is_some(),
TagType::AiffText => self.text_chunks.is_some(),
_ => false,
}
}
}
impl AiffFile {
#[cfg(feature = "id3v2")]
/// Returns a reference to the ID3v2 tag if it exists
pub fn id3v2_tag(&self) -> Option<&Tag> {
self.id3v2.as_ref()
}
#[cfg(feature = "id3v2")]
/// Returns a mutable reference to the ID3v2 tag if it exists
pub fn id3v2_tag_mut(&mut self) -> Option<&mut Tag> {
self.id3v2.as_mut()
}
#[cfg(feature = "aiff_text_chunks")]
/// Returns a reference to the text chunks tag if it exists
pub fn text_chunks(&self) -> Option<&Tag> {
self.text_chunks.as_ref()
}
#[cfg(feature = "aiff_text_chunks")]
/// Returns a mutable reference to the text chunks tag if it exists
pub fn text_chunks_mut(&mut self) -> Option<&mut Tag> {
self.text_chunks.as_mut()
}
tag_methods! {
AiffFile => ID3v2, id3v2_tag, Id3v2Tag; Text_Chunks, text_chunks, AiffTextChunks
}

View file

@ -0,0 +1,65 @@
use crate::error::{LoftyError, Result};
use crate::types::properties::FileProperties;
use std::io::Read;
use std::time::Duration;
use byteorder::{BigEndian, ReadBytesExt};
pub(super) fn read_properties(comm: &mut &[u8], stream_len: u32) -> Result<FileProperties> {
let channels = comm.read_u16::<BigEndian>()? as u8;
if channels == 0 {
return Err(LoftyError::Aiff("File contains 0 channels"));
}
let sample_frames = comm.read_u32::<BigEndian>()?;
let _sample_size = comm.read_u16::<BigEndian>()?;
let mut sample_rate_bytes = [0; 10];
comm.read_exact(&mut sample_rate_bytes)?;
let sign = u64::from(sample_rate_bytes[0] & 0x80);
sample_rate_bytes[0] &= 0x7f;
let mut exponent = u16::from(sample_rate_bytes[0]) << 8 | u16::from(sample_rate_bytes[1]);
exponent = exponent - 16383 + 1023;
let fraction = &mut sample_rate_bytes[2..];
fraction[0] &= 0x7f;
let fraction: Vec<u64> = fraction.iter_mut().map(|v| u64::from(*v)).collect();
let fraction = fraction[0] << 56
| fraction[1] << 48
| fraction[2] << 40
| fraction[3] << 32
| fraction[4] << 24
| fraction[5] << 16
| fraction[6] << 8
| fraction[7];
let f64_bytes = sign << 56 | u64::from(exponent) << 52 | fraction >> 11;
let float = f64::from_be_bytes(f64_bytes.to_be_bytes());
let sample_rate = float.round() as u32;
let (duration, bitrate) = if sample_rate > 0 && sample_frames > 0 {
let length = (u64::from(sample_frames) * 1000) / u64::from(sample_rate);
(
Duration::from_millis(length),
(u64::from(stream_len * 8) / length) as u32,
)
} else {
(Duration::ZERO, 0)
};
Ok(FileProperties::new(
duration,
Some(bitrate),
Some(sample_rate),
Some(channels),
))
}

View file

@ -1,12 +1,13 @@
#[cfg(feature = "aiff_text_chunks")]
use super::tag::AiffTextChunks;
use super::AiffFile;
use crate::error::{LoftyError, Result};
#[cfg(feature = "id3v2")]
use crate::logic::id3::v2::read::parse_id3v2;
use crate::types::item::{ItemKey, ItemValue, TagItem};
use crate::types::properties::FileProperties;
use crate::types::tag::{Tag, TagType};
#[cfg(feature = "id3v2")]
use crate::logic::id3::v2::tag::Id3v2Tag;
use std::io::{Read, Seek, SeekFrom};
use std::time::Duration;
use byteorder::{BigEndian, ReadBytesExt};
@ -24,64 +25,6 @@ where
Ok(())
}
fn read_properties(comm: &mut &[u8], stream_len: u32) -> Result<FileProperties> {
let channels = comm.read_u16::<BigEndian>()? as u8;
if channels == 0 {
return Err(LoftyError::Aiff("File contains 0 channels"));
}
let sample_frames = comm.read_u32::<BigEndian>()?;
let _sample_size = comm.read_u16::<BigEndian>()?;
let mut sample_rate_bytes = [0; 10];
comm.read_exact(&mut sample_rate_bytes)?;
let sign = u64::from(sample_rate_bytes[0] & 0x80);
sample_rate_bytes[0] &= 0x7f;
let mut exponent = u16::from(sample_rate_bytes[0]) << 8 | u16::from(sample_rate_bytes[1]);
exponent = exponent - 16383 + 1023;
let fraction = &mut sample_rate_bytes[2..];
fraction[0] &= 0x7f;
let fraction: Vec<u64> = fraction.iter_mut().map(|v| u64::from(*v)).collect();
let fraction = fraction[0] << 56
| fraction[1] << 48
| fraction[2] << 40
| fraction[3] << 32
| fraction[4] << 24
| fraction[5] << 16
| fraction[6] << 8
| fraction[7];
let f64_bytes = sign << 56 | u64::from(exponent) << 52 | fraction >> 11;
let float = f64::from_be_bytes(f64_bytes.to_be_bytes());
let sample_rate = float.round() as u32;
let (duration, bitrate) = if sample_rate > 0 && sample_frames > 0 {
let length = (u64::from(sample_frames) * 1000) / u64::from(sample_rate);
(
Duration::from_millis(length),
(u64::from(stream_len * 8) / length) as u32,
)
} else {
(Duration::ZERO, 0)
};
Ok(FileProperties::new(
duration,
Some(bitrate),
Some(sample_rate),
Some(channels),
))
}
pub(in crate::logic) fn read_from<R>(data: &mut R) -> Result<AiffFile>
where
R: Read + Seek,
@ -91,26 +34,30 @@ where
let mut comm = None;
let mut stream_len = 0;
let mut text_chunks = Tag::new(TagType::AiffText);
let mut id3: Option<Tag> = None;
#[cfg(feature = "aiff_text_chunks")]
let mut text_chunks = AiffTextChunks::default();
#[cfg(feature = "id3v2")]
let mut id3v2_tag: Option<Id3v2Tag> = None;
let mut fourcc = [0; 4];
while let (Ok(()), Ok(size)) = (data.read_exact(&mut fourcc), data.read_u32::<BigEndian>()) {
match &fourcc {
#[cfg(feature = "aiff_text_chunks")]
b"NAME" | b"AUTH" | b"(c) " => {
let mut value = vec![0; size as usize];
data.read_exact(&mut value)?;
// It's safe to unwrap here since this code is unreachable unless the fourcc is valid
let item = TagItem::new(
ItemKey::from_key(&TagType::AiffText, std::str::from_utf8(&fourcc).unwrap())
.unwrap(),
ItemValue::Text(String::from_utf8(value)?),
);
let value = String::from_utf8(value)?;
text_chunks.insert_item(item);
match &fourcc {
b"NAME" => text_chunks.name = Some(value),
b"AUTH" => text_chunks.author = Some(value),
b"(c) " => text_chunks.copyright = Some(value),
_ => unreachable!(),
}
}
#[cfg(feature = "id3v2")]
b"ID3 " | b"id3 " => {
let mut value = vec![0; size as usize];
data.read_exact(&mut value)?;
@ -122,7 +69,7 @@ where
data.seek(SeekFrom::Current(10))?;
}
id3 = Some(id3v2)
id3v2_tag = Some(id3v2);
}
b"COMM" => {
if comm.is_none() {
@ -146,6 +93,11 @@ where
data.seek(SeekFrom::Current(i64::from(size)))?;
}
}
// Chunks only start on even boundaries
if size % 2 != 0 {
data.seek(SeekFrom::Current(1))?;
}
}
if comm.is_none() {
@ -156,11 +108,20 @@ where
return Err(LoftyError::Aiff("File does not contain a \"SSND\" chunk"));
}
let properties = read_properties(&mut &*comm.unwrap(), stream_len)?;
let properties = super::properties::read_properties(&mut &*comm.unwrap(), stream_len)?;
Ok(AiffFile {
properties,
text_chunks: (text_chunks.item_count() > 0).then(|| text_chunks),
id3v2: id3,
#[cfg(feature = "aiff_text_chunks")]
text_chunks: match text_chunks {
AiffTextChunks {
name: None,
author: None,
copyright: None,
} => None,
_ => Some(text_chunks),
},
#[cfg(feature = "id3v2")]
id3v2_tag,
})
}

View file

@ -1,54 +1,122 @@
use crate::error::Result;
use crate::types::item::{ItemKey, ItemValue};
use crate::types::item::{ItemKey, ItemValue, TagItem};
use crate::types::tag::{Tag, TagType};
use std::fs::File;
use std::io::{Read, Seek, SeekFrom, Write};
use byteorder::{BigEndian, LittleEndian, ReadBytesExt};
use byteorder::{BigEndian, ReadBytesExt};
pub(in crate::logic) fn write_aiff_text(data: &mut File, tag: &Tag) -> Result<()> {
let mut text_chunks = Vec::new();
#[cfg(feature = "aiff_text_chunks")]
#[derive(Default)]
/// AIFF text chunks
///
/// AIFF has a few chunks for storing basic metadata, all of
/// which can only appear once in a file.
pub struct AiffTextChunks {
/// The name of the piece
pub name: Option<String>,
/// The author of the piece
pub author: Option<String>,
/// A copyright notice consisting of the date followed
/// by the copyright owner
pub copyright: Option<String>,
}
let items = tag.items().iter().filter(|i| {
(i.key() == &ItemKey::TrackTitle
|| i.key() == &ItemKey::TrackArtist
|| i.key() == &ItemKey::CopyrightMessage)
&& std::mem::discriminant(i.value())
== std::mem::discriminant(&ItemValue::Text(String::new()))
});
impl AiffTextChunks {
pub fn write_to(&self, file: &mut File) -> Result<()> {
Into::<AiffTextChunksRef>::into(self).write_to(file)
}
}
for i in items {
// Already covered
let value = match i.value() {
ItemValue::Text(value) => value,
_ => unreachable!(),
impl From<AiffTextChunks> for Tag {
fn from(input: AiffTextChunks) -> Self {
let mut tag = Tag::new(TagType::AiffText);
let push_item = |field: Option<String>, item_key: ItemKey, tag: &mut Tag| {
if let Some(text) = field {
tag.insert_item_unchecked(TagItem::new(item_key, ItemValue::Text(text)))
}
};
let len = (value.len() as u32).to_be_bytes();
push_item(input.name, ItemKey::TrackTitle, &mut tag);
push_item(input.author, ItemKey::TrackArtist, &mut tag);
push_item(input.copyright, ItemKey::CopyrightMessage, &mut tag);
// Safe to unwrap since we retained the only possible values
text_chunks.extend(
i.key()
.map_key(&TagType::AiffText)
.unwrap()
.as_bytes()
.iter(),
);
text_chunks.extend(len.iter());
text_chunks.extend(value.as_bytes().iter());
tag
}
}
impl From<Tag> for AiffTextChunks {
fn from(input: Tag) -> Self {
Self {
name: input.get_string(&ItemKey::TrackTitle).map(str::to_owned),
author: input.get_string(&ItemKey::TrackArtist).map(str::to_owned),
copyright: input
.get_string(&ItemKey::CopyrightMessage)
.map(str::to_owned),
}
}
}
pub(crate) struct AiffTextChunksRef<'a> {
pub name: Option<&'a str>,
pub author: Option<&'a str>,
pub copyright: Option<&'a str>,
}
impl<'a> Into<AiffTextChunksRef<'a>> for &'a AiffTextChunks {
fn into(self) -> AiffTextChunksRef<'a> {
AiffTextChunksRef {
name: self.name.as_deref(),
author: self.author.as_deref(),
copyright: self.copyright.as_deref(),
}
}
}
impl<'a> Into<AiffTextChunksRef<'a>> for &'a Tag {
fn into(self) -> AiffTextChunksRef<'a> {
AiffTextChunksRef {
name: self.get_string(&ItemKey::TrackTitle),
author: self.get_string(&ItemKey::TrackArtist),
copyright: self.get_string(&ItemKey::CopyrightMessage),
}
}
}
impl<'a> AiffTextChunksRef<'a> {
pub(in crate::logic) fn write_to(&self, file: &mut File) -> Result<()> {
write_to(file, self)
}
}
pub(in crate::logic) fn write_to(data: &mut File, tag: &AiffTextChunksRef) -> Result<()> {
fn write_chunk(writer: &mut Vec<u8>, key: &str, value: Option<&str>) {
if let Some(val) = value {
let len = (val.len() as u32).to_be_bytes();
writer.extend(key.as_bytes().iter());
writer.extend(len.iter());
writer.extend(val.as_bytes().iter());
}
}
let mut chunks_remove = Vec::new();
super::read::verify_aiff(data)?;
while let (Ok(fourcc), Ok(size)) = (
data.read_u32::<LittleEndian>(),
data.read_u32::<BigEndian>(),
) {
let fourcc_b = &fourcc.to_le_bytes();
let mut text_chunks = Vec::new();
write_chunk(&mut text_chunks, "NAME", tag.name);
write_chunk(&mut text_chunks, "AUTH", tag.author);
write_chunk(&mut text_chunks, "(c) ", tag.copyright);
let mut chunks_remove = Vec::new();
let mut fourcc = [0; 4];
while let (Ok(()), Ok(size)) = (data.read_exact(&mut fourcc), data.read_u32::<BigEndian>()) {
let pos = (data.seek(SeekFrom::Current(0))? - 8) as usize;
if fourcc_b == b"NAME" || fourcc_b == b"AUTH" || fourcc_b == b"(c) " {
if &fourcc == b"NAME" || &fourcc == b"AUTH" || &fourcc == b"(c) " {
chunks_remove.push((pos, (pos + 8 + size as usize)))
}

View file

@ -1,17 +1,18 @@
use super::read::verify_aiff;
use crate::error::{LoftyError, Result};
use crate::logic::id3::v2::tag::Id3v2TagRef;
use crate::logic::iff::aiff::tag::AiffTextChunksRef;
use crate::types::tag::{Tag, TagType};
use std::fs::File;
pub(crate) fn write_to(data: &mut File, tag: &Tag) -> Result<()> {
verify_aiff(data)?;
use byteorder::BigEndian;
pub(crate) fn write_to(data: &mut File, tag: &Tag) -> Result<()> {
match tag.tag_type() {
TagType::AiffText => super::tag::write_aiff_text(data, tag),
TagType::Id3v2 => crate::logic::id3::v2::write::write_id3v2_to_chunk_file::<
byteorder::BigEndian,
>(data, tag),
#[cfg(feature = "aiff_text_chunks")]
TagType::AiffText => Into::<AiffTextChunksRef>::into(tag).write_to(data),
#[cfg(feature = "id3v2")]
TagType::Id3v2 => Into::<Id3v2TagRef>::into(tag).write_to_chunk_file::<BigEndian>(data),
_ => Err(LoftyError::UnsupportedTag),
}
}

View file

@ -1,78 +1,28 @@
pub(crate) mod properties;
mod read;
mod tag;
#[cfg(feature = "riff_info_list")]
pub(crate) mod tag;
pub(in crate::logic) mod write;
use crate::error::Result;
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::{Tag, TagType};
use crate::types::tag::TagType;
use properties::WavProperties;
use tag::RiffInfoList;
use std::io::{Read, Seek};
use std::time::Duration;
#[allow(missing_docs, non_camel_case_types)]
/// A WAV file's format
pub enum WavFormat {
PCM,
IEEE_FLOAT,
Other(u16),
}
/// A WAV file's audio properties
pub struct WavProperties {
format: WavFormat,
duration: Duration,
bitrate: u32,
sample_rate: u32,
channels: u8,
}
impl From<WavProperties> for FileProperties {
fn from(input: WavProperties) -> Self {
Self {
duration: input.duration,
bitrate: Some(input.bitrate),
sample_rate: Some(input.sample_rate),
channels: Some(input.channels),
}
}
}
impl WavProperties {
/// Duration
pub fn duration(&self) -> Duration {
self.duration
}
/// Bitrate (kbps)
pub fn bitrate(&self) -> u32 {
self.bitrate
}
/// Sample rate (Hz)
pub fn sample_rate(&self) -> u32 {
self.sample_rate
}
/// Channel count
pub fn channels(&self) -> u8 {
self.channels
}
/// WAV format
pub fn format(&self) -> &WavFormat {
&self.format
}
}
/// A WAV file
pub struct WavFile {
#[cfg(feature = "riff_info_list")]
/// A RIFF INFO LIST
pub(crate) riff_info: Option<Tag>,
pub(crate) riff_info: Option<RiffInfoList>,
#[cfg(feature = "id3v2")]
/// An ID3v2 tag
pub(crate) id3v2: Option<Tag>,
pub(crate) id3v2_tag: Option<Id3v2Tag>,
/// The file's audio properties
pub(crate) properties: WavProperties,
}
@ -82,10 +32,13 @@ impl From<WavFile> for TaggedFile {
Self {
ty: FileType::WAV,
properties: FileProperties::from(input.properties),
tags: vec![input.riff_info, input.id3v2]
.into_iter()
.flatten()
.collect(),
tags: vec![
input.riff_info.map(|ri| ri.into()),
input.id3v2_tag.map(|id3| id3.into()),
]
.into_iter()
.flatten()
.collect(),
}
}
}
@ -106,40 +59,18 @@ impl AudioFile for WavFile {
}
fn contains_tag(&self) -> bool {
self.id3v2.is_some() || self.riff_info.is_some()
self.id3v2_tag.is_some() || self.riff_info.is_some()
}
fn contains_tag_type(&self, tag_type: &TagType) -> bool {
match tag_type {
TagType::Id3v2 => self.id3v2.is_some(),
TagType::Id3v2 => self.id3v2_tag.is_some(),
TagType::RiffInfo => self.riff_info.is_some(),
_ => false,
}
}
}
impl WavFile {
#[cfg(feature = "id3v2")]
/// Returns a reference to the ID3v2 tag if it exists
pub fn id3v2_tag(&self) -> Option<&Tag> {
self.id3v2.as_ref()
}
#[cfg(feature = "id3v2")]
/// Returns a mutable reference to the ID3v2 tag if it exists
pub fn id3v2_tag_mut(&mut self) -> Option<&mut Tag> {
self.id3v2.as_mut()
}
#[cfg(feature = "riff_info_list")]
/// Returns a reference to the RIFF INFO tag if it exists
pub fn riff_info(&self) -> Option<&Tag> {
self.riff_info.as_ref()
}
#[cfg(feature = "riff_info_list")]
/// Returns a mutable reference to the RIFF INFO tag if it exists
pub fn riff_info_mut(&mut self) -> Option<&mut Tag> {
self.riff_info.as_mut()
}
tag_methods! {
WavFile => ID3v2, id3v2_tag, Id3v2Tag; RIFF_INFO, riff_info, RiffInfoList
}

View file

@ -0,0 +1,146 @@
use crate::error::{LoftyError, Result};
use crate::types::properties::FileProperties;
use std::time::Duration;
use byteorder::{LittleEndian, ReadBytesExt};
const PCM: u16 = 0x0001;
const IEEE_FLOAT: u16 = 0x0003;
const EXTENSIBLE: u16 = 0xfffe;
#[allow(missing_docs, non_camel_case_types)]
/// A WAV file's format
pub enum WavFormat {
PCM,
IEEE_FLOAT,
Other(u16),
}
/// A WAV file's audio properties
pub struct WavProperties {
format: WavFormat,
duration: Duration,
bitrate: u32,
sample_rate: u32,
channels: u8,
}
impl From<WavProperties> for FileProperties {
fn from(input: WavProperties) -> Self {
Self {
duration: input.duration,
bitrate: Some(input.bitrate),
sample_rate: Some(input.sample_rate),
channels: Some(input.channels),
}
}
}
impl WavProperties {
/// Duration
pub fn duration(&self) -> Duration {
self.duration
}
/// Bitrate (kbps)
pub fn bitrate(&self) -> u32 {
self.bitrate
}
/// Sample rate (Hz)
pub fn sample_rate(&self) -> u32 {
self.sample_rate
}
/// Channel count
pub fn channels(&self) -> u8 {
self.channels
}
/// WAV format
pub fn format(&self) -> &WavFormat {
&self.format
}
}
pub(super) fn read_properties(
fmt: &mut &[u8],
total_samples: u32,
stream_len: u32,
) -> Result<WavProperties> {
let mut format_tag = fmt.read_u16::<LittleEndian>()?;
let channels = fmt.read_u16::<LittleEndian>()? as u8;
if channels == 0 {
return Err(LoftyError::Wav("File contains 0 channels"));
}
let sample_rate = fmt.read_u32::<LittleEndian>()?;
let bytes_per_second = fmt.read_u32::<LittleEndian>()?;
// Skip 2 bytes
// Block align (2)
let _ = fmt.read_u16::<LittleEndian>()?;
let bits_per_sample = fmt.read_u16::<LittleEndian>()?;
if format_tag == EXTENSIBLE {
if fmt.len() < 40 {
return Err(LoftyError::Wav(
"Extensible format identified, invalid \"fmt \" chunk size found (< 40)",
));
}
// Skip 8 bytes
// cbSize (Size of extra format information) (2)
// Valid bits per sample (2)
// Channel mask (4)
let _ = fmt.read_u64::<LittleEndian>()?;
format_tag = fmt.read_u16::<LittleEndian>()?;
}
let non_pcm = format_tag != PCM && format_tag != IEEE_FLOAT;
if non_pcm && total_samples == 0 {
return Err(LoftyError::Wav(
"Non-PCM format identified, no \"fact\" chunk found",
));
}
let sample_frames = if non_pcm {
total_samples
} else if bits_per_sample > 0 {
stream_len / u32::from(u16::from(channels) * ((bits_per_sample + 7) / 8))
} else {
0
};
let (duration, bitrate) = if sample_rate > 0 && sample_frames > 0 {
let length = (u64::from(sample_frames) * 1000) / u64::from(sample_rate);
(
Duration::from_millis(length),
(u64::from(stream_len * 8) / length) as u32,
)
} else if bytes_per_second > 0 {
let length = (u64::from(stream_len) * 1000) / u64::from(bytes_per_second);
(Duration::from_millis(length), (bytes_per_second * 8) / 1000)
} else {
(Duration::ZERO, 0)
};
Ok(WavProperties {
format: match format_tag {
PCM => WavFormat::PCM,
IEEE_FLOAT => WavFormat::IEEE_FLOAT,
other => WavFormat::Other(other),
},
duration,
bitrate,
sample_rate,
channels,
})
}

View file

@ -1,17 +1,15 @@
use super::{WavFile, WavFormat, WavProperties};
#[cfg(feature = "riff_info_list")]
use super::tag::RiffInfoList;
use super::WavFile;
use crate::error::{LoftyError, Result};
#[cfg(feature = "id3v2")]
use crate::logic::id3::v2::read::parse_id3v2;
use crate::types::tag::{Tag, TagType};
use std::io::{Read, Seek, SeekFrom};
use std::time::Duration;
use crate::logic::id3::v2::tag::Id3v2Tag;
use byteorder::{LittleEndian, ReadBytesExt};
const PCM: u16 = 0x0001;
const IEEE_FLOAT: u16 = 0x0003;
const EXTENSIBLE: u16 = 0xfffe;
pub(in crate::logic::iff) fn verify_wav<T>(data: &mut T) -> Result<()>
where
T: Read + Seek,
@ -30,83 +28,6 @@ where
Ok(())
}
fn read_properties(fmt: &mut &[u8], total_samples: u32, stream_len: u32) -> Result<WavProperties> {
let mut format_tag = fmt.read_u16::<LittleEndian>()?;
let channels = fmt.read_u16::<LittleEndian>()? as u8;
if channels == 0 {
return Err(LoftyError::Wav("File contains 0 channels"));
}
let sample_rate = fmt.read_u32::<LittleEndian>()?;
let bytes_per_second = fmt.read_u32::<LittleEndian>()?;
// Skip 2 bytes
// Block align (2)
let _ = fmt.read_u16::<LittleEndian>()?;
let bits_per_sample = fmt.read_u16::<LittleEndian>()?;
if format_tag == EXTENSIBLE {
if fmt.len() < 40 {
return Err(LoftyError::Wav(
"Extensible format identified, invalid \"fmt \" chunk size found (< 40)",
));
}
// Skip 8 bytes
// cbSize (Size of extra format information) (2)
// Valid bits per sample (2)
// Channel mask (4)
let _ = fmt.read_u64::<LittleEndian>()?;
format_tag = fmt.read_u16::<LittleEndian>()?;
}
let non_pcm = format_tag != PCM && format_tag != IEEE_FLOAT;
if non_pcm && total_samples == 0 {
return Err(LoftyError::Wav(
"Non-PCM format identified, no \"fact\" chunk found",
));
}
let sample_frames = if non_pcm {
total_samples
} else if bits_per_sample > 0 {
stream_len / u32::from(u16::from(channels) * ((bits_per_sample + 7) / 8))
} else {
0
};
let (duration, bitrate) = if sample_rate > 0 && sample_frames > 0 {
let length = (u64::from(sample_frames) * 1000) / u64::from(sample_rate);
(
Duration::from_millis(length),
(u64::from(stream_len * 8) / length) as u32,
)
} else if bytes_per_second > 0 {
let length = (u64::from(stream_len) * 1000) / u64::from(bytes_per_second);
(Duration::from_millis(length), (bytes_per_second * 8) / 1000)
} else {
(Duration::ZERO, 0)
};
Ok(WavProperties {
format: match format_tag {
PCM => WavFormat::PCM,
IEEE_FLOAT => WavFormat::IEEE_FLOAT,
other => WavFormat::Other(other),
},
duration,
bitrate,
sample_rate,
channels,
})
}
pub(in crate::logic) fn read_from<R>(data: &mut R) -> Result<WavFile>
where
R: Read + Seek,
@ -117,8 +38,9 @@ where
let mut total_samples = 0_u32;
let mut fmt = Vec::new();
let mut riff_info = Tag::new(TagType::RiffInfo);
let mut id3: Option<Tag> = None;
#[cfg(feature = "riff_info_list")]
let mut riff_info = RiffInfoList::default();
let mut id3v2_tag: Option<Id3v2Tag> = None;
let mut fourcc = [0; 4];
@ -133,18 +55,16 @@ where
data.read_exact(&mut value)?;
fmt = value;
continue;
} else {
data.seek(SeekFrom::Current(i64::from(size)))?;
}
data.seek(SeekFrom::Current(i64::from(size)))?;
}
b"fact" => {
if total_samples == 0 {
total_samples = data.read_u32::<LittleEndian>()?;
continue;
} else {
data.seek(SeekFrom::Current(4))?;
}
data.seek(SeekFrom::Current(4))?;
}
b"data" => {
if stream_len == 0 {
@ -157,13 +77,18 @@ where
let mut list_type = [0; 4];
data.read_exact(&mut list_type)?;
#[cfg(feature = "riff_info_list")]
if &list_type == b"INFO" {
let end = data.seek(SeekFrom::Current(0))? + u64::from(size - 4);
super::tag::read::parse_riff_info(data, end, &mut riff_info)?;
} else {
}
#[cfg(not(feature = "riff_info_list"))]
{
data.seek(SeekFrom::Current(i64::from(size)))?;
}
}
#[cfg(feature = "id3v2")]
b"ID3 " | b"id3 " => {
let mut value = vec![0; size as usize];
data.read_exact(&mut value)?;
@ -175,12 +100,17 @@ where
data.seek(SeekFrom::Current(10))?;
}
id3 = Some(id3v2);
id3v2_tag = Some(id3v2);
}
_ => {
data.seek(SeekFrom::Current(i64::from(size)))?;
}
}
// Chunks only start on even boundaries
if size % 2 != 0 {
data.seek(SeekFrom::Current(1))?;
}
}
if fmt.len() < 16 {
@ -194,8 +124,10 @@ where
}
Ok(WavFile {
properties: read_properties(&mut &*fmt, total_samples, stream_len)?,
riff_info: (riff_info.item_count() > 0).then(|| riff_info),
id3v2: id3,
properties: super::properties::read_properties(&mut &*fmt, total_samples, stream_len)?,
#[cfg(feature = "riff_info_list")]
riff_info: (!riff_info.items.is_empty()).then(|| riff_info),
#[cfg(feature = "id3v2")]
id3v2_tag,
})
}

View file

@ -1,2 +1,125 @@
pub(in crate::logic::iff::wav) mod read;
pub(in crate::logic::iff::wav) mod write;
use crate::error::Result;
use crate::types::item::{ItemKey, ItemValue, TagItem};
use crate::types::tag::{Tag, TagType};
use std::fs::File;
#[derive(Default)]
/// A RIFF INFO LIST
pub struct RiffInfoList {
/// A collection of chunk-value pairs
pub(crate) items: Vec<(String, String)>,
}
impl RiffInfoList {
pub fn push(&mut self, key: String, value: String) {
if valid_key(key.as_str()) {
self.items.push((key, value))
}
}
pub fn remove(&mut self, key: &str) {
self.items
.iter()
.position(|(k, _)| k == key)
.map(|p| self.items.remove(p));
}
pub fn items(&self) -> &[(String, String)] {
self.items.as_slice()
}
}
impl RiffInfoList {
pub fn write_to(&self, file: &mut File) -> Result<()> {
Into::<RiffInfoListRef>::into(self).write_to(file)
}
}
impl From<RiffInfoList> for Tag {
fn from(input: RiffInfoList) -> Self {
let mut tag = Tag::new(TagType::RiffInfo);
for (k, v) in input.items {
let item_key = ItemKey::from_key(&TagType::RiffInfo, &k);
tag.insert_item_unchecked(TagItem::new(
item_key,
ItemValue::Text(v.trim_matches('\0').to_string()),
));
}
tag
}
}
impl From<Tag> for RiffInfoList {
fn from(input: Tag) -> Self {
let mut riff_info = RiffInfoList::default();
for item in input.items {
if let ItemValue::Text(val) | ItemValue::Locator(val) = item.item_value {
let item_key = match item.item_key {
ItemKey::Unknown(unknown) => {
if unknown.len() == 4 && unknown.is_ascii() {
unknown.to_string()
} else {
continue;
}
}
// Safe to unwrap since we already checked ItemKey::Unknown
k => k.map_key(&TagType::RiffInfo, false).unwrap().to_string(),
};
riff_info.items.push((item_key, val))
}
}
riff_info
}
}
pub(crate) struct RiffInfoListRef<'a> {
items: Box<dyn Iterator<Item = (&'a str, &'a String)> + 'a>,
}
impl<'a> Into<RiffInfoListRef<'a>> for &'a RiffInfoList {
fn into(self) -> RiffInfoListRef<'a> {
RiffInfoListRef {
items: Box::new(self.items.iter().map(|(k, v)| (k.as_str(), v))),
}
}
}
impl<'a> Into<RiffInfoListRef<'a>> for &'a Tag {
fn into(self) -> RiffInfoListRef<'a> {
RiffInfoListRef {
items: Box::new(self.items.iter().filter_map(|i| {
if let ItemValue::Text(val) | ItemValue::Locator(val) = &i.item_value {
let item_key = i.key().map_key(&TagType::RiffInfo, true).unwrap();
if item_key.len() == 4 && item_key.is_ascii() {
Some((item_key, val))
} else {
None
}
} else {
None
}
})),
}
}
}
impl<'a> RiffInfoListRef<'a> {
pub(crate) fn write_to(&mut self, file: &mut File) -> Result<()> {
write::write_riff_info(file, self)
}
}
fn valid_key(key: &str) -> bool {
key.len() == 4 && key.is_ascii()
}

View file

@ -1,6 +1,5 @@
use super::RiffInfoList;
use crate::error::{LoftyError, Result};
use crate::types::item::{ItemKey, ItemValue, TagItem};
use crate::types::tag::{Tag, TagType};
use std::io::{Read, Seek, SeekFrom};
@ -9,25 +8,22 @@ use byteorder::{LittleEndian, ReadBytesExt};
pub(in crate::logic::iff::wav) fn parse_riff_info<R>(
data: &mut R,
end: u64,
tag: &mut Tag,
tag: &mut RiffInfoList,
) -> Result<()>
where
R: Read + Seek,
{
while data.seek(SeekFrom::Current(0))? != end {
let mut key = [0; 4];
let mut key = vec![0; 4];
data.read_exact(&mut key)?;
let key_str = std::str::from_utf8(&key)
let key_str = String::from_utf8(key)
.map_err(|_| LoftyError::Wav("Non UTF-8 key found in RIFF INFO"))?;
if !key_str.is_ascii() {
return Err(LoftyError::Wav("Non ascii key found in RIFF INFO"));
return Err(LoftyError::Wav("Non-ascii key found in RIFF INFO"));
}
let item_key = ItemKey::from_key(&TagType::RiffInfo, key_str)
.unwrap_or_else(|| ItemKey::Unknown(key_str.to_string()));
let size = data.read_u32::<LittleEndian>()?;
let mut value = vec![0; size as usize];
@ -41,9 +37,9 @@ where
let value_str = std::str::from_utf8(&value)
.map_err(|_| LoftyError::Wav("Non UTF-8 value found in RIFF INFO"))?;
tag.insert_item_unchecked(TagItem::new(
item_key,
ItemValue::Text(value_str.trim_matches('\0').to_string()),
tag.items.push((
key_str.to_string(),
value_str.trim_matches('\0').to_string(),
));
}

View file

@ -1,22 +1,23 @@
use super::RiffInfoListRef;
use crate::error::{LoftyError, Result};
use crate::types::item::ItemValue;
use crate::types::tag::{Tag, TagType};
use std::fs::File;
use std::io::{Read, Seek, SeekFrom, Write};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
pub(in crate::logic::iff::wav) fn write_riff_info(data: &mut File, tag: &Tag) -> Result<()> {
pub(in crate::logic::iff::wav) fn write_riff_info(
data: &mut File,
tag: &mut RiffInfoListRef,
) -> Result<()> {
let mut riff_info_bytes = Vec::new();
create_riff_info(tag, &mut riff_info_bytes)?;
create_riff_info(&mut tag.items, &mut riff_info_bytes)?;
if find_info_list(data)? {
let info_list_size = data.read_u32::<LittleEndian>()? as usize;
data.seek(SeekFrom::Current(-8))?;
let (info_list, info_list_size) = find_info_list(data)?;
let info_list_start = data.seek(SeekFrom::Current(0))? as usize;
let info_list_end = info_list_start + 8 + info_list_size;
if info_list {
let info_list_start = data.seek(SeekFrom::Current(-12))? as usize;
let info_list_end = info_list_start + 8 + info_list_size as usize;
data.seek(SeekFrom::Start(0))?;
@ -45,15 +46,15 @@ pub(in crate::logic::iff::wav) fn write_riff_info(data: &mut File, tag: &Tag) ->
Ok(())
}
fn find_info_list<T>(data: &mut T) -> Result<bool>
fn find_info_list<T>(data: &mut T) -> Result<(bool, u32)>
where
T: Read + Seek,
{
let mut fourcc = [0; 4];
let mut found_info = false;
let mut info = (false, 0);
while let (Ok(()), Ok(size)) = (
while let (Ok(()), Ok(mut size)) = (
data.read_exact(&mut fourcc),
data.read_u32::<LittleEndian>(),
) {
@ -62,54 +63,52 @@ where
data.read_exact(&mut list_type)?;
if &list_type == b"INFO" {
data.seek(SeekFrom::Current(-8))?;
found_info = true;
info = (true, size);
break;
}
data.seek(SeekFrom::Current(-8))?;
}
if size % 2 != 0 {
size += 1;
}
data.seek(SeekFrom::Current(i64::from(size)))?;
}
Ok(found_info)
Ok(info)
}
fn create_riff_info(tag: &Tag, bytes: &mut Vec<u8>) -> Result<()> {
if tag.item_count() == 0 {
fn create_riff_info(
items: &mut dyn Iterator<Item = (&str, &String)>,
bytes: &mut Vec<u8>,
) -> Result<()> {
let mut items = items.peekable();
if items.peek().is_none() {
return Ok(());
}
bytes.extend(b"LIST".iter());
bytes.extend(b"INFO".iter());
for item in tag.items() {
if let Some(key) = item.key().map_key(&TagType::RiffInfo) {
if key.len() == 4 && key.is_ascii() {
if let ItemValue::Text(value) = item.value() {
if value.is_empty() {
continue;
}
let val_b = value.as_bytes();
// Account for null terminator
let len = val_b.len() + 1;
// Each value has to be null terminated and have an even length
let (size, terminator): (u32, &[u8]) = if len % 2 == 0 {
(len as u32, &[0])
} else {
((len + 1) as u32, &[0, 0])
};
bytes.extend(key.as_bytes().iter());
bytes.extend(size.to_le_bytes().iter());
bytes.extend(val_b.iter());
bytes.extend(terminator.iter());
}
}
for (k, v) in items {
if v.is_empty() {
continue;
}
let val_b = v.as_bytes();
// Account for null terminator
let len = val_b.len() + 1;
// Each value has to be null terminated and have an even length
let terminator: &[u8] = if len % 2 == 0 { &[0] } else { &[0, 0] };
bytes.extend(k.as_bytes().iter());
bytes.extend((len as u32).to_le_bytes().iter());
bytes.extend(val_b.iter());
bytes.extend(terminator.iter());
}
let packet_size = bytes.len() - 4;

View file

@ -1,17 +1,19 @@
use super::read::verify_wav;
use crate::error::{LoftyError, Result};
use crate::logic::id3::v2::tag::Id3v2TagRef;
use crate::logic::iff::wav::tag::RiffInfoListRef;
use crate::types::tag::{Tag, TagType};
use std::fs::File;
use byteorder::LittleEndian;
pub(crate) fn write_to(data: &mut File, tag: &Tag) -> Result<()> {
verify_wav(data)?;
match tag.tag_type() {
TagType::RiffInfo => super::tag::write::write_riff_info(data, tag),
TagType::Id3v2 => crate::logic::id3::v2::write::write_id3v2_to_chunk_file::<
byteorder::LittleEndian,
>(data, tag),
TagType::RiffInfo => Into::<RiffInfoListRef>::into(tag).write_to(data),
TagType::Id3v2 => Into::<Id3v2TagRef>::into(tag).write_to_chunk_file::<LittleEndian>(data),
_ => Err(LoftyError::UnsupportedTag),
}
}

View file

@ -1,16 +1,16 @@
pub(crate) mod ape;
pub(crate) mod id3;
pub(crate) mod iff;
pub(crate) mod mp3;
pub(crate) mod mp4;
pub(crate) mod ogg;
use ogg::constants::{OPUSTAGS, VORBIS_COMMENT_HEAD};
#[cfg(any(feature = "id3v1", feature = "id3v2"))]
pub(crate) mod id3;
use crate::error::Result;
use crate::logic::mp4::ilst::IlstRef;
use crate::logic::ogg::tag::VorbisCommentsRef;
use crate::types::file::FileType;
use crate::types::tag::Tag;
use ogg::constants::{OPUSTAGS, VORBIS_COMMENT_HEAD};
use std::fs::File;
@ -18,11 +18,35 @@ pub(crate) fn write_tag(tag: &Tag, file: &mut File, file_type: FileType) -> Resu
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, tag),
FileType::FLAC => {
ogg::flac::write::write_to(file, &mut Into::<VorbisCommentsRef>::into(tag))
}
FileType::MP3 => mp3::write::write_to(file, tag),
FileType::MP4 => mp4::ilst::write::write_to(file, tag),
FileType::MP4 => mp4::ilst::write::write_to(file, &mut Into::<IlstRef>::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),
}
}
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()
}
#[doc = "Sets the " $display_name]
pub fn [<set_ $name>](&mut self, tag: $ty) {
self.$name = Some(tag)
}
)*
}
}
}
}
pub(in crate::logic) use tag_methods;

View file

@ -3,8 +3,12 @@ pub(crate) mod header;
pub(crate) mod read;
pub(in crate::logic) mod write;
use crate::logic::ape::tag::ApeTag;
use crate::logic::id3::v1::tag::Id3v1Tag;
use crate::logic::id3::v2::tag::Id3v2Tag;
use crate::logic::tag_methods;
use crate::types::file::{AudioFile, FileType, TaggedFile};
use crate::{FileProperties, Result, Tag, TagType};
use crate::{FileProperties, Result, TagType};
use header::{ChannelMode, Layer, MpegVersion};
use std::io::{Read, Seek};
@ -73,13 +77,13 @@ impl Mp3Properties {
pub struct Mp3File {
#[cfg(feature = "id3v2")]
/// An ID3v2 tag
pub(crate) id3v2: Option<Tag>,
pub(crate) id3v2_tag: Option<Id3v2Tag>,
#[cfg(feature = "id3v1")]
/// An ID3v1 tag
pub(crate) id3v1: Option<Tag>,
pub(crate) id3v1_tag: Option<Id3v1Tag>,
#[cfg(feature = "ape")]
/// An APEv1/v2 tag
pub(crate) ape: Option<Tag>,
pub(crate) ape_tag: Option<ApeTag>,
/// The file's audio properties
pub(crate) properties: Mp3Properties,
}
@ -89,10 +93,14 @@ impl From<Mp3File> for TaggedFile {
Self {
ty: FileType::MP3,
properties: FileProperties::from(input.properties),
tags: vec![input.id3v1, input.id3v2, input.ape]
.into_iter()
.flatten()
.collect(),
tags: vec![
input.id3v2_tag.map(|id3v2| id3v2.into()),
input.id3v1_tag.map(|id3v1| id3v1.into()),
input.ape_tag.map(|at| at.into()),
]
.into_iter()
.flatten()
.collect(),
}
}
}
@ -112,53 +120,19 @@ impl AudioFile for Mp3File {
}
fn contains_tag(&self) -> bool {
self.id3v2.is_some() || self.id3v1.is_some() || self.ape.is_some()
self.id3v2_tag.is_some() || self.id3v1_tag.is_some() || self.ape_tag.is_some()
}
fn contains_tag_type(&self, tag_type: &TagType) -> bool {
match tag_type {
TagType::Ape => self.ape.is_some(),
TagType::Id3v2 => self.id3v2.is_some(),
TagType::Id3v1 => self.id3v1.is_some(),
TagType::Ape => self.ape_tag.is_some(),
TagType::Id3v2 => self.id3v2_tag.is_some(),
TagType::Id3v1 => self.id3v1_tag.is_some(),
_ => false,
}
}
}
impl Mp3File {
#[cfg(feature = "id3v2")]
/// Returns a reference to the ID3v2 tag if it exists
pub fn id3v2_tag(&self) -> Option<&Tag> {
self.id3v2.as_ref()
}
#[cfg(feature = "id3v2")]
/// Returns a mutable reference to the ID3v2 tag if it exists
pub fn id3v2_tag_mut(&mut self) -> Option<&mut Tag> {
self.id3v2.as_mut()
}
#[cfg(feature = "id3v1")]
/// Returns a reference to the ID3v1 tag if it exists
pub fn id3v1_tag(&self) -> Option<&Tag> {
self.id3v1.as_ref()
}
#[cfg(feature = "id3v1")]
/// Returns a mutable reference to the ID3v1 tag if it exists
pub fn id3v1_tag_mut(&mut self) -> Option<&mut Tag> {
self.id3v1.as_mut()
}
#[cfg(feature = "ape")]
/// Returns a reference to the APEv1/2 tag if it exists
pub fn ape_tag(&self) -> Option<&Tag> {
self.ape.as_ref()
}
#[cfg(feature = "ape")]
/// Returns a mutable reference to the APEv1/2 tag if it exists
pub fn ape_tag_mut(&mut self) -> Option<&mut Tag> {
self.ape.as_mut()
}
tag_methods! {
Mp3File => ID3v2, id3v2_tag, Id3v2Tag; ID3v1, id3v1_tag, Id3v1Tag; APE, ape_tag, ApeTag
}

View file

@ -2,12 +2,14 @@ use super::header::{verify_frame_sync, Header, XingHeader};
use super::{Mp3File, Mp3Properties};
use crate::error::{LoftyError, Result};
use crate::logic::id3::unsynch_u32;
use crate::logic::id3::v1::tag::Id3v1Tag;
use crate::logic::id3::v2::read::parse_id3v2;
use crate::types::tag::Tag;
use std::io::{Read, Seek, SeekFrom};
use std::time::Duration;
use crate::id3::v2::Id3v2Tag;
use crate::logic::ape::tag::ApeTag;
use byteorder::{BigEndian, ByteOrder, ReadBytesExt};
fn read_properties(
@ -62,9 +64,9 @@ pub(crate) fn read_from<R>(data: &mut R) -> Result<Mp3File>
where
R: Read + Seek,
{
let mut id3v2: Option<Tag> = None;
let mut id3v1: Option<Tag> = None;
let mut ape: Option<Tag> = None;
let mut id3v2_tag: Option<Id3v2Tag> = None;
let mut id3v1_tag: Option<Id3v1Tag> = None;
let mut ape_tag: Option<ApeTag> = None;
let mut first_mpeg_frame = (None, 0);
let mut last_mpeg_frame = (None, 0);
@ -100,14 +102,14 @@ where
let mut id3v2_read = vec![0; size];
data.read_exact(&mut id3v2_read)?;
let id3v2_tag = parse_id3v2(&mut &*id3v2_read)?;
let id3v2 = parse_id3v2(&mut &*id3v2_read)?;
// Skip over the footer
if id3v2_tag.flags().footer {
if id3v2.flags().footer {
data.seek(SeekFrom::Current(10))?;
}
id3v2 = Some(id3v2_tag);
id3v2_tag = Some(id3v2);
continue;
}
@ -117,7 +119,7 @@ where
let mut id3v1_read = [0; 128];
data.read_exact(&mut id3v1_read)?;
id3v1 = Some(crate::logic::id3::v1::read::parse_id3v1(id3v1_read));
id3v1_tag = Some(crate::logic::id3::v1::read::parse_id3v1(id3v1_read));
continue;
}
[b'A', b'P', b'E', b'T'] => {
@ -125,7 +127,7 @@ where
data.read_exact(&mut header_remaining)?;
if &header_remaining == b"AGEX" {
ape = Some(crate::logic::ape::tag::read::read_ape_tag(data, false)?.0);
ape_tag = Some(crate::logic::ape::tag::read::read_ape_tag(data, false)?.0);
continue;
}
}
@ -150,9 +152,12 @@ where
let xing_header = XingHeader::read(&mut &xing_reader[..]).ok();
Ok(Mp3File {
id3v2,
id3v1,
ape,
#[cfg(feature = "id3v2")]
id3v2_tag,
#[cfg(feature = "id3v1")]
id3v1_tag,
#[cfg(feature = "ape")]
ape_tag,
properties: read_properties(first_mpeg_frame, last_mpeg_frame, xing_header),
})
}

View file

@ -1,13 +1,16 @@
use crate::error::{LoftyError, Result};
use crate::logic::ape::tag::ApeTagRef;
use crate::logic::id3::v1::tag::Id3v1TagRef;
use crate::logic::id3::v2::tag::Id3v2TagRef;
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() {
TagType::Ape => crate::logic::ape::tag::write::write_to(data, tag),
TagType::Id3v1 => crate::logic::id3::v1::write::write_id3v1(data, tag),
TagType::Id3v2 => crate::logic::id3::v2::write::write_id3v2(data, tag),
TagType::Ape => Into::<ApeTagRef>::into(tag).write_to(data),
TagType::Id3v1 => Into::<Id3v1TagRef>::into(tag).write_to(data),
TagType::Id3v2 => Into::<Id3v2TagRef>::into(tag).write_to(data),
_ => Err(LoftyError::UnsupportedTag),
}
}

View file

@ -1,61 +0,0 @@
use crate::error::{LoftyError, Result};
use std::io::{Read, Seek, SeekFrom};
use byteorder::{BigEndian, ReadBytesExt};
pub(crate) struct Atom {
pub(crate) start: u64,
pub(crate) len: u64,
pub(crate) extended: bool,
pub(crate) ident: String,
}
impl Atom {
pub(crate) fn read<R>(data: &mut R) -> Result<Self>
where
R: Read + Seek,
{
let start = data.seek(SeekFrom::Current(0))?;
let len = data.read_u32::<BigEndian>()?;
let mut ident = [0; 4];
data.read_exact(&mut ident)?;
let (len, extended) = match len {
// The atom extends to the end of the file
0 => {
let pos = data.seek(SeekFrom::Current(0))?;
let end = data.seek(SeekFrom::End(0))?;
data.seek(SeekFrom::Start(pos))?;
(end - pos, false)
}
// There's an extended length
1 => (data.read_u64::<BigEndian>()?, true),
_ if len < 8 => return Err(LoftyError::BadAtom("Found an invalid length (< 8)")),
_ => (u64::from(len), false),
};
Ok(Self {
start,
len,
extended,
ident: if ident[0] == 0xA9 {
let end = simdutf8::basic::from_utf8(&ident[1..])
.map_err(|_| LoftyError::BadAtom("Encountered a non UTF-8 atom identifier"))?;
let mut ident = String::from('\u{a9}');
ident.push_str(end);
ident
} else {
simdutf8::basic::from_utf8(&ident)
.map_err(|_| LoftyError::BadAtom("Encountered a non UTF-8 atom identifier"))?
.to_string()
},
})
}
}

View file

@ -0,0 +1,93 @@
use crate::error::{LoftyError, Result};
use std::io::{Read, Seek, SeekFrom};
use crate::mp4::AtomIdent;
use byteorder::{BigEndian, ReadBytesExt};
pub(crate) struct AtomInfo {
pub(crate) start: u64,
pub(crate) len: u64,
pub(crate) extended: bool,
pub(crate) ident: AtomIdent,
}
impl AtomInfo {
pub(crate) fn read<R>(data: &mut R) -> Result<Self>
where
R: Read + Seek,
{
let start = data.seek(SeekFrom::Current(0))?;
let len = data.read_u32::<BigEndian>()?;
let mut ident = [0; 4];
data.read_exact(&mut ident)?;
let mut atom_ident = AtomIdent::Fourcc(ident);
// Encountered a freeform identifier
if &ident == b"----" {
atom_ident = parse_freeform(data)?;
}
let (len, extended) = match len {
// The atom extends to the end of the file
0 => {
let pos = data.seek(SeekFrom::Current(0))?;
let end = data.seek(SeekFrom::End(0))?;
data.seek(SeekFrom::Start(pos))?;
(end - pos, false)
}
// There's an extended length
1 => (data.read_u64::<BigEndian>()?, true),
_ if len < 8 => return Err(LoftyError::BadAtom("Found an invalid length (< 8)")),
_ => (u64::from(len), false),
};
Ok(Self {
start,
len,
extended,
ident: atom_ident,
})
}
}
fn parse_freeform<R>(data: &mut R) -> Result<AtomIdent>
where
R: Read + Seek,
{
let mean = freeform_chunk(data, b"mean")?;
let name = freeform_chunk(data, b"name")?;
Ok(AtomIdent::Freeform { mean, name })
}
fn freeform_chunk<R>(data: &mut R, name: &[u8]) -> Result<String>
where
R: Read + Seek,
{
let atom = AtomInfo::read(data)?;
match atom.ident {
AtomIdent::Fourcc(ref fourcc) if fourcc == name => {
// Version (1)
// Flags (3)
data.seek(SeekFrom::Current(4))?;
// Already read the size, identifier, and version/flags (12 bytes)
let mut content = vec![0; (atom.len - 12) as usize];
data.read_exact(&mut content)?;
String::from_utf8(content).map_err(|_| {
LoftyError::BadAtom("Found a non UTF-8 string while reading freeform identifier")
})
}
_ => Err(LoftyError::BadAtom(
"Found freeform identifier \"----\" with no trailing \"mean\" or \"name\" atoms",
)),
}
}

View file

@ -1,2 +1,246 @@
pub(in crate::logic::mp4) mod read;
pub(in crate::logic) mod write;
use crate::picture::Picture;
use crate::types::item::{ItemKey, ItemValue, TagItem};
use crate::types::tag::{Tag, TagType};
use std::convert::TryInto;
#[derive(Default)]
/// An Mp4
pub struct Ilst {
pub(crate) atoms: Vec<Atom>,
}
impl From<Ilst> for Tag {
fn from(input: Ilst) -> Self {
let mut tag = Self::new(TagType::Mp4Atom);
for atom in input.atoms {
let value = match atom.data {
AtomData::UTF8(text) | AtomData::UTF16(text) => ItemValue::Text(text),
AtomData::Picture(pic) => {
tag.pictures.push(pic);
continue;
}
_ => continue,
};
let key = ItemKey::from_key(
&TagType::Mp4Atom,
&match atom.ident {
AtomIdent::Fourcc(fourcc) => {
fourcc.iter().map(|b| *b as char).collect::<String>()
}
AtomIdent::Freeform { mean, name } => {
format!("----:{}:{}", mean, name)
}
},
);
tag.items.push(TagItem::new(key, value));
}
tag
}
}
impl From<Tag> for Ilst {
fn from(input: Tag) -> Self {
let mut ilst = Self::default();
for item in input.items {
if let Some(ident) = item_key_to_ident(item.key()).map(|k| k.into()) {
let data = match item.item_value {
ItemValue::Text(text) => AtomData::UTF8(text),
_ => continue,
};
ilst.atoms.push(Atom { ident, data })
}
}
ilst
}
}
pub struct Atom {
ident: AtomIdent,
data: AtomData,
}
#[derive(Eq, PartialEq)]
pub enum AtomIdent {
/// A four byte identifier
///
/// Many FOURCCs start with `0xA9` (©), and should be a UTF-8 string.
Fourcc([u8; 4]),
/// A freeform identifier
///
/// # Example
///
/// ```text
/// ----:com.apple.iTunes:SUBTITLE
/// ─┬── ────────┬─────── ───┬────
/// ╰freeform identifier ╰name
/// |
/// ╰mean
/// ```
///
/// * `mean`: A string using a reverse DNS naming convention
/// * `name`: A string identifying the atom
Freeform { mean: String, name: String },
}
/// The data of an atom
///
/// NOTES:
///
/// * This only covers the most common data types.
/// See the list of [well-known data types](https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW34)
/// for codes.
/// * There are only two variants for integers, which
/// will come from codes `21` and `22`. All other integer
/// types will be stored as [`AtomData::Unknown`], refer
/// to the link above for codes.
pub enum AtomData {
UTF8(String),
UTF16(String),
/// A JPEG, PNG, GIF *(Deprecated)*, or BMP image
///
/// The type is read from the picture itself
Picture(Picture),
/// A big endian signed integer (1-4 bytes)
SignedInteger(i32),
/// A big endian unsigned integer (1-4 bytes)
UnsignedInteger(u32),
/// Unknown data
///
/// Due to the number of possible types, there are many
/// **specified** types that are going to fall into this
/// variant.
Unknown {
code: u32,
data: Vec<u8>,
},
}
pub(crate) struct IlstRef<'a> {
atoms: Box<dyn Iterator<Item = AtomRef<'a>> + 'a>,
}
pub(crate) struct AtomRef<'a> {
ident: AtomIdentRef<'a>,
data: AtomDataRef<'a>,
}
impl<'a> Into<AtomRef<'a>> for &'a Atom {
fn into(self) -> AtomRef<'a> {
AtomRef {
ident: (&self.ident).into(),
data: (&self.data).into(),
}
}
}
pub(crate) enum AtomIdentRef<'a> {
Fourcc([u8; 4]),
Freeform { mean: &'a str, name: &'a str },
}
impl<'a> Into<AtomIdentRef<'a>> for &'a AtomIdent {
fn into(self) -> AtomIdentRef<'a> {
match self {
AtomIdent::Fourcc(fourcc) => AtomIdentRef::Fourcc(*fourcc),
AtomIdent::Freeform { mean, name } => AtomIdentRef::Freeform { mean, name },
}
}
}
impl<'a> From<AtomIdentRef<'a>> for AtomIdent {
fn from(input: AtomIdentRef<'a>) -> Self {
match input {
AtomIdentRef::Fourcc(fourcc) => AtomIdent::Fourcc(fourcc),
AtomIdentRef::Freeform { mean, name } => AtomIdent::Freeform {
mean: mean.to_string(),
name: name.to_string(),
},
}
}
}
pub(crate) enum AtomDataRef<'a> {
UTF8(&'a str),
UTF16(&'a str),
Picture(&'a Picture),
SignedInteger(i32),
UnsignedInteger(u32),
Unknown { code: u32, data: &'a [u8] },
}
impl<'a> Into<AtomDataRef<'a>> for &'a AtomData {
fn into(self) -> AtomDataRef<'a> {
match self {
AtomData::UTF8(utf8) => AtomDataRef::UTF8(utf8),
AtomData::UTF16(utf16) => AtomDataRef::UTF16(utf16),
AtomData::Picture(pic) => AtomDataRef::Picture(pic),
AtomData::SignedInteger(int) => AtomDataRef::SignedInteger(*int),
AtomData::UnsignedInteger(uint) => AtomDataRef::UnsignedInteger(*uint),
AtomData::Unknown { code, data } => AtomDataRef::Unknown { code: *code, data },
}
}
}
impl<'a> Into<IlstRef<'a>> for &'a Ilst {
fn into(self) -> IlstRef<'a> {
IlstRef {
atoms: Box::new(self.atoms.iter().map(|a| a.into())),
}
}
}
impl<'a> Into<IlstRef<'a>> for &'a Tag {
fn into(self) -> IlstRef<'a> {
let iter = self.items.iter().filter_map(|i| {
if let (Some(ident), ItemValue::Text(text)) = (item_key_to_ident(i.key()), i.value()) {
Some(AtomRef {
ident,
data: AtomDataRef::UTF8(text),
})
} else {
None
}
});
IlstRef {
atoms: Box::new(iter),
}
}
}
fn item_key_to_ident(key: &ItemKey) -> Option<AtomIdentRef> {
key.map_key(&TagType::Mp4Atom, true).and_then(|ident| {
if ident.starts_with("----") {
let mut split = ident.split(':');
split.next();
let mean = split.next();
let name = split.next();
if let (Some(mean), Some(name)) = (mean, name) {
Some(AtomIdentRef::Freeform { mean, name })
} else {
None
}
} else {
let fourcc = ident.chars().map(|c| c as u8).collect::<Vec<_>>();
if let Ok(fourcc) = TryInto::<[u8; 4]>::try_into(fourcc) {
Some(AtomIdentRef::Fourcc(fourcc))
} else {
None
}
}
})
}

View file

@ -1,18 +1,16 @@
use super::{Atom, AtomData, AtomIdent, Ilst};
use crate::error::{LoftyError, Result};
use crate::logic::id3::v2::util::text_utils::utf16_decode;
use crate::logic::id3::v2::util::text_utils::TextEncoding;
use crate::logic::mp4::atom::Atom;
use crate::logic::id3::v2::util::text_utils::{utf16_decode, TextEncoding};
use crate::logic::mp4::atom_info::AtomInfo;
use crate::logic::mp4::read::skip_unneeded;
use crate::types::item::{ItemKey, ItemValue, TagItem};
use crate::types::picture::{MimeType, Picture, PictureInformation, PictureType};
use crate::types::tag::{Tag, TagType};
use std::borrow::Cow;
use std::io::{Cursor, Read, Seek, SeekFrom};
use byteorder::{BigEndian, ReadBytesExt};
use byteorder::ReadBytesExt;
pub(crate) fn parse_ilst<R>(data: &mut R, len: u64) -> Result<Option<Tag>>
pub(crate) fn parse_ilst<R>(data: &mut R, len: u64) -> Result<Option<Ilst>>
where
R: Read + Seek,
{
@ -21,96 +19,77 @@ where
let mut cursor = Cursor::new(contents);
let mut tag = Tag::new(TagType::Mp4Atom);
let mut tag = Ilst::default();
while let Ok(atom) = Atom::read(&mut cursor) {
// Safe to unwrap here since ItemKey::Unknown exists
let key = match &*atom.ident {
"free" | "skip" => {
skip_unneeded(&mut cursor, atom.extended, atom.len)?;
continue;
}
"covr" => {
let (mime_type, picture) = match parse_data(&mut cursor)? {
(ItemValue::Binary(picture), 13) => (MimeType::Jpeg, picture),
(ItemValue::Binary(picture), 14) => (MimeType::Png, picture),
(ItemValue::Binary(picture), 27) => (MimeType::Bmp, picture),
// GIF is deprecated
(ItemValue::Binary(picture), 12) => (MimeType::Gif, picture),
// Type 0 is implicit
(ItemValue::Binary(picture), 0) => (MimeType::None, picture),
_ => return Err(LoftyError::BadAtom("\"covr\" atom has an unknown type")),
};
tag.push_picture(Picture {
pic_type: PictureType::Other,
text_encoding: TextEncoding::UTF8,
mime_type,
description: None,
information: PictureInformation {
width: 0,
height: 0,
color_depth: 0,
num_colors: 0,
},
data: Cow::from(picture),
});
continue;
}
"----" => ItemKey::from_key(&TagType::Mp4Atom, &*parse_freeform(&mut cursor)?),
other => ItemKey::from_key(&TagType::Mp4Atom, other),
}
.unwrap();
let data = parse_data(&mut cursor)?.0;
match key {
ItemKey::TrackNumber | ItemKey::DiscNumber => {
if let ItemValue::Binary(pair) = data {
let pair = &mut &pair[2..6];
let number = u32::from(pair.read_u16::<BigEndian>()?);
let total = u32::from(pair.read_u16::<BigEndian>()?);
if total == 0 {
match key {
ItemKey::TrackNumber => tag.insert_item_unchecked(TagItem::new(
ItemKey::TrackTotal,
ItemValue::UInt(total),
)),
ItemKey::DiscNumber => tag.insert_item_unchecked(TagItem::new(
ItemKey::DiscTotal,
ItemValue::UInt(total),
)),
_ => unreachable!(),
}
}
if number == 0 {
tag.insert_item_unchecked(TagItem::new(key, ItemValue::UInt(number)))
}
} else {
return Err(LoftyError::BadAtom(
"Expected atom data to include integer pair",
));
while let Ok(atom) = AtomInfo::read(&mut cursor) {
let ident = match &atom.ident {
AtomIdent::Fourcc(ref fourcc) => match fourcc {
b"free" | b"skip" => {
skip_unneeded(&mut cursor, atom.extended, atom.len)?;
continue;
}
}
_ => tag.insert_item_unchecked(TagItem::new(key, data)),
}
b"covr" => {
let value = parse_data(&mut cursor)?;
let (mime_type, data) = match value {
AtomData::Unknown { code, data } => match code {
// Type 0 is implicit
0 => (MimeType::None, data),
// GIF is deprecated
12 => (MimeType::Gif, data),
13 => (MimeType::Jpeg, data),
14 => (MimeType::Png, data),
27 => (MimeType::Bmp, data),
_ => {
return Err(LoftyError::BadAtom(
"\"covr\" atom has an unknown type",
))
}
},
_ => return Err(LoftyError::BadAtom("\"covr\" atom has an unknown type")),
};
tag.atoms.push(Atom {
ident: atom.ident,
data: AtomData::Picture(Picture {
pic_type: PictureType::Other,
text_encoding: TextEncoding::UTF8,
mime_type,
description: None,
information: PictureInformation {
width: 0,
height: 0,
color_depth: 0,
num_colors: 0,
},
data: Cow::from(data),
}),
});
continue;
}
_ => atom.ident,
},
_ => atom.ident,
};
let data = parse_data(&mut cursor)?;
tag.atoms.push(Atom { ident, data })
}
Ok(Some(tag))
}
fn parse_data<R>(data: &mut R) -> Result<(ItemValue, u32)>
fn parse_data<R>(data: &mut R) -> Result<AtomData>
where
R: Read + Seek,
{
let atom = Atom::read(data)?;
let atom = AtomInfo::read(data)?;
if atom.ident != "data" {
return Err(LoftyError::BadAtom("Expected atom \"data\" to follow name"));
match atom.ident {
AtomIdent::Fourcc(ref name) if name == b"data" => {}
_ => return Err(LoftyError::BadAtom("Expected atom \"data\" to follow name")),
}
// We don't care about the version
@ -129,26 +108,25 @@ where
// https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW35
let value = match flags {
1 => ItemValue::Text(String::from_utf8(content)?),
2 => ItemValue::Text(utf16_decode(&*content, u16::from_be_bytes)?),
15 => ItemValue::Locator(String::from_utf8(content)?),
22 | 76 | 77 | 78 => parse_uint(&*content)?,
21 | 66 | 67 | 74 => parse_int(&*content)?,
_ => ItemValue::Binary(content),
1 => AtomData::UTF8(String::from_utf8(content)?),
2 => AtomData::UTF16(utf16_decode(&*content, u16::from_be_bytes)?),
21 => AtomData::SignedInteger(parse_int(&content)?),
22 => AtomData::UnsignedInteger(parse_uint(&content)?),
code => AtomData::Unknown {
code,
data: content,
},
};
Ok((value, flags))
Ok(value)
}
fn parse_uint(bytes: &[u8]) -> Result<ItemValue> {
fn parse_uint(bytes: &[u8]) -> Result<u32> {
Ok(match bytes.len() {
1 => ItemValue::UInt(u32::from(bytes[0])),
2 => ItemValue::UInt(u32::from(u16::from_be_bytes([bytes[0], bytes[1]]))),
3 => ItemValue::UInt(u32::from_be_bytes([0, bytes[0], bytes[1], bytes[2]])),
4 => ItemValue::UInt(u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])),
8 => ItemValue::UInt64(u64::from_be_bytes([
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
])),
1 => u32::from(bytes[0]),
2 => u32::from(u16::from_be_bytes([bytes[0], bytes[1]])),
3 => u32::from_be_bytes([0, bytes[0], bytes[1], bytes[2]]),
4 => u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
_ => {
return Err(LoftyError::BadAtom(
"Unexpected atom size for type \"BE unsigned integer\"",
@ -157,15 +135,12 @@ fn parse_uint(bytes: &[u8]) -> Result<ItemValue> {
})
}
fn parse_int(bytes: &[u8]) -> Result<ItemValue> {
fn parse_int(bytes: &[u8]) -> Result<i32> {
Ok(match bytes.len() {
1 => ItemValue::Int(i32::from(bytes[0])),
2 => ItemValue::Int(i32::from(i16::from_be_bytes([bytes[0], bytes[1]]))),
3 => ItemValue::Int(i32::from_be_bytes([0, bytes[0], bytes[1], bytes[2]]) as i32),
4 => ItemValue::Int(i32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as i32),
8 => ItemValue::Int64(i64::from_be_bytes([
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
])),
1 => i32::from(bytes[0]),
2 => i32::from(i16::from_be_bytes([bytes[0], bytes[1]])),
3 => i32::from_be_bytes([0, bytes[0], bytes[1], bytes[2]]) as i32,
4 => i32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as i32,
_ => {
return Err(LoftyError::BadAtom(
"Unexpected atom size for type \"BE signed integer\"",
@ -173,45 +148,3 @@ fn parse_int(bytes: &[u8]) -> Result<ItemValue> {
}
})
}
fn parse_freeform<R>(data: &mut R) -> Result<String>
where
R: Read + Seek,
{
// ----:????:????
let mut freeform = String::new();
freeform.push_str("----:");
freeform_chunk(data, "mean", &mut freeform)?;
freeform.push(':');
freeform_chunk(data, "name", &mut freeform)?;
Ok(freeform)
}
fn freeform_chunk<R>(data: &mut R, name: &str, freeform: &mut String) -> Result<()>
where
R: Read + Seek,
{
let atom = Atom::read(data)?;
if atom.ident != name {
return Err(LoftyError::BadAtom(
"Found freeform identifier \"----\" with no trailing \"mean\" or \"name\" atoms",
));
}
// Version (1)
// Flags (3)
data.seek(SeekFrom::Current(4))?;
// Already read the size, identifier, and version/flags (12 bytes)
let mut content = vec![0; (atom.len - 12) as usize];
data.read_exact(&mut content)?;
freeform.push_str(std::str::from_utf8(&*content).map_err(|_| {
LoftyError::BadAtom("Found a non UTF-8 string while reading freeform identifier")
})?);
Ok(())
}

View file

@ -1,23 +1,18 @@
use super::{AtomDataRef, IlstRef};
use crate::error::{LoftyError, Result};
use crate::logic::mp4::ilst::{AtomIdentRef, AtomRef};
use crate::logic::mp4::moov::Moov;
use crate::logic::mp4::read::nested_atom;
use crate::logic::mp4::read::verify_mp4;
use crate::picture::MimeType;
use crate::types::item::ItemValue;
use crate::types::picture::Picture;
use crate::types::tag::{Tag, TagType};
use std::convert::TryInto;
use std::fs::File;
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
use byteorder::{BigEndian, WriteBytesExt};
pub(in crate::logic) fn write_to(data: &mut File, tag: &Tag) -> Result<()> {
if tag.tag_type() != &TagType::Mp4Atom {
return Err(LoftyError::UnsupportedTag);
}
pub(in crate::logic) fn write_to(data: &mut File, tag: &mut IlstRef) -> Result<()> {
verify_mp4(data)?;
let moov = Moov::find(data)?;
@ -31,10 +26,10 @@ pub(in crate::logic) fn write_to(data: &mut File, tag: &Tag) -> Result<()> {
let mut cursor = Cursor::new(file_bytes);
cursor.seek(SeekFrom::Start(pos))?;
let ilst = build_ilst(tag)?;
let ilst = build_ilst(&mut tag.atoms)?;
let remove_tag = ilst.is_empty();
let udta = nested_atom(&mut cursor, moov.len, "udta")?;
let udta = nested_atom(&mut cursor, moov.len, b"udta")?;
// Nothing to do
if remove_tag && udta.is_none() {
@ -48,11 +43,11 @@ pub(in crate::logic) fn write_to(data: &mut File, tag: &Tag) -> Result<()> {
// ilst is nested in udta.meta, so we need to check what atoms actually exist
if let Some(udta) = udta {
if let Some(meta) = nested_atom(&mut cursor, udta.len, "meta")? {
if let Some(meta) = nested_atom(&mut cursor, udta.len, b"meta")? {
// Skip version and flags
cursor.seek(SeekFrom::Current(4))?;
let (replacement, range, existing_ilst_size) =
if let Some(ilst_existing) = nested_atom(&mut cursor, meta.len - 4, "ilst")? {
if let Some(ilst_existing) = nested_atom(&mut cursor, meta.len - 4, b"ilst")? {
let ilst_existing_size = ilst_existing.len;
let replacement = if remove_tag { Vec::new() } else { ilst };
@ -184,55 +179,28 @@ fn write_size(start: u64, size: u64, extended: bool, writer: &mut Cursor<Vec<u8>
Ok(())
}
fn build_ilst(tag: &Tag) -> Result<Vec<u8>> {
if tag.item_count() == 0 && tag.picture_count() == 0 {
return Ok(Vec::new());
}
fn build_ilst(atoms: &mut dyn Iterator<Item = AtomRef>) -> Result<Vec<u8>> {
let mut peek = atoms.peekable();
let items = tag
.items()
.iter()
.filter_map(|i| {
let key = i.key().map_key(&TagType::Mp4Atom).unwrap();
let valid_value = std::mem::discriminant(&ItemValue::Binary(Vec::new()))
!= std::mem::discriminant(i.value());
((key.chars().count() == 4 || key.starts_with("----")) && valid_value)
.then(|| (key, i.value()))
})
.collect::<Vec<(&str, &ItemValue)>>();
if items.is_empty() {
if peek.peek().is_none() {
return Ok(Vec::new());
}
let mut writer = Cursor::new(vec![0, 0, 0, 0, b'i', b'l', b's', b't']);
writer.seek(SeekFrom::End(0))?;
for (key, value) in items {
for atom in peek {
let start = writer.seek(SeekFrom::Current(0))?;
// Empty size, we get it later
writer.write_all(&[0; 4])?;
if key.starts_with("----") {
write_freeform(key, &mut writer)?;
} else {
// "©" is 2 bytes, we only want to write the second one
writer.write_all(&if key.starts_with('©') {
let key_bytes = key.as_bytes();
[key_bytes[1], key_bytes[2], key_bytes[3], key_bytes[4]]
} else if key.len() > 4 {
return Err(LoftyError::BadAtom(
"Attempted to write an atom identifier bigger than 4 bytes",
));
} else {
key.as_bytes().try_into().unwrap()
})?;
match atom.ident {
AtomIdentRef::Fourcc(ref fourcc) => writer.write_all(fourcc)?,
AtomIdentRef::Freeform { mean, name } => write_freeform(mean, name, &mut writer)?,
}
write_item(value, &mut writer)?;
write_atom_data(&atom.data, &mut writer)?;
let end = writer.seek(SeekFrom::Current(0))?;
@ -245,10 +213,6 @@ fn build_ilst(tag: &Tag) -> Result<Vec<u8>> {
writer.seek(SeekFrom::Start(end))?;
}
for pic in tag.pictures() {
write_picture(pic, &mut writer)?;
}
let size = writer.get_ref().len();
write_size(
@ -261,29 +225,18 @@ fn build_ilst(tag: &Tag) -> Result<Vec<u8>> {
Ok(writer.into_inner())
}
fn write_freeform(freeform: &str, writer: &mut Cursor<Vec<u8>>) -> Result<()> {
fn write_freeform(mean: &str, name: &str, writer: &mut Cursor<Vec<u8>>) -> Result<()> {
// ---- : ???? : ????
let freeform_split = freeform.splitn(3, ':').collect::<Vec<&str>>();
if freeform_split.len() != 3 {
return Err(LoftyError::BadAtom(
"Attempted to write an incomplete freeform identifier",
));
}
// ----
writer.write_all(freeform_split[0].as_bytes())?;
writer.write_all(b"----")?;
// .... MEAN 0000 ????
let mean = freeform_split[1];
writer.write_u32::<BigEndian>((12 + mean.len()) as u32)?;
writer.write_all(&[b'm', b'e', b'a', b'n', 0, 0, 0, 0])?;
writer.write_all(mean.as_bytes())?;
// .... NAME 0000 ????
let name = freeform_split[2];
writer.write_u32::<BigEndian>((12 + name.len()) as u32)?;
writer.write_all(&[b'n', b'a', b'm', b'e', 0, 0, 0, 0])?;
writer.write_all(name.as_bytes())?;
@ -291,15 +244,14 @@ fn write_freeform(freeform: &str, writer: &mut Cursor<Vec<u8>>) -> Result<()> {
Ok(())
}
fn write_item(value: &ItemValue, writer: &mut Cursor<Vec<u8>>) -> Result<()> {
fn write_atom_data(value: &AtomDataRef, writer: &mut Cursor<Vec<u8>>) -> Result<()> {
match value {
ItemValue::Text(text) => write_data(1, text.as_bytes(), writer),
ItemValue::Locator(locator) => write_data(2, locator.as_bytes(), writer),
ItemValue::UInt(uint) => write_data(22, uint.to_be_bytes().as_ref(), writer),
ItemValue::UInt64(uint64) => write_data(78, uint64.to_be_bytes().as_ref(), writer),
ItemValue::Int(int) => write_data(21, int.to_be_bytes().as_ref(), writer),
ItemValue::Int64(int64) => write_data(74, int64.to_be_bytes().as_ref(), writer),
_ => unreachable!(),
AtomDataRef::UTF8(text) => write_data(1, text.as_bytes(), writer),
AtomDataRef::UTF16(text) => write_data(2, text.as_bytes(), writer),
AtomDataRef::Picture(pic) => write_picture(pic, writer),
AtomDataRef::SignedInteger(int) => write_data(21, int.to_be_bytes().as_ref(), writer),
AtomDataRef::UnsignedInteger(uint) => write_data(22, uint.to_be_bytes().as_ref(), writer),
AtomDataRef::Unknown { code, data } => write_data(*code, data, writer),
}
}
@ -318,8 +270,14 @@ fn write_picture(picture: &Picture, writer: &mut Cursor<Vec<u8>>) -> Result<()>
}
}
fn write_data(flags: u8, data: &[u8], writer: &mut Cursor<Vec<u8>>) -> Result<()> {
// .... DATA (flags) 0000 (data)
fn write_data(flags: u32, data: &[u8], writer: &mut Cursor<Vec<u8>>) -> Result<()> {
if flags > 16_777_215 {
return Err(LoftyError::BadAtom(
"Attempted to write a code that cannot fit in 24 bits",
));
}
// .... DATA (version = 0) (flags) (locale = 0000) (data)
let size = 16_u64 + data.len() as u64;
writer.write_all(&[0, 0, 0, 0, b'd', b'a', b't', b'a'])?;
@ -328,7 +286,9 @@ fn write_data(flags: u8, data: &[u8], writer: &mut Cursor<Vec<u8>>) -> Result<()
// Version
writer.write_u8(0)?;
writer.write_all(&[0, 0, flags])?;
writer.write_uint::<BigEndian>(u64::from(flags), 3)?;
// Locale
writer.write_all(&[0; 4])?;
writer.write_all(data)?;

View file

@ -1,12 +1,16 @@
mod atom;
pub(in crate::logic) mod ilst;
mod atom_info;
#[cfg(feature = "mp4_atoms")]
pub(crate) mod ilst;
mod moov;
mod properties;
mod read;
mod trak;
use crate::logic::tag_methods;
use crate::types::file::{AudioFile, FileType, TaggedFile};
use crate::{FileProperties, Result, Tag, TagType};
use crate::{FileProperties, Result, TagType};
#[cfg(feature = "mp4_atoms")]
use ilst::Ilst;
use std::io::{Read, Seek};
use std::time::Duration;
@ -72,7 +76,7 @@ pub struct Mp4File {
pub(crate) ftyp: String,
#[cfg(feature = "mp4_atoms")]
/// The [`Tag`] parsed from the ilst atom, not guaranteed
pub(crate) ilst: Option<Tag>,
pub(crate) ilst: Option<Ilst>,
/// The file's audio properties
pub(crate) properties: Mp4Properties,
}
@ -83,7 +87,7 @@ impl From<Mp4File> for TaggedFile {
ty: FileType::MP4,
properties: FileProperties::from(input.properties),
tags: if let Some(ilst) = input.ilst {
vec![ilst]
vec![ilst.into()]
} else {
Vec::new()
},
@ -122,16 +126,8 @@ impl Mp4File {
pub fn ftyp(&self) -> &str {
self.ftyp.as_ref()
}
#[cfg(feature = "mp4_atoms")]
/// Returns a reference to the "ilst" tag if it exists
pub fn ilst(&self) -> Option<&Tag> {
self.ilst.as_ref()
}
#[cfg(feature = "mp4_atoms")]
/// Returns a mutable reference to the "ilst" tag if it exists
pub fn ilst_mut(&mut self) -> Option<&mut Tag> {
self.ilst.as_mut()
}
}
tag_methods! {
Mp4File => ilst, ilst, Ilst
}

View file

@ -1,9 +1,10 @@
use super::atom::Atom;
use super::atom_info::AtomInfo;
#[cfg(feature = "mp4_atoms")]
use super::ilst::read::parse_ilst;
use super::ilst::{AtomIdent, Ilst};
use super::read::skip_unneeded;
use super::trak::Trak;
use crate::error::{LoftyError, Result};
use crate::types::tag::Tag;
use std::io::{Read, Seek};
@ -12,18 +13,18 @@ use byteorder::{BigEndian, ReadBytesExt};
pub(crate) struct Moov {
pub(crate) traks: Vec<Trak>,
// Represents a parsed moov.udta.meta.ilst since we don't need anything else
pub(crate) meta: Option<Tag>,
pub(crate) meta: Option<Ilst>,
}
impl Moov {
pub(crate) fn find<R>(data: &mut R) -> Result<Atom>
pub(crate) fn find<R>(data: &mut R) -> Result<AtomInfo>
where
R: Read + Seek,
{
let mut moov = (false, None);
while let Ok(atom) = Atom::read(data) {
if &*atom.ident == "moov" {
while let Ok(atom) = AtomInfo::read(data) {
if atom.ident == AtomIdent::Fourcc(*b"moov") {
moov = (true, Some(atom));
break;
}
@ -45,21 +46,27 @@ impl Moov {
let mut traks = Vec::new();
let mut meta = None;
while let Ok(atom) = Atom::read(data) {
match &*atom.ident {
"trak" => traks.push(Trak::parse(data, &atom)?),
"udta" => {
meta = meta_from_udta(data, atom.len - 8)?;
while let Ok(atom) = AtomInfo::read(data) {
if let AtomIdent::Fourcc(fourcc) = atom.ident {
match &fourcc {
b"trak" => traks.push(Trak::parse(data, &atom)?),
b"udta" => {
meta = meta_from_udta(data, atom.len - 8)?;
}
_ => skip_unneeded(data, atom.extended, atom.len)?,
}
_ => skip_unneeded(data, atom.extended, atom.len)?,
continue;
}
skip_unneeded(data, atom.extended, atom.len)?
}
Ok(Self { traks, meta })
}
}
fn meta_from_udta<R>(data: &mut R, len: u64) -> Result<Option<Tag>>
fn meta_from_udta<R>(data: &mut R, len: u64) -> Result<Option<Ilst>>
where
R: Read + Seek,
{
@ -67,9 +74,9 @@ where
let mut meta = (false, 0_u64);
while read < len {
let atom = Atom::read(data)?;
let atom = AtomInfo::read(data)?;
if &*atom.ident == "meta" {
if atom.ident == AtomIdent::Fourcc(*b"meta") {
meta = (true, atom.len);
break;
}
@ -91,9 +98,9 @@ where
let mut islt = (false, 0_u64);
while read < meta.1 {
let atom = Atom::read(data)?;
let atom = AtomInfo::read(data)?;
if &*atom.ident == "ilst" {
if atom.ident == AtomIdent::Fourcc(*b"ilst") {
islt = (true, atom.len);
break;
}
@ -102,6 +109,7 @@ where
skip_unneeded(data, atom.extended, atom.len)?;
}
#[cfg(feature = "mp4_atoms")]
if islt.0 {
return parse_ilst(data, islt.1 - 8);
}

View file

@ -1,4 +1,5 @@
use super::atom::Atom;
use super::atom_info::AtomInfo;
use super::ilst::AtomIdent;
use super::read::nested_atom;
use super::read::skip_unneeded;
use super::trak::Trak;
@ -30,32 +31,39 @@ where
let mut read = 8;
while read < mdia.len {
let atom = Atom::read(data)?;
let atom = AtomInfo::read(data)?;
match &*atom.ident {
"mdhd" => {
skip_unneeded(data, atom.extended, atom.len)?;
mdhd = Some(atom)
}
"hdlr" => {
// The hdlr atom is followed by 8 zeros
data.seek(SeekFrom::Current(8))?;
let mut handler_type = [0; 4];
data.read_exact(&mut handler_type)?;
if &handler_type == b"soun" {
audio_track = true
if let AtomIdent::Fourcc(fourcc) = atom.ident {
match &fourcc {
b"mdhd" => {
skip_unneeded(data, atom.extended, atom.len)?;
mdhd = Some(atom)
}
b"hdlr" => {
// The hdlr atom is followed by 8 zeros
data.seek(SeekFrom::Current(8))?;
skip_unneeded(data, atom.extended, atom.len - 12)?;
}
"minf" => minf = Some(atom),
_ => {
skip_unneeded(data, atom.extended, atom.len)?;
read += atom.len
let mut handler_type = [0; 4];
data.read_exact(&mut handler_type)?;
if &handler_type == b"soun" {
audio_track = true
}
skip_unneeded(data, atom.extended, atom.len - 12)?;
}
b"minf" => minf = Some(atom),
_ => {
skip_unneeded(data, atom.extended, atom.len)?;
read += atom.len;
}
}
continue;
}
skip_unneeded(data, atom.extended, atom.len)?;
read += atom.len;
}
}
@ -106,8 +114,8 @@ where
if let Some(minf) = minf {
data.seek(SeekFrom::Start(minf.start + 8))?;
if let Some(stbl) = nested_atom(data, minf.len, "stbl")? {
if let Some(stsd) = nested_atom(data, stbl.len, "stsd")? {
if let Some(stbl) = nested_atom(data, minf.len, b"stbl")? {
if let Some(stsd) = nested_atom(data, stbl.len, b"stsd")? {
let mut stsd = vec![0; (stsd.len - 8) as usize];
data.read_exact(&mut stsd)?;
@ -119,12 +127,18 @@ where
// Number of entries (4)
stsd_reader.seek(SeekFrom::Start(8))?;
let atom = Atom::read(&mut stsd_reader)?;
let atom = AtomInfo::read(&mut stsd_reader)?;
match &*atom.ident {
"mp4a" => mp4a_properties(&mut stsd_reader, &mut properties)?,
"alac" => alac_properties(&mut stsd_reader, &mut properties)?,
unknown => properties.codec = Mp4Codec::Unknown(unknown.to_string()),
if let AtomIdent::Fourcc(ref fourcc) = atom.ident {
match fourcc {
b"mp4a" => mp4a_properties(&mut stsd_reader, &mut properties)?,
b"alac" => alac_properties(&mut stsd_reader, &mut properties)?,
unknown => {
if let Ok(codec) = std::str::from_utf8(unknown) {
properties.codec = Mp4Codec::Unknown(codec.to_string())
}
}
}
}
}
}
@ -157,11 +171,11 @@ where
data.seek(SeekFrom::Current(2))?;
// This information is often followed by an esds (elementary stream descriptor) atom containing the bitrate
if let Ok(esds) = Atom::read(data) {
if let Ok(esds) = AtomInfo::read(data) {
// There are 4 bytes we expect to be zeroed out
// Version (1)
// Flags (3)
if &*esds.ident == "esds" && data.read_u32::<BigEndian>()? == 0 {
if esds.ident == AtomIdent::Fourcc(*b"esds") && data.read_u32::<BigEndian>()? == 0 {
let mut descriptor = [0; 4];
data.read_exact(&mut descriptor)?;
@ -220,8 +234,8 @@ where
// First alac atom's content (28)
data.seek(SeekFrom::Start(44))?;
if let Ok(alac) = Atom::read(data) {
if &*alac.ident == "alac" {
if let Ok(alac) = AtomInfo::read(data) {
if alac.ident == AtomIdent::Fourcc(*b"alac") {
// Skipping 13 bytes
// Version (4)
// Samples per frame (4)

View file

@ -1,8 +1,9 @@
use super::atom::Atom;
use super::atom_info::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};
@ -10,9 +11,9 @@ pub(in crate::logic::mp4) fn verify_mp4<R>(data: &mut R) -> Result<String>
where
R: Read + Seek,
{
let atom = Atom::read(data)?;
let atom = AtomInfo::read(data)?;
if atom.ident != "ftyp" {
if atom.ident != AtomIdent::Fourcc(*b"ftyp") {
return Err(LoftyError::UnknownFormat);
}
@ -61,7 +62,7 @@ where
Ok(())
}
pub(crate) fn nested_atom<R>(data: &mut R, len: u64, expected: &str) -> Result<Option<Atom>>
pub(crate) fn nested_atom<R>(data: &mut R, len: u64, expected: &[u8]) -> Result<Option<AtomInfo>>
where
R: Read + Seek,
{
@ -69,10 +70,10 @@ where
let mut ret = None;
while read < len {
let atom = Atom::read(data)?;
let atom = AtomInfo::read(data)?;
match &*atom.ident {
ident if ident == expected => {
match atom.ident {
AtomIdent::Fourcc(ref fourcc) if fourcc == expected => {
ret = Some(atom);
break;
}

View file

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

View file

@ -2,50 +2,34 @@ mod block;
mod read;
pub(crate) mod write;
use super::tag::VorbisComments;
use crate::error::Result;
use crate::logic::tag_methods;
use crate::types::file::{AudioFile, FileType, TaggedFile};
use crate::types::item::{ItemKey, ItemValue, TagItem};
use crate::types::properties::FileProperties;
use crate::types::tag::{Tag, TagType};
use crate::types::tag::TagType;
use std::io::{Read, Seek};
/// A FLAC file
pub struct FlacFile {
/// The file's audio properties
pub(crate) properties: FileProperties,
#[cfg(feature = "vorbis_comments")]
/// The file vendor's name found in the vorbis comments (if it exists)
pub(crate) vendor: Option<String>,
#[cfg(feature = "vorbis_comments")]
/// The vorbis comments contained in the file
///
/// NOTE: This field being `Some` does not mean the file has vorbis comments, as Picture blocks exist.
pub(crate) vorbis_comments: Option<Tag>,
pub(crate) vorbis_comments: Option<VorbisComments>,
/// The file's audio properties
pub(crate) properties: FileProperties,
}
impl From<FlacFile> for TaggedFile {
fn from(input: FlacFile) -> Self {
// Preserve vendor string
let tags = {
if let Some(mut tag) = input.vorbis_comments {
if let Some(vendor) = input.vendor {
tag.insert_item_unchecked(TagItem::new(
ItemKey::EncoderSoftware,
ItemValue::Text(vendor),
))
}
vec![tag]
} else {
Vec::new()
}
};
Self {
ty: FileType::FLAC,
properties: input.properties,
tags,
tags: input
.vorbis_comments
.map_or_else(Vec::new, |t| vec![t.into()]),
}
}
}
@ -77,16 +61,6 @@ impl AudioFile for FlacFile {
}
}
impl FlacFile {
#[cfg(feature = "vorbis_comments")]
/// Returns a reference to the Vorbis comments tag if it exists
pub fn vorbis_comments(&self) -> Option<&Tag> {
self.vorbis_comments.as_ref()
}
#[cfg(feature = "vorbis_comments")]
/// Returns a mutable reference to the Vorbis comments tag if it exists
pub fn vorbis_comments_mut(&mut self) -> Option<&mut Tag> {
self.vorbis_comments.as_mut()
}
tag_methods! {
FlacFile => Vorbis_Comments, vorbis_comments, VorbisComments
}

View file

@ -2,9 +2,9 @@ use super::block::Block;
use super::FlacFile;
use crate::error::{LoftyError, Result};
use crate::logic::ogg::read::read_comments;
use crate::logic::ogg::tag::VorbisComments;
use crate::picture::Picture;
use crate::types::properties::FileProperties;
use crate::types::tag::{Tag, TagType};
use std::io::{Read, Seek, SeekFrom};
use std::time::Duration;
@ -92,16 +92,21 @@ where
let mut last_block = stream_info.last;
let mut vendor = None;
let mut tag = Tag::new(TagType::VorbisComments);
let mut tag = VorbisComments {
vendor: String::new(),
items: vec![],
pictures: vec![],
};
while !last_block {
let block = Block::read(data)?;
last_block = block.last;
match block.ty {
4 => vendor = Some(read_comments(&mut &*block.content, &mut tag)?),
6 => tag.push_picture(Picture::from_flac_bytes(&*block.content)?),
4 => read_comments(&mut &*block.content, &mut tag)?,
6 => tag
.pictures
.push(Picture::from_flac_bytes(&*block.content)?),
_ => {}
}
}
@ -116,7 +121,6 @@ where
Ok(FlacFile {
properties,
vendor,
vorbis_comments: (!(tag.picture_count() == 0 && tag.item_count() == 0)).then(|| tag),
vorbis_comments: (!(tag.items.is_empty() && tag.pictures.is_empty())).then(|| tag),
})
}

View file

@ -1,21 +1,16 @@
use super::block::Block;
use super::read::verify_flac;
use crate::error::{LoftyError, Result};
use crate::logic::ogg::tag::VorbisCommentsRef;
use crate::logic::ogg::write::create_comments;
use crate::picture::Picture;
use crate::types::item::{ItemKey, ItemValue, TagItem};
use crate::types::tag::{Tag, TagType};
use std::fs::File;
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
use byteorder::{LittleEndian, WriteBytesExt};
pub(in crate::logic) fn write_to(data: &mut File, tag: &Tag) -> Result<()> {
if tag.tag_type() != &TagType::VorbisComments {
return Err(LoftyError::UnsupportedTag);
}
pub(in crate::logic) fn write_to(data: &mut File, tag: &mut VorbisCommentsRef) -> Result<()> {
let stream_info = verify_flac(data)?;
let stream_info_end = stream_info.end as usize;
@ -78,22 +73,13 @@ pub(in crate::logic) fn write_to(data: &mut File, tag: &Tag) -> Result<()> {
);
}
let vendor = if let Some(ItemValue::Text(vendor)) = tag
.get_item_ref(&ItemKey::EncoderSoftware)
.map(TagItem::value)
{
Some(vendor)
} else {
None
};
let mut comment_blocks = Cursor::new(Vec::new());
let mut comment_blocks = Vec::new();
create_comment_block(
&mut comment_blocks,
vendor.unwrap_or(&String::new()),
tag.items(),
)?;
create_picture_blocks(&mut comment_blocks, tag.pictures())?;
create_comment_block(&mut comment_blocks, tag.vendor, &mut tag.items)?;
let mut comment_blocks = comment_blocks.into_inner();
create_picture_blocks(&mut comment_blocks, tag.pictures)?;
if blocks_remove.is_empty() {
file_bytes.splice(0..0, comment_blocks);
@ -117,25 +103,43 @@ pub(in crate::logic) fn write_to(data: &mut File, tag: &Tag) -> Result<()> {
Ok(())
}
fn create_comment_block(writer: &mut Vec<u8>, vendor: &str, items: &[TagItem]) -> Result<()> {
if !items.is_empty() {
fn create_comment_block(
writer: &mut Cursor<Vec<u8>>,
vendor: &str,
items: &mut dyn Iterator<Item = (&str, &String)>,
) -> Result<()> {
let mut peek = items.peekable();
if peek.peek().is_some() {
let mut byte = 0_u8;
byte |= 4 & 0x7f;
writer.write_u8(byte)?;
writer.write_u32::<LittleEndian>(vendor.len() as u32)?;
writer.write_all(vendor.as_bytes())?;
writer.write_u32::<LittleEndian>(items.len() as u32)?;
create_comments(writer, items);
let item_count_pos = writer.seek(SeekFrom::Current(0))?;
let mut count = 0;
let len = (writer.len() - 1) as u32;
writer.write_u32::<LittleEndian>(count)?;
create_comments(writer, &mut count, items)?;
let len = (writer.get_ref().len() - 1) as u32;
if len > 65535 {
return Err(LoftyError::TooMuchData);
}
writer.splice(1..1, len.to_be_bytes()[1..].to_vec());
let comment_end = writer.seek(SeekFrom::Current(0))?;
writer.seek(SeekFrom::Start(item_count_pos))?;
writer.write_u32::<LittleEndian>(count)?;
writer.seek(SeekFrom::Start(comment_end))?;
writer
.get_mut()
.splice(1..1, len.to_be_bytes()[1..].to_vec());
}
Ok(())

View file

@ -6,10 +6,13 @@ use ogg_pager::Page;
pub(crate) mod constants;
pub(crate) mod read;
#[cfg(feature = "vorbis_comments")]
pub(crate) mod write;
pub(crate) mod flac;
pub(crate) mod opus;
#[cfg(feature = "vorbis_comments")]
pub(crate) mod tag;
pub(crate) mod vorbis;
pub fn page_from_packet(packet: &mut [u8]) -> Result<Vec<Page>> {

View file

@ -1,94 +1,34 @@
pub(in crate::logic::ogg) mod properties;
pub(in crate::logic::ogg) mod write;
pub(crate) mod properties;
pub(super) mod write;
use super::find_last_page;
use super::tag::VorbisComments;
use crate::error::Result;
use crate::logic::ogg::constants::{OPUSHEAD, OPUSTAGS};
use crate::types::file::{AudioFile, FileType, TaggedFile};
use crate::types::item::{ItemKey, ItemValue, TagItem};
use crate::types::properties::FileProperties;
use crate::types::tag::{Tag, TagType};
use crate::types::tag::TagType;
use properties::OpusProperties;
use std::io::{Read, Seek};
use std::time::Duration;
/// An Opus file's audio properties
pub struct OpusProperties {
duration: Duration,
bitrate: u32,
channels: u8,
version: u8,
input_sample_rate: u32,
}
impl From<OpusProperties> for FileProperties {
fn from(input: OpusProperties) -> Self {
Self {
duration: input.duration,
bitrate: Some(input.bitrate),
sample_rate: Some(input.input_sample_rate),
channels: Some(input.channels),
}
}
}
impl OpusProperties {
/// Duration
pub fn duration(&self) -> Duration {
self.duration
}
/// Bitrate (kbps)
pub fn bitrate(&self) -> u32 {
self.bitrate
}
/// Channel count
pub fn channels(&self) -> u8 {
self.channels
}
/// Opus version
pub fn version(&self) -> u8 {
self.version
}
/// Input sample rate
pub fn input_sample_rate(&self) -> u32 {
self.input_sample_rate
}
}
/// An OGG Opus file
pub struct OpusFile {
#[cfg(feature = "vorbis_comments")]
/// The file vendor's name
pub(crate) vendor: String,
#[cfg(feature = "vorbis_comments")]
/// The vorbis comments contained in the file
///
/// NOTE: While a metadata packet is required, it isn't required to actually have any data.
pub(crate) vorbis_comments: Tag,
pub(crate) vorbis_comments: VorbisComments,
/// The file's audio properties
pub(crate) properties: OpusProperties,
}
impl From<OpusFile> for TaggedFile {
fn from(input: OpusFile) -> Self {
// Preserve vendor string
let mut tag = input.vorbis_comments;
if !input.vendor.is_empty() {
tag.insert_item_unchecked(TagItem::new(
ItemKey::EncoderSoftware,
ItemValue::Text(input.vendor),
))
}
Self {
ty: FileType::Opus,
properties: FileProperties::from(input.properties),
tags: vec![tag],
tags: vec![input.vorbis_comments.into()],
}
}
}
@ -103,9 +43,10 @@ impl AudioFile for OpusFile {
let file_information = super::read::read_from(reader, OPUSHEAD, OPUSTAGS)?;
Ok(Self {
properties: properties::read_properties(reader, &file_information.2)?,
vendor: file_information.0,
vorbis_comments: file_information.1,
properties: properties::read_properties(reader, &file_information.1)?,
#[cfg(feature = "vorbis_comments")]
// Safe to unwrap, a metadata packet is mandatory in Opus
vorbis_comments: file_information.0.unwrap(),
})
}
@ -129,13 +70,13 @@ impl AudioFile for OpusFile {
impl OpusFile {
#[cfg(feature = "vorbis_comments")]
/// Returns a reference to the Vorbis comments tag
pub fn vorbis_comments(&self) -> &Tag {
pub fn vorbis_comments(&self) -> &VorbisComments {
&self.vorbis_comments
}
#[cfg(feature = "vorbis_comments")]
/// Returns a mutable reference to the Vorbis comments tag
pub fn vorbis_comments_mut(&mut self) -> &mut Tag {
pub fn vorbis_comments_mut(&mut self) -> &mut VorbisComments {
&mut self.vorbis_comments
}
}

View file

@ -1,5 +1,6 @@
use super::{find_last_page, OpusProperties};
use super::find_last_page;
use crate::error::{LoftyError, Result};
use crate::types::properties::FileProperties;
use std::io::{Read, Seek, SeekFrom};
use std::time::Duration;
@ -7,6 +8,53 @@ use std::time::Duration;
use byteorder::{LittleEndian, ReadBytesExt};
use ogg_pager::Page;
/// An Opus file's audio properties
pub struct OpusProperties {
duration: Duration,
bitrate: u32,
channels: u8,
version: u8,
input_sample_rate: u32,
}
impl From<OpusProperties> for FileProperties {
fn from(input: OpusProperties) -> Self {
Self {
duration: input.duration,
bitrate: Some(input.bitrate),
sample_rate: Some(input.input_sample_rate),
channels: Some(input.channels),
}
}
}
impl OpusProperties {
/// Duration
pub fn duration(&self) -> Duration {
self.duration
}
/// Bitrate (kbps)
pub fn bitrate(&self) -> u32 {
self.bitrate
}
/// Channel count
pub fn channels(&self) -> u8 {
self.channels
}
/// Opus version
pub fn version(&self) -> u8 {
self.version
}
/// Input sample rate
pub fn input_sample_rate(&self) -> u32 {
self.input_sample_rate
}
}
pub(in crate::logic::ogg) fn read_properties<R>(
data: &mut R,
first_page: &Page,

View file

@ -1,17 +1,16 @@
use super::tag::VorbisComments;
use super::verify_signature;
use crate::error::{LoftyError, Result};
use crate::picture::Picture;
use crate::types::item::{ItemKey, ItemValue, TagItem};
use crate::types::tag::{Tag, TagType};
use std::io::{Read, Seek, SeekFrom};
use byteorder::{LittleEndian, ReadBytesExt};
use ogg_pager::Page;
pub type OGGTags = (String, Tag, Page);
pub type OGGTags = (Option<VorbisComments>, Page);
pub(crate) fn read_comments<R>(data: &mut R, tag: &mut Tag) -> Result<String>
pub(super) fn read_comments<R>(data: &mut R, tag: &mut VorbisComments) -> Result<()>
where
R: Read,
{
@ -25,6 +24,8 @@ where
Err(_) => return Err(LoftyError::Ogg("File has an invalid vendor string")),
};
tag.vendor = vendor;
let comments_total_len = data.read_u32::<LittleEndian>()?;
for _ in 0..comments_total_len {
@ -37,18 +38,15 @@ where
let split: Vec<&str> = comment.splitn(2, '=').collect();
if split[0] == "METADATA_BLOCK_PICTURE" {
tag.push_picture(Picture::from_flac_bytes(split[1].as_bytes())?)
} else {
// It's safe to unwrap here since any unknown key is wrapped in ItemKey::Unknown
tag.insert_item(TagItem::new(
ItemKey::from_key(&TagType::VorbisComments, split[0]).unwrap(),
ItemValue::Text(split[1].to_string()),
));
match &*split[0] {
"METADATA_BLOCK_PICTURE" => tag
.pictures
.push(Picture::from_flac_bytes(split[1].as_bytes())?),
_ => tag.items.push((split[0].to_string(), split[1].to_string())),
}
}
Ok(vendor)
Ok(())
}
pub(crate) fn read_from<T>(data: &mut T, header_sig: &[u8], comment_sig: &[u8]) -> Result<OGGTags>
@ -78,10 +76,20 @@ where
}
}
let mut tag = Tag::new(TagType::VorbisComments);
#[cfg(feature = "vorbis_comments")]
{
let mut tag = VorbisComments {
vendor: String::new(),
items: vec![],
pictures: vec![],
};
let reader = &mut &md_pages[..];
let vendor = read_comments(reader, &mut tag)?;
let reader = &mut &md_pages[..];
read_comments(reader, &mut tag)?;
Ok((vendor, tag, first_page))
Ok((Some(tag), first_page))
}
#[cfg(not(feature = "vorbis_comments"))]
Ok((None, first_page))
}

124
src/logic/ogg/tag.rs Normal file
View file

@ -0,0 +1,124 @@
use crate::error::{LoftyError, Result};
use crate::logic::ogg::constants::{OPUSHEAD, VORBIS_IDENT_HEAD};
use crate::probe::Probe;
use crate::types::file::FileType;
use crate::types::item::{ItemKey, ItemValue, TagItem};
use crate::types::picture::Picture;
use crate::types::tag::{Tag, TagType};
use std::fs::File;
#[derive(Default)]
/// Vorbis comments
pub struct VorbisComments {
/// An identifier for the encoding software
pub vendor: String,
/// A collection of key-value pairs
pub items: Vec<(String, String)>,
/// A collection of all pictures
pub pictures: Vec<Picture>,
}
impl VorbisComments {
pub fn write_to(&self, file: &mut File) -> Result<()> {
Into::<VorbisCommentsRef>::into(self).write_to(file)
}
}
impl From<VorbisComments> for Tag {
fn from(input: VorbisComments) -> Self {
let mut tag = Tag::new(TagType::VorbisComments);
tag.insert_item_unchecked(TagItem::new(
ItemKey::EncoderSoftware,
ItemValue::Text(input.vendor),
));
for (k, v) in input.items {
tag.insert_item_unchecked(TagItem::new(
ItemKey::from_key(&TagType::VorbisComments, &k),
ItemValue::Text(v),
));
}
for pic in input.pictures {
tag.push_picture(pic)
}
tag
}
}
impl From<Tag> for VorbisComments {
fn from(input: Tag) -> Self {
let mut vorbis_comments = Self::default();
if let Some(vendor) = input.get_string(&ItemKey::EncoderSoftware) {
vorbis_comments.vendor = vendor.to_string()
}
for item in input.items {
// Discard binary items, as they are not allowed in Vorbis comments
let val = match item.value() {
ItemValue::Text(text) | ItemValue::Locator(text) => text,
_ => continue,
};
// Safe to unwrap since all ItemKeys map in Vorbis comments
let key = item.key().map_key(&TagType::VorbisComments, true).unwrap();
vorbis_comments
.items
.push((key.to_string(), val.to_string()));
}
vorbis_comments
}
}
pub(crate) struct VorbisCommentsRef<'a> {
pub vendor: &'a str,
pub items: Box<dyn Iterator<Item = (&'a str, &'a String)> + 'a>,
pub pictures: &'a [Picture],
}
impl<'a> VorbisCommentsRef<'a> {
fn write_to(&mut self, file: &mut File) -> Result<()> {
match Probe::new().file_type(file) {
Some(FileType::FLAC) => super::flac::write::write_to(file, self),
Some(FileType::Opus) => super::write::write(file, self, OPUSHEAD),
Some(FileType::Vorbis) => super::write::write(file, self, VORBIS_IDENT_HEAD),
_ => Err(LoftyError::UnsupportedTag),
}
}
}
impl<'a> Into<VorbisCommentsRef<'a>> for &'a VorbisComments {
fn into(self) -> VorbisCommentsRef<'a> {
VorbisCommentsRef {
vendor: self.vendor.as_str(),
items: Box::new(self.items.as_slice().iter().map(|(k, v)| (k.as_str(), v))),
pictures: self.pictures.as_slice(),
}
}
}
impl<'a> Into<VorbisCommentsRef<'a>> for &'a Tag {
fn into(self) -> VorbisCommentsRef<'a> {
let vendor = self.get_string(&ItemKey::EncoderSoftware).unwrap_or("");
let items = self.items.iter().filter_map(|i| match i.value() {
ItemValue::Text(val) | ItemValue::Locator(val) => Some((
i.key().map_key(&TagType::VorbisComments, true).unwrap(),
val,
)),
_ => None,
});
VorbisCommentsRef {
vendor,
items: Box::new(items),
pictures: self.pictures(),
}
}
}

View file

@ -1,112 +1,34 @@
pub(in crate::logic::ogg) mod properties;
pub(crate) mod properties;
pub(in crate::logic::ogg) mod write;
use super::find_last_page;
use super::tag::VorbisComments;
use crate::error::Result;
use crate::logic::ogg::constants::{VORBIS_COMMENT_HEAD, VORBIS_IDENT_HEAD};
use crate::types::file::{AudioFile, FileType, TaggedFile};
use crate::types::item::{ItemKey, ItemValue, TagItem};
use crate::types::properties::FileProperties;
use crate::types::tag::{Tag, TagType};
use crate::types::tag::TagType;
use properties::VorbisProperties;
use std::io::{Read, Seek};
use std::time::Duration;
/// An OGG Vorbis file's audio properties
pub struct VorbisProperties {
duration: Duration,
bitrate: u32,
sample_rate: u32,
channels: u8,
version: u32,
bitrate_maximum: u32,
bitrate_nominal: u32,
bitrate_minimum: u32,
}
impl From<VorbisProperties> for FileProperties {
fn from(input: VorbisProperties) -> Self {
Self {
duration: input.duration,
bitrate: Some(input.bitrate),
sample_rate: Some(input.sample_rate),
channels: Some(input.channels),
}
}
}
impl VorbisProperties {
/// Duration
pub fn duration(&self) -> Duration {
self.duration
}
/// Bitrate (kbps)
pub fn bitrate(&self) -> u32 {
self.bitrate
}
/// Sample rate (Hz)
pub fn sample_rate(&self) -> u32 {
self.sample_rate
}
/// Channel count
pub fn channels(&self) -> u8 {
self.channels
}
/// Vorbis version
pub fn version(&self) -> u32 {
self.version
}
/// Maximum bitrate
pub fn bitrate_max(&self) -> u32 {
self.bitrate_maximum
}
/// Nominal bitrate
pub fn bitrate_nominal(&self) -> u32 {
self.bitrate_nominal
}
/// Minimum bitrate
pub fn bitrate_min(&self) -> u32 {
self.bitrate_minimum
}
}
/// An OGG Vorbis file
pub struct VorbisFile {
#[cfg(feature = "vorbis_comments")]
/// The file vendor's name
pub(crate) vendor: String,
#[cfg(feature = "vorbis_comments")]
/// The vorbis comments contained in the file
///
/// NOTE: While a metadata packet is required, it isn't required to actually have any data.
pub(crate) vorbis_comments: Tag,
pub(crate) vorbis_comments: VorbisComments,
/// The file's audio properties
pub(crate) properties: VorbisProperties,
}
impl From<VorbisFile> for TaggedFile {
fn from(input: VorbisFile) -> Self {
// Preserve vendor string
let mut tag = input.vorbis_comments;
if !input.vendor.is_empty() {
tag.insert_item_unchecked(TagItem::new(
ItemKey::EncoderSoftware,
ItemValue::Text(input.vendor),
))
}
Self {
ty: FileType::Vorbis,
properties: FileProperties::from(input.properties),
tags: vec![tag],
tags: vec![input.vorbis_comments.into()],
}
}
}
@ -122,9 +44,10 @@ impl AudioFile for VorbisFile {
super::read::read_from(reader, VORBIS_IDENT_HEAD, VORBIS_COMMENT_HEAD)?;
Ok(Self {
properties: properties::read_properties(reader, &file_information.2)?,
vendor: file_information.0,
vorbis_comments: file_information.1,
properties: properties::read_properties(reader, &file_information.1)?,
#[cfg(feature = "vorbis_comments")]
// Safe to unwrap, a metadata packet is mandatory in OGG Vorbis
vorbis_comments: file_information.0.unwrap(),
})
}
@ -148,13 +71,13 @@ impl AudioFile for VorbisFile {
impl VorbisFile {
#[cfg(feature = "vorbis_comments")]
/// Returns a reference to the Vorbis comments tag
pub fn vorbis_comments(&self) -> &Tag {
pub fn vorbis_comments(&self) -> &VorbisComments {
&self.vorbis_comments
}
#[cfg(feature = "vorbis_comments")]
/// Returns a mutable reference to the Vorbis comments tag
pub fn vorbis_comments_mut(&mut self) -> &mut Tag {
pub fn vorbis_comments_mut(&mut self) -> &mut VorbisComments {
&mut self.vorbis_comments
}
}

View file

@ -1,5 +1,6 @@
use super::{find_last_page, VorbisProperties};
use super::find_last_page;
use crate::error::{LoftyError, Result};
use crate::types::properties::FileProperties;
use std::io::{Read, Seek};
use std::time::Duration;
@ -7,6 +8,71 @@ use std::time::Duration;
use byteorder::{LittleEndian, ReadBytesExt};
use ogg_pager::Page;
/// An OGG Vorbis file's audio properties
pub struct VorbisProperties {
duration: Duration,
bitrate: u32,
sample_rate: u32,
channels: u8,
version: u32,
bitrate_maximum: u32,
bitrate_nominal: u32,
bitrate_minimum: u32,
}
impl From<VorbisProperties> for FileProperties {
fn from(input: VorbisProperties) -> Self {
Self {
duration: input.duration,
bitrate: Some(input.bitrate),
sample_rate: Some(input.sample_rate),
channels: Some(input.channels),
}
}
}
impl VorbisProperties {
/// Duration
pub fn duration(&self) -> Duration {
self.duration
}
/// Bitrate (kbps)
pub fn bitrate(&self) -> u32 {
self.bitrate
}
/// Sample rate (Hz)
pub fn sample_rate(&self) -> u32 {
self.sample_rate
}
/// Channel count
pub fn channels(&self) -> u8 {
self.channels
}
/// Vorbis version
pub fn version(&self) -> u32 {
self.version
}
/// Maximum bitrate
pub fn bitrate_max(&self) -> u32 {
self.bitrate_maximum
}
/// Nominal bitrate
pub fn bitrate_nominal(&self) -> u32 {
self.bitrate_nominal
}
/// Minimum bitrate
pub fn bitrate_min(&self) -> u32 {
self.bitrate_minimum
}
}
pub(in crate::logic::ogg) fn read_properties<R>(
data: &mut R,
first_page: &Page,

View file

@ -2,64 +2,83 @@ use super::{page_from_packet, verify_signature};
use crate::error::{LoftyError, Result};
use crate::logic::ogg::constants::OPUSTAGS;
use crate::logic::ogg::constants::VORBIS_COMMENT_HEAD;
use crate::types::item::{ItemValue, TagItem};
use crate::logic::ogg::tag::VorbisCommentsRef;
use crate::types::tag::{Tag, TagType};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use std::convert::TryFrom;
use std::fs::File;
use std::io::{Read, Seek, SeekFrom, Write};
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use ogg_pager::Page;
pub(crate) fn create_comments(packet: &mut Vec<u8>, items: &[TagItem]) {
for item in items {
if let ItemValue::Text(value) = item.value() {
let comment = format!(
"{}={}",
item.key().map_key(&TagType::VorbisComments).unwrap(),
value
);
let comment_b = comment.as_bytes();
let bytes_len = comment_b.len();
if u32::try_from(bytes_len as u64).is_ok() {
packet.extend((bytes_len as u32).to_le_bytes().iter());
packet.extend(comment_b.iter());
}
}
pub(in crate::logic) fn write_to(data: &mut File, tag: &Tag, sig: &[u8]) -> Result<()> {
match tag.tag_type() {
TagType::VorbisComments => write(data, &mut Into::<VorbisCommentsRef>::into(tag), sig),
_ => Err(LoftyError::UnsupportedTag),
}
}
fn create_pages(tag: &Tag, writer: &mut Vec<u8>) -> Result<Vec<Page>> {
let item_count = tag.item_count() + tag.picture_count();
#[cfg(feature = "vorbis_comments")]
pub(crate) fn create_comments(
packet: &mut impl Write,
count: &mut u32,
items: &mut dyn Iterator<Item = (&str, &String)>,
) -> Result<()> {
for (k, v) in items {
let comment = format!("{}={}", k, v);
writer.write_u32::<LittleEndian>(item_count)?;
create_comments(writer, tag.items());
let comment_b = comment.as_bytes();
let bytes_len = comment_b.len();
for pic in tag.pictures() {
if u32::try_from(bytes_len as u64).is_ok() {
*count += 1;
packet.write_all(&(bytes_len as u32).to_le_bytes())?;
packet.write_all(comment_b)?;
}
}
Ok(())
}
#[cfg(feature = "vorbis_comments")]
fn create_pages(tag: &mut VorbisCommentsRef, writer: &mut Cursor<Vec<u8>>) -> Result<Vec<Page>> {
let item_count_pos = writer.seek(SeekFrom::Current(0))?;
writer.write_u32::<LittleEndian>(0)?;
let mut count = 0;
create_comments(writer, &mut count, &mut tag.items)?;
for pic in tag.pictures {
let picture = format!(
"METADATA_BLOCK_PICTURE={}",
base64::encode(pic.as_flac_bytes())
);
let picture_b = picture.as_bytes();
let bytes_len = picture_b.len();
if u32::try_from(bytes_len as u64).is_ok() {
count += 1;
writer.write_u32::<LittleEndian>(bytes_len as u32)?;
writer.write_all(picture_b)?;
}
}
page_from_packet(writer)
let packet_end = writer.seek(SeekFrom::Current(0))?;
writer.seek(SeekFrom::Start(item_count_pos))?;
writer.write_u32::<LittleEndian>(count)?;
writer.seek(SeekFrom::Start(packet_end))?;
page_from_packet(writer.get_mut())
}
pub(in crate::logic) fn write_to(data: &mut File, tag: &Tag, sig: &[u8]) -> Result<()> {
if tag.tag_type() != &TagType::VorbisComments {
return Err(LoftyError::UnsupportedTag);
}
#[cfg(feature = "vorbis_comments")]
pub(super) fn write(data: &mut File, tag: &mut VorbisCommentsRef, sig: &[u8]) -> Result<()> {
let first_page = Page::read(data, false)?;
let ser = first_page.serial;
@ -77,7 +96,8 @@ pub(in crate::logic) fn write_to(data: &mut File, tag: &Tag, sig: &[u8]) -> Resu
let mut vendor = vec![0; vendor_len as usize];
md_reader.read_exact(&mut vendor)?;
let mut packet = Vec::new();
let mut packet = Cursor::new(Vec::new());
packet.write_all(sig)?;
packet.write_u32::<LittleEndian>(vendor_len)?;
packet.write_all(&vendor)?;

View file

@ -1,3 +1,4 @@
use crate::error::{LoftyError, Result};
use crate::logic::ape::ApeFile;
use crate::logic::iff::aiff::AiffFile;
use crate::logic::iff::wav::WavFile;
@ -6,8 +7,7 @@ use crate::logic::mp4::Mp4File;
use crate::logic::ogg::flac::FlacFile;
use crate::logic::ogg::opus::OpusFile;
use crate::logic::ogg::vorbis::VorbisFile;
use crate::types::file::AudioFile;
use crate::{FileType, LoftyError, Result, TaggedFile};
use crate::types::file::{AudioFile, FileType, TaggedFile};
use std::io::{Cursor, Read, Seek};
use std::path::Path;

View file

@ -51,28 +51,27 @@ impl TaggedFile {
/// | `FLAC`, `Opus`, `Vorbis` | `VorbisComments` |
/// | `MP4` | `Mp4Atom` |
pub fn primary_tag(&self) -> Option<&Tag> {
let tag_type = match self.ty {
FileType::AIFF | FileType::MP3 | FileType::WAV => &TagType::Id3v2,
FileType::APE => &TagType::Ape,
FileType::FLAC | FileType::Opus | FileType::Vorbis => &TagType::VorbisComments,
FileType::MP4 => &TagType::Mp4Atom,
};
self.tag(tag_type)
self.tag(&Self::primary_tag_type(self.ty))
}
/// Gets a mutable reference to the file's "Primary tag"
///
/// See [`primary_tag`](Self::primary_tag) for an explanation
pub fn primary_tag_mut(&mut self) -> Option<&mut Tag> {
let tag_type = match self.ty {
FileType::AIFF | FileType::MP3 | FileType::WAV => &TagType::Id3v2,
FileType::APE => &TagType::Ape,
FileType::FLAC | FileType::Opus | FileType::Vorbis => &TagType::VorbisComments,
FileType::MP4 => &TagType::Mp4Atom,
};
self.tag_mut(&Self::primary_tag_type(self.ty))
}
self.tag_mut(tag_type)
fn primary_tag_type(f_ty: FileType) -> TagType {
match f_ty {
#[cfg(feature = "id3v2")]
FileType::AIFF | FileType::MP3 | FileType::WAV => TagType::Id3v2,
#[cfg(feature = "ape")]
FileType::APE => TagType::Ape,
#[cfg(feature = "vorbis_comments")]
FileType::FLAC | FileType::Opus | FileType::Vorbis => TagType::VorbisComments,
#[cfg(feature = "mp4_atoms")]
FileType::MP4 => TagType::Mp4Atom,
}
}
/// Gets the first tag, if there are any

View file

@ -1,8 +1,6 @@
use crate::logic::id3::v1::constants::VALID_ITEMKEYS;
use crate::TagType;
#[cfg(feature = "id3v2")]
use crate::logic::id3::v2::frame::Id3v2Frame;
macro_rules! first_key {
($key:tt $(| $remaining:expr)*) => {
$key
@ -21,7 +19,7 @@ macro_rules! first_key {
// The standard key(s) **must** come before any popular non-standard keys.
// Keys should appear in order of popularity.
macro_rules! item_keys {
(ALLOWED_UNKNOWN => [$($unknown_tag_type:pat),+]; $($variant:ident => [$($($tag_type:pat)|* => $($key:tt)|+),+]),+) => {
($($variant:ident => [$($($tag_type:pat)|* => $($key:tt)|+),+]),+) => {
#[derive(PartialEq, Clone, Debug, Eq, Hash)]
#[allow(missing_docs)]
#[non_exhaustive]
@ -30,9 +28,6 @@ macro_rules! item_keys {
$(
$variant,
)+
#[cfg(feature = "id3v2")]
/// An item that only exists in ID3v2
Id3v2Specific(Id3v2Frame),
/// When a key couldn't be mapped to another variant
///
/// This **will not** allow writing keys that are out of spec (Eg. ID3v2.4 frame IDs **must** be 4 characters)
@ -44,37 +39,29 @@ macro_rules! item_keys {
///
/// NOTE: If used with ID3v2, this will only check against the ID3v2.4 keys.
/// If you wish to use a V2 or V3 key, see [`upgrade_v2`](crate::id3::v2::upgrade_v2) and [`upgrade_v3`](crate::id3::v2::upgrade_v3)
pub fn from_key(tag_type: &TagType, key: &str) -> Option<Self> {
pub fn from_key(tag_type: &TagType, key: &str) -> Self {
match tag_type {
$(
$(
$($tag_type)|* if $(key.eq_ignore_ascii_case($key))||* => Some(ItemKey::$variant),
$($tag_type)|* if $(key.eq_ignore_ascii_case($key))||* => ItemKey::$variant,
)+
)+
$(
$unknown_tag_type => Some(ItemKey::Unknown(key.to_string())),
)+
_ => None,
_ => Self::Unknown(key.to_string()),
}
}
/// Maps the variant to a format-specific key
///
/// NOTE: Since all ID3v2 tags are upgraded to [`Id3v2Version::V4`](crate::id3::v2::Id3v2Version), the
/// version provided does not matter. They cannot be downgraded.
pub fn map_key(&self, tag_type: &TagType) -> Option<&str> {
/// 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) {
$(
$(
($($tag_type)|*, ItemKey::$variant) => Some(first_key!($($key)|*)),
)+
)+
$(
($unknown_tag_type, ItemKey::Unknown(unknown)) => Some(&*unknown),
)+
// Need a special case here to allow for checked insertion, the result isn't actually used.
#[cfg(feature = "id3v2")]
(TagType::Id3v2, ItemKey::Id3v2Specific(_)) => Some(""),
(_, ItemKey::Unknown(unknown)) if allow_unknown => Some(&*unknown),
_ => None,
}
}
@ -83,7 +70,6 @@ macro_rules! item_keys {
}
item_keys!(
ALLOWED_UNKNOWN => [TagType::Ape, TagType::VorbisComments, TagType::Mp4Atom];
// Titles
AlbumTitle => [
TagType::Id3v2 => "TALB", TagType::Mp4Atom => "\u{a9}alb",
@ -432,84 +418,29 @@ item_keys!(
pub enum ItemValue {
/// Any UTF-8 encoded text
Text(String),
/// **(APE/ID3v2 ONLY)** Any UTF-8 encoded locator of external information
/// Any UTF-8 encoded locator of external information
///
/// This is only gets special treatment in ID3v2 and APE tags, being written
/// as a normal string in other tags
Locator(String),
/// **(APE/ID3v2/MP4 ONLY)** Binary information
///
/// In the case of ID3v2, this is the type of a [`Id3v2Frame::EncapsulatedObject`](crate::id3::v2::Id3v2Frame::EncapsulatedObject),
/// [`Id3v2Frame::SyncText`](crate::id3::v2::Id3v2Frame::SyncText), and any unknown frame.
///
/// For APEv2 and MP4, the only use is for unknown items.
/// Binary information
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(any(feature = "id3v2", feature = "ape"))]
#[derive(Clone, Debug, PartialEq, Eq, Hash, Default)]
#[allow(clippy::struct_excessive_bools)]
/// **(ID3v2/APEv2 ONLY)** Various flags to describe the content of an item
///
/// 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
///
/// 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.
/// This is usually used in combination with `compression` and `encryption` (depending on encryption method).
///
/// If using encryption, the final size must be added. It will be ignored if using compression.
pub data_length_indicator: (bool, u32),
pub(crate) enum ItemValueRef<'a> {
Text(&'a str),
Locator(&'a str),
Binary(&'a [u8]),
}
impl<'a> Into<ItemValueRef<'a>> for &'a ItemValue {
fn into(self) -> ItemValueRef<'a> {
match self {
ItemValue::Text(text) => ItemValueRef::Text(text),
ItemValue::Locator(locator) => ItemValueRef::Locator(locator),
ItemValue::Binary(binary) => ItemValueRef::Binary(binary),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
@ -517,8 +448,6 @@ pub struct TagItemFlags {
pub struct TagItem {
pub(crate) item_key: ItemKey,
pub(crate) item_value: ItemValue,
#[cfg(any(feature = "id3v2", feature = "ape"))]
pub(crate) flags: TagItemFlags,
}
impl TagItem {
@ -534,10 +463,9 @@ impl TagItem {
item_key: ItemKey,
item_value: ItemValue,
) -> Option<Self> {
item_key.map_key(tag_type).is_some().then(|| Self {
item_key.map_key(tag_type, false).is_some().then(|| Self {
item_key,
item_value,
flags: TagItemFlags::default(),
})
}
@ -546,22 +474,9 @@ impl TagItem {
Self {
item_key,
item_value,
flags: TagItemFlags::default(),
}
}
#[cfg(any(feature = "id3v2", feature = "ape"))]
/// Returns a reference to the [`TagItemFlags`]
pub fn flags(&self) -> &TagItemFlags {
&self.flags
}
#[cfg(any(feature = "id3v2", feature = "ape"))]
/// Set the item's flags
pub fn set_flags(&mut self, flags: TagItemFlags) {
self.flags = flags
}
/// Returns a reference to the [`ItemKey`]
pub fn key(&self) -> &ItemKey {
&self.item_key
@ -573,13 +488,10 @@ impl TagItem {
}
pub(crate) fn re_map(&self, tag_type: &TagType) -> Option<()> {
#[cfg(any(feature = "id3v2", feature = "ape"))]
{
(!self.flags().read_only && self.item_key.map_key(tag_type).is_some()).then(|| ())
}
#[cfg(not(any(feature = "id3v2", feature = "ape")))]
{
self.item_key.map_key(tag_type).is_some().then(|| ())
if tag_type == &TagType::Id3v1 {
return VALID_ITEMKEYS.contains(&self.item_key).then(|| ());
}
self.item_key.map_key(tag_type, false).is_some().then(|| ())
}
}

View file

@ -1,6 +1,6 @@
use crate::logic::id3::v2::util::text_utils::TextEncoding;
use crate::logic::id3::v2::Id3v2Version;
use crate::{LoftyError, Result};
#[cfg(feature = "id3v2")]
use {crate::logic::id3::v2::util::text_utils::TextEncoding, crate::logic::id3::v2::Id3v2Version};
use std::borrow::Cow;
use std::io::{Cursor, Read};
@ -10,8 +10,8 @@ use byteorder::WriteBytesExt;
#[cfg(any(feature = "vorbis_comments", feature = "id3v2",))]
use byteorder::{BigEndian, ReadBytesExt};
/// Mime types for covers.
#[derive(Debug, Clone, Eq, PartialEq)]
/// Mime types for pictures.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum MimeType {
/// PNG image
Png,
@ -74,7 +74,7 @@ impl MimeType {
/// The picture type
#[allow(missing_docs)]
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub enum PictureType {
Other,
Icon,
@ -165,30 +165,30 @@ impl PictureType {
/// Get an APE item key from a PictureType
#[cfg(feature = "ape")]
pub fn as_ape_key(&self) -> &str {
pub fn as_ape_key(&self) -> Option<&str> {
match self {
Self::Other => "Cover Art (Other)",
Self::Icon => "Cover Art (Png Icon)",
Self::OtherIcon => "Cover Art (Icon)",
Self::CoverFront => "Cover Art (Front)",
Self::CoverBack => "Cover Art (Back)",
Self::Leaflet => "Cover Art (Leaflet)",
Self::Media => "Cover Art (Media)",
Self::LeadArtist => "Cover Art (Lead Artist)",
Self::Artist => "Cover Art (Artist)",
Self::Conductor => "Cover Art (Conductor)",
Self::Band => "Cover Art (Band)",
Self::Composer => "Cover Art (Composer)",
Self::Lyricist => "Cover Art (Lyricist)",
Self::RecordingLocation => "Cover Art (Recording Location)",
Self::DuringRecording => "Cover Art (During Recording)",
Self::DuringPerformance => "Cover Art (During Performance)",
Self::ScreenCapture => "Cover Art (Video Capture)",
Self::BrightFish => "Cover Art (Fish)",
Self::Illustration => "Cover Art (Illustration)",
Self::BandLogo => "Cover Art (Band Logotype)",
Self::PublisherLogo => "Cover Art (Publisher Logotype)",
Self::Undefined(_) => "",
Self::Other => Some("Cover Art (Other)"),
Self::Icon => Some("Cover Art (Png Icon)"),
Self::OtherIcon => Some("Cover Art (Icon)"),
Self::CoverFront => Some("Cover Art (Front)"),
Self::CoverBack => Some("Cover Art (Back)"),
Self::Leaflet => Some("Cover Art (Leaflet)"),
Self::Media => Some("Cover Art (Media)"),
Self::LeadArtist => Some("Cover Art (Lead Artist)"),
Self::Artist => Some("Cover Art (Artist)"),
Self::Conductor => Some("Cover Art (Conductor)"),
Self::Band => Some("Cover Art (Band)"),
Self::Composer => Some("Cover Art (Composer)"),
Self::Lyricist => Some("Cover Art (Lyricist)"),
Self::RecordingLocation => Some("Cover Art (Recording Location)"),
Self::DuringRecording => Some("Cover Art (During Recording)"),
Self::DuringPerformance => Some("Cover Art (During Performance)"),
Self::ScreenCapture => Some("Cover Art (Video Capture)"),
Self::BrightFish => Some("Cover Art (Fish)"),
Self::Illustration => Some("Cover Art (Illustration)"),
Self::BandLogo => Some("Cover Art (Band Logotype)"),
Self::PublisherLogo => Some("Cover Art (Publisher Logotype)"),
Self::Undefined(_) => None,
}
}
@ -223,7 +223,7 @@ impl PictureType {
}
/// Information about a [`Picture`]
#[derive(Debug, Clone, Eq, PartialEq)]
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct PictureInformation {
/// The picture's width in pixels
pub width: u32,
@ -236,10 +236,11 @@ pub struct PictureInformation {
}
/// Represents a picture.
#[derive(Debug, Clone, Eq, PartialEq)]
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Picture {
/// The picture type according to ID3v2 APIC
pub pic_type: PictureType,
#[cfg(feature = "id3v2")]
/// **(ONLY APPLICABLE TO ID3v2)** The text encoding
pub text_encoding: TextEncoding,
/// The picture's mimetype
@ -272,179 +273,123 @@ impl Picture {
}
#[cfg(feature = "id3v2")]
#[allow(clippy::single_match_else)]
/// Convert a [`Picture`] to a ID3v2 A/PIC byte Vec
///
/// NOTE: This does not include the frame header
///
/// # Errors
///
/// * Too much data was provided
///
/// ID3v2.2:
///
/// * The mimetype is not [`MimeType::Png`] or [`MimeType::Jpeg`]
/// * Too much data was provided
///
/// ID3v2.3/4:
///
/// * Too much data was provided
pub fn as_apic_bytes(&self, version: Id3v2Version) -> Result<Vec<u8>> {
match version {
Id3v2Version::V2 => {
// ID3v2.2 PIC is pretty limited with formats
let format = match self.mime_type {
MimeType::Png => "PNG",
MimeType::Jpeg => "JPG",
_ => return Err(LoftyError::BadPictureFormat(self.mime_type.to_string())),
};
let mut data = vec![self.text_encoding as u8];
let mut data = vec![self.text_encoding as u8];
let max_size = if version == Id3v2Version::V2 {
// ID3v2.2 PIC is pretty limited with formats
let format = match self.mime_type {
MimeType::Png => "PNG",
MimeType::Jpeg => "JPG",
_ => return Err(LoftyError::BadPictureFormat(self.mime_type.to_string())),
};
data.write_all(format.as_bytes())?;
data.write_u8(self.pic_type.as_u8())?;
data.write_all(format.as_bytes())?;
if let Some(description) = &self.description {
data.write_all(&*crate::logic::id3::v2::util::text_utils::encode_text(
description,
self.text_encoding,
true,
))?;
}
// ID3v2.2 uses a 24-bit number for sizes
0xFFFF_FF16_u64
} else {
data.write_all(self.mime_type.as_str().as_bytes())?;
data.write_u8(0)?;
data.write_all(&*self.data)?;
u64::from(u32::MAX)
};
let size = data.len() - 6;
data.write_u8(self.pic_type.as_u8())?;
if size as u64 > u64::from(u32::MAX) {
return Err(LoftyError::TooMuchData);
}
let size = (size as u32).to_be_bytes();
if size[0] != 0 {
return Err(LoftyError::TooMuchData);
}
Ok(data)
}
_ => {
let mut data = vec![self.text_encoding as u8];
data.write_all(self.mime_type.as_str().as_bytes())?;
data.write_u8(self.pic_type.as_u8())?;
if let Some(description) = &self.description {
data.write_all(&*crate::logic::id3::v2::util::text_utils::encode_text(
description,
self.text_encoding,
true,
))?;
}
data.write_u8(0)?;
data.write_all(&*self.data)?;
let size = data.len();
if size as u64 > u64::from(u32::MAX) {
return Err(LoftyError::TooMuchData);
}
Ok(data)
match &self.description {
Some(description) => {
data.write_all(&*crate::logic::id3::v2::util::text_utils::encode_text(
description,
self.text_encoding,
true,
))?
}
None => data.write_u8(0)?,
}
data.write_all(&*self.data)?;
let size = data.len();
if size as u64 > max_size {
return Err(LoftyError::TooMuchData);
}
Ok(data)
}
#[cfg(feature = "id3v2")]
#[allow(clippy::single_match_else)]
/// Get a [`Picture`] from ID3v2 A/PIC bytes:
///
/// NOTE: This expects the frame header to have already been skipped
///
/// # Errors
///
/// This function will return [`NotAPicture`][LoftyError::NotAPicture] if at any point it's unable to parse the data
/// * There isn't enough data present
/// * The data isn't a picture
///
/// ID3v2.2:
///
/// * The format is not "PNG" or "JPG"
pub fn from_apic_bytes(bytes: &[u8], version: Id3v2Version) -> Result<Self> {
let mut cursor = Cursor::new(bytes);
if let Some(encoding) = TextEncoding::from_u8(cursor.read_u8()?) {
return match version {
Id3v2Version::V2 => {
let mut format = [0; 3];
cursor.read_exact(&mut format)?;
let encoding = match TextEncoding::from_u8(cursor.read_u8()?) {
Some(encoding) => encoding,
None => return Err(LoftyError::NotAPicture),
};
let mime_type = match format {
[b'P', b'N', b'G'] => MimeType::Png,
[b'J', b'P', b'G'] => MimeType::Jpeg,
_ => {
return Err(LoftyError::BadPictureFormat(
String::from_utf8_lossy(&format).to_string(),
))
}
};
let mime_type = if version == Id3v2Version::V2 {
let mut format = [0; 3];
cursor.read_exact(&mut format)?;
let picture_type = PictureType::from_u8(cursor.read_u8()?);
let description = crate::logic::id3::v2::util::text_utils::decode_text(
&mut cursor,
encoding,
true,
)?
.map(Cow::from);
let mut data = Vec::new();
cursor.read_to_end(&mut data)?;
Ok(Picture {
pic_type: picture_type,
text_encoding: encoding,
mime_type,
description,
information: PictureInformation {
width: 0,
height: 0,
color_depth: 0,
num_colors: 0,
},
data: Cow::from(data),
})
}
match format {
[b'P', b'N', b'G'] => MimeType::Png,
[b'J', b'P', b'G'] => MimeType::Jpeg,
_ => {
let mime_type = (crate::logic::id3::v2::util::text_utils::decode_text(
&mut cursor,
encoding,
true,
)?)
.map_or(MimeType::None, |mime_type| MimeType::from_str(&*mime_type));
let picture_type = PictureType::from_u8(cursor.read_u8()?);
let description = crate::logic::id3::v2::util::text_utils::decode_text(
&mut cursor,
encoding,
true,
)?
.map(Cow::from);
let mut data = Vec::new();
cursor.read_to_end(&mut data)?;
Ok(Picture {
pic_type: picture_type,
text_encoding: encoding,
mime_type,
description,
information: PictureInformation {
width: 0,
height: 0,
color_depth: 0,
num_colors: 0,
},
data: Cow::from(data),
})
return Err(LoftyError::BadPictureFormat(
String::from_utf8_lossy(&format).to_string(),
))
}
};
}
}
} else {
(crate::logic::id3::v2::util::text_utils::decode_text(&mut cursor, encoding, true)?)
.map_or(MimeType::None, |mime_type| MimeType::from_str(&*mime_type))
};
Err(LoftyError::NotAPicture)
let picture_type = PictureType::from_u8(cursor.read_u8()?);
let description =
crate::logic::id3::v2::util::text_utils::decode_text(&mut cursor, encoding, true)?
.map(Cow::from);
let mut data = Vec::new();
cursor.read_to_end(&mut data)?;
Ok(Picture {
pic_type: picture_type,
text_encoding: encoding,
mime_type,
description,
information: PictureInformation {
width: 0,
height: 0,
color_depth: 0,
num_colors: 0,
},
data: Cow::from(data),
})
}
#[cfg(feature = "vorbis_comments")]
@ -497,10 +442,7 @@ impl Picture {
///
/// This function will return [`NotAPicture`][LoftyError::NotAPicture] if at any point it's unable to parse the data
pub fn from_flac_bytes(bytes: &[u8]) -> Result<Self> {
let data = match base64::decode(bytes) {
Ok(o) => o,
Err(_) => bytes.to_vec(),
};
let data = base64::decode(bytes).unwrap_or_else(|_| bytes.to_vec());
let mut cursor = Cursor::new(data);

View file

@ -1,17 +1,13 @@
use super::item::{ItemKey, ItemValue, TagItem};
use super::picture::{Picture, PictureType};
use crate::error::{LoftyError, Result};
#[cfg(feature = "id3v2_restrictions")]
use crate::logic::id3::v2::items::restrictions::TagRestrictions;
use crate::probe::Probe;
use std::fs::{File, OpenOptions};
use std::path::Path;
#[cfg(feature = "quick_tag_accessors")]
use paste::paste;
#[cfg(feature = "quick_tag_accessors")]
macro_rules! common_items {
($($item_key:ident => $name:tt),+) => {
paste! {
@ -41,38 +37,14 @@ macro_rules! common_items {
}
}
#[cfg(feature = "id3v2")]
#[derive(Default, Copy, Clone)]
#[allow(clippy::struct_excessive_bools)]
/// **(ID3v2 ONLY)** Flags that apply to the entire tag
pub struct TagFlags {
/// Whether or not all frames are unsynchronised. See [`TagItemFlags::unsynchronisation`](crate::TagItemFlags::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),
}
#[derive(Clone)]
/// Represents a parsed tag
///
/// NOTE: Items and pictures are separated
pub struct Tag {
tag_type: TagType,
pictures: Vec<Picture>,
items: Vec<TagItem>,
#[cfg(feature = "id3v2")]
flags: TagFlags,
pub(crate) pictures: Vec<Picture>,
pub(crate) items: Vec<TagItem>,
}
impl IntoIterator for Tag {
@ -84,33 +56,6 @@ impl IntoIterator for Tag {
}
}
impl Tag {
/// The tag's items as a slice
pub fn as_slice(&self) -> &[TagItem] {
&*self.items
}
/// Retain tag items based on the predicate
///
/// See [`Vec::retain`](std::vec::Vec::retain)
pub fn retain<F>(&mut self, f: F)
where
F: FnMut(&TagItem) -> bool,
{
self.items.retain(f)
}
/// Find the first TagItem matching the predicate
///
/// See [`Iterator::find`](std::iter::Iterator::find)
pub fn find<P>(&mut self, predicate: P) -> Option<&TagItem>
where
P: for<'a> FnMut(&'a &TagItem) -> bool,
{
self.items.iter().find(predicate)
}
}
impl Tag {
/// Initialize a new tag with a certain [`TagType`]
pub fn new(tag_type: TagType) -> Self {
@ -118,15 +63,6 @@ impl Tag {
tag_type,
pictures: vec![],
items: vec![],
flags: TagFlags::default(),
}
}
#[cfg(feature = "id3v2")]
/// **(ID3v2 ONLY)** Restrict the tag's flags
pub fn set_flags(&mut self, flags: TagFlags) {
if TagType::Id3v2 == self.tag_type {
self.flags = flags
}
}
}
@ -154,12 +90,6 @@ impl Tag {
pub fn item_count(&self) -> u32 {
self.items.len() as u32
}
#[cfg(feature = "id3v2")]
/// Returns the [`TagFlags`]
pub fn flags(&self) -> &TagFlags {
&self.flags
}
}
impl Tag {
@ -225,10 +155,7 @@ impl Tag {
/// Insert a [`TagItem`], replacing any existing one of the same type
///
/// NOTES:
///
/// * This **will** respect [`TagItemFlags::read_only`](crate::TagItemFlags::read_only)
/// * This **will** verify an [`ItemKey`] mapping exists for the target [`TagType`]
/// NOTE: This **will** verify an [`ItemKey`] mapping exists for the target [`TagType`]
///
/// # Warning
///
@ -247,17 +174,21 @@ impl Tag {
///
/// Notes:
///
/// * This **will not** respect [`TagItemFlags::read_only`](crate::TagItemFlags::read_only)
/// * This **will not** verify an [`ItemKey`] mapping exists
/// * This **will not** allow writing item keys that are out of spec (keys are verified before writing)
///
/// This is only necessary if using [`ItemKey::Unknown`] or single [`ItemKey`]s that are parts of larger lists.
/// This is only necessary if dealing with [`ItemKey::Unknown`].
pub fn insert_item_unchecked(&mut self, item: TagItem) {
match self.items.iter_mut().find(|i| i.item_key == item.item_key) {
None => self.items.push(item),
Some(i) => *i = item,
};
}
/// An alias for [`Tag::insert_item`] that doesn't require the user to create a [`TagItem`]
pub fn insert_text(&mut self, item_key: ItemKey, text: String) -> bool {
self.insert_item(TagItem::new(item_key, ItemValue::Text(text)))
}
}
impl Tag {
@ -302,7 +233,33 @@ impl Tag {
}
}
#[cfg(feature = "quick_tag_accessors")]
impl Tag {
/// The tag's items as a slice
pub fn as_slice(&self) -> &[TagItem] {
&*self.items
}
/// Retain tag items based on the predicate
///
/// See [`Vec::retain`](std::vec::Vec::retain)
pub fn retain<F>(&mut self, f: F)
where
F: FnMut(&TagItem) -> bool,
{
self.items.retain(f)
}
/// Find the first TagItem matching the predicate
///
/// See [`Iterator::find`](std::iter::Iterator::find)
pub fn find<P>(&mut self, predicate: P) -> Option<&TagItem>
where
P: for<'a> FnMut(&'a &TagItem) -> bool,
{
self.items.iter().find(predicate)
}
}
common_items!(TrackArtist => artist, TrackTitle => title, AlbumTitle => album_title, AlbumArtist => album_artist);
/// The tag's format

View file

@ -1,14 +1,12 @@
mod util;
use lofty::{FileType, ItemKey, ItemValue, Probe, TagItem, TagType};
use std::io::Write;
use std::io::{Seek, Write};
#[test]
fn read() {
// Here we have an AIFF file with both an ID3v2 chunk and text chunks
let file = Probe::new()
.read_from_path("tests/assets/a_mixed.aiff")
.unwrap();
let file = Probe::new().read_from_path("tests/assets/a.aiff").unwrap();
assert_eq!(file.file_type(), &FileType::AIFF);
@ -21,10 +19,8 @@ fn read() {
#[test]
fn write() {
let mut file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open("tests/assets/a_mixed.aiff")
let mut file = tempfile::tempfile().unwrap();
file.write_all(&std::fs::read("tests/assets/a.aiff").unwrap())
.unwrap();
let mut tagged_file = Probe::new().read_from(&mut file).unwrap();
@ -37,6 +33,8 @@ fn write() {
// Text chunks
crate::set_artist!(tagged_file, tag_mut, TagType::AiffText, "Bar artist", 1 => file, "Baz artist");
drop(tagged_file);
// Now reread the file
let mut tagged_file = Probe::new().read_from(&mut file).unwrap();
@ -47,10 +45,10 @@ fn write() {
#[test]
fn remove_text_chunks() {
crate::remove_tag!("tests/assets/a_mixed.aiff", TagType::AiffText);
crate::remove_tag!("tests/assets/a.aiff", TagType::AiffText);
}
#[test]
fn remove_id3v2() {
crate::remove_tag!("tests/assets/a_mixed.aiff", TagType::Id3v2);
crate::remove_tag!("tests/assets/a.aiff", TagType::Id3v2);
}

View file

@ -1,7 +1,7 @@
mod util;
use lofty::{FileType, ItemKey, ItemValue, Probe, TagItem, TagType};
use std::io::Write;
use std::io::{Seek, Write};
#[test]
fn read() {
@ -23,11 +23,8 @@ fn read() {
#[test]
fn write() {
// We don't write an ID3v2 tag here since it's against the spec
let mut file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open("tests/assets/a.ape")
let mut file = tempfile::tempfile().unwrap();
file.write_all(&std::fs::read("tests/assets/a.ape").unwrap())
.unwrap();
let mut tagged_file = Probe::new().read_from(&mut file).unwrap();

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,7 +1,7 @@
mod util;
use lofty::{FileType, ItemKey, ItemValue, Probe, TagItem, TagType};
use std::io::Write;
use std::io::{Seek, Write};
#[test]
fn read() {
@ -16,10 +16,8 @@ fn read() {
#[test]
fn write() {
let mut file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open("tests/assets/a.m4a")
let mut file = tempfile::tempfile().unwrap();
file.write_all(&std::fs::read("tests/assets/a.m4a").unwrap())
.unwrap();
let mut tagged_file = Probe::new().read_from(&mut file).unwrap();

View file

@ -1,7 +1,7 @@
mod util;
use lofty::{FileType, ItemKey, ItemValue, Probe, TagItem, TagType};
use std::io::Write;
use std::io::{Seek, Write};
#[test]
fn read() {
@ -22,10 +22,8 @@ fn read() {
#[test]
fn write() {
let mut file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open("tests/assets/a.mp3")
let mut file = tempfile::tempfile().unwrap();
file.write_all(&std::fs::read("tests/assets/a.mp3").unwrap())
.unwrap();
let mut tagged_file = Probe::new().read_from(&mut file).unwrap();

View file

@ -1,7 +1,7 @@
mod util;
use lofty::{FileType, ItemKey, ItemValue, Probe, TagItem, TagType};
use std::io::Write;
use std::io::{Seek, Write};
// The tests for OGG Opus/Vorbis are nearly identical
// We have the vendor string and a title stored in the tag
@ -61,11 +61,8 @@ fn read(path: &str, file_type: &FileType) {
}
fn write(path: &str, file_type: &FileType) {
let mut file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(path)
.unwrap();
let mut file = tempfile::tempfile().unwrap();
file.write_all(&std::fs::read(path).unwrap()).unwrap();
let mut tagged_file = Probe::new().read_from(&mut file).unwrap();
@ -73,6 +70,8 @@ fn write(path: &str, file_type: &FileType) {
crate::set_artist!(tagged_file, primary_tag_mut, "Foo artist", 2 => file, "Bar artist");
drop(tagged_file);
// Now reread the file
let mut tagged_file = Probe::new().read_from(&mut file).unwrap();

View file

@ -70,6 +70,8 @@ macro_rules! remove_tag {
assert!($tag_type.remove_from(&mut file));
file.seek(std::io::SeekFrom::Start(0)).unwrap();
let tagged_file = Probe::new().read_from(&mut file).unwrap();
assert!(tagged_file.tag(&$tag_type).is_none());
};

View file

@ -1,7 +1,7 @@
mod util;
use lofty::{FileType, ItemKey, ItemValue, Probe, TagItem, TagType};
use std::io::Write;
use std::io::{Seek, Write};
#[test]
fn read() {
@ -21,10 +21,8 @@ fn read() {
#[test]
fn write() {
let mut file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open("tests/assets/a_mixed.wav")
let mut file = tempfile::tempfile().unwrap();
file.write_all(&std::fs::read("tests/assets/a_mixed.wav").unwrap())
.unwrap();
let mut tagged_file = Probe::new().read_from(&mut file).unwrap();
@ -37,6 +35,8 @@ fn write() {
// RIFF INFO
crate::set_artist!(tagged_file, tag_mut, TagType::RiffInfo, "Bar artist", 1 => file, "Baz artist");
drop(tagged_file);
// Now reread the file
let mut tagged_file = Probe::new().read_from(&mut file).unwrap();