Add tests for reading individual tag formats

This commit is contained in:
Serial 2021-11-27 13:28:40 -05:00
parent f7bb543f36
commit 5638326ff2
22 changed files with 418 additions and 23 deletions

View file

@ -5,6 +5,7 @@ use crate::types::tag::TagType;
use std::convert::TryFrom;
#[derive(Debug, PartialEq)]
pub struct ApeItem {
pub read_only: bool,
pub(crate) key: String,

View file

@ -10,8 +10,9 @@ use crate::types::tag::{Tag, TagType};
use std::collections::HashMap;
use std::convert::TryInto;
use std::fs::File;
use std::io::{Read, Seek};
#[derive(Default)]
#[derive(Default, Debug, PartialEq)]
/// An APE tag
pub struct ApeTag {
pub read_only: bool,
@ -33,6 +34,13 @@ impl ApeTag {
}
impl ApeTag {
pub fn read_from<R>(reader: &mut R) -> Result<Self>
where
R: Read + Seek,
{
Ok(read::read_ape_tag(reader, false)?.0)
}
pub fn write_to(&self, file: &mut File) -> Result<()> {
Into::<ApeTagRef>::into(self).write_to(file)
}

View file

@ -19,10 +19,10 @@ pub fn parse_id3v1(reader: [u8; 128]) -> Id3v1Tag {
tag.album = decode_text(&reader[60..90]);
tag.year = decode_text(&reader[90..94]);
let range = if reader[119] == 0 && reader[122] != 0 {
tag.track_number = Some(reader[122]);
let range = if reader[119] == 0 && reader[123] != 0 {
tag.track_number = Some(reader[123]);
94_usize..123
94_usize..122
} else {
94..124
};

View file

@ -5,7 +5,7 @@ use crate::types::tag::{Tag, TagType};
use std::fs::File;
#[derive(Default, Debug)]
#[derive(Default, Debug, PartialEq)]
/// An ID3v1 tag
///
/// ID3v1 is a severely limited format, with each field
@ -59,6 +59,10 @@ impl Id3v1Tag {
&& self.genre.is_none()
}
pub fn read_from(tag: [u8; 128]) -> Self {
super::read::parse_id3v1(tag)
}
pub fn write_to(&self, file: &mut File) -> Result<()> {
Into::<Id3v1TagRef>::into(self).write_to(file)
}

View file

@ -11,7 +11,10 @@ use std::io::Read;
use byteorder::{BigEndian, ReadBytesExt};
pub(crate) fn parse_id3v2(bytes: &mut &[u8]) -> Result<Id3v2Tag> {
pub(crate) fn parse_id3v2<R>(bytes: &mut R) -> Result<Id3v2Tag>
where
R: Read,
{
let mut header = [0; 10];
bytes.read_exact(&mut header)?;

View file

@ -11,9 +11,11 @@ use crate::types::tag::{Tag, TagType};
use std::convert::TryInto;
use std::fs::File;
use std::io::Read;
use byteorder::ByteOrder;
#[derive(PartialEq, Debug)]
pub struct Id3v2Tag {
flags: Id3v2TagFlags,
pub(super) original_version: Id3v2Version,
@ -80,6 +82,13 @@ impl Id3v2Tag {
}
impl Id3v2Tag {
pub fn read_from<R>(reader: &mut R) -> Result<Self>
where
R: Read,
{
super::read::parse_id3v2(reader)
}
pub fn write_to(&self, file: &mut File) -> Result<()> {
Into::<Id3v2TagRef>::into(self).write_to(file)
}
@ -156,7 +165,7 @@ impl From<Tag> for Id3v2Tag {
}
}
#[derive(Default, Copy, Clone)]
#[derive(Default, Copy, Clone, Debug, PartialEq)]
#[allow(clippy::struct_excessive_bools)]
/// Flags that apply to the entire tag
pub struct Id3v2TagFlags {

View file

@ -27,7 +27,7 @@ impl<B: ByteOrder> Chunks<B> {
pub fn next<R>(&mut self, data: &mut R) -> Result<()>
where
R: Read + Seek,
R: Read,
{
data.read_exact(&mut self.fourcc)?;
self.size = data.read_u32::<B>()?;
@ -37,7 +37,7 @@ impl<B: ByteOrder> Chunks<B> {
pub fn content<R>(&mut self, data: &mut R) -> Result<Vec<u8>>
where
R: Read + Seek,
R: Read,
{
let mut content = vec![0; self.size as usize];
data.read_exact(&mut content)?;

View file

@ -6,8 +6,9 @@ use crate::types::item::{ItemKey, ItemValue, TagItem};
use crate::types::tag::{Tag, TagType};
use std::fs::File;
use std::io::{Read, Seek};
#[derive(Default)]
#[derive(Default, Debug, PartialEq)]
/// A RIFF INFO LIST
pub struct RiffInfoList {
/// A collection of chunk-value pairs
@ -15,8 +16,12 @@ pub struct RiffInfoList {
}
impl RiffInfoList {
pub fn push(&mut self, key: String, value: String) {
pub fn insert(&mut self, key: String, value: String) {
if valid_key(key.as_str()) {
self.items
.iter()
.position(|(k, _)| k == &key)
.map(|p| self.items.remove(p));
self.items.push((key, value))
}
}
@ -34,6 +39,17 @@ impl RiffInfoList {
}
impl RiffInfoList {
pub fn read_from<R>(reader: &mut R, end: u64) -> Result<Self>
where
R: Read + Seek,
{
let mut tag = Self::default();
read::parse_riff_info(reader, end, &mut tag)?;
Ok(tag)
}
pub fn write_to(&self, file: &mut File) -> Result<()> {
Into::<RiffInfoListRef>::into(self).write_to(file)
}

View file

@ -13,7 +13,7 @@ use std::time::Duration;
use byteorder::{BigEndian, ByteOrder, ReadBytesExt};
fn read_properties(
mut first_frame: (Header, u64),
first_frame: (Header, u64),
last_frame: (Header, u64),
xing_header: Option<XingHeader>,
file_length: u64,

View file

@ -1,8 +1,8 @@
use crate::error::{LoftyError, Result};
use crate::logic::mp4::ilst::AtomIdent;
use std::io::{Read, Seek, SeekFrom};
use crate::mp4::AtomIdent;
use byteorder::{BigEndian, ReadBytesExt};
pub(crate) struct AtomInfo {

View file

@ -1,19 +1,48 @@
pub(in crate::logic::mp4) mod read;
pub(in crate::logic) mod write;
use crate::error::Result;
use crate::types::item::{ItemKey, ItemValue, TagItem};
use crate::types::picture::{Picture, PictureType};
use crate::types::tag::{Tag, TagType};
use std::convert::TryInto;
use std::io::Read;
#[cfg(feature = "mp4_atoms")]
#[derive(Default)]
#[derive(Default, PartialEq, Debug)]
/// An Mp4
pub struct Ilst {
pub(crate) atoms: Vec<Atom>,
}
impl Ilst {
pub fn atom(&self, ident: &AtomIdent) -> Option<&Atom> {
self.atoms.iter().find(|a| &a.ident == ident)
}
pub fn insert_atom(&mut self, atom: Atom) {
self.remove_atom(&atom.ident);
self.atoms.push(atom);
}
pub fn remove_atom(&mut self, ident: &AtomIdent) {
self.atoms
.iter()
.position(|a| &a.ident == ident)
.map(|p| self.atoms.remove(p));
}
}
impl Ilst {
pub fn read_from<R>(reader: &mut R, len: u64) -> Result<Self>
where
R: Read,
{
read::parse_ilst(reader, len)
}
}
#[cfg(feature = "mp4_atoms")]
impl From<Ilst> for Tag {
fn from(input: Ilst) -> Self {
@ -80,11 +109,18 @@ impl From<Tag> for Ilst {
}
#[cfg(feature = "mp4_atoms")]
#[derive(Debug, PartialEq)]
pub struct Atom {
ident: AtomIdent,
data: AtomData,
}
impl Atom {
pub fn new(ident: AtomIdent, data: AtomData) -> Self {
Self { ident, data }
}
}
#[derive(Eq, PartialEq, Debug)]
pub enum AtomIdent {
/// A four byte identifier
@ -109,6 +145,7 @@ pub enum AtomIdent {
}
#[cfg(feature = "mp4_atoms")]
#[derive(Debug, PartialEq)]
/// The data of an atom
///
/// NOTES:

View file

@ -10,12 +10,12 @@ use std::io::{Cursor, Read, Seek, SeekFrom};
use byteorder::ReadBytesExt;
pub(crate) fn parse_ilst<R>(data: &mut R, len: u64) -> Result<Option<Ilst>>
pub(crate) fn parse_ilst<R>(reader: &mut R, len: u64) -> Result<Ilst>
where
R: Read + Seek,
R: Read,
{
let mut contents = vec![0; len as usize];
data.read_exact(&mut contents)?;
reader.read_exact(&mut contents)?;
let mut cursor = Cursor::new(contents);
@ -71,7 +71,7 @@ where
tag.atoms.push(Atom { ident, data })
}
Ok(Some(tag))
Ok(tag)
}
fn parse_data<R>(data: &mut R) -> Result<AtomData>

View file

@ -111,7 +111,7 @@ where
#[cfg(feature = "mp4_atoms")]
if islt.0 {
return parse_ilst(data, islt.1 - 8);
return parse_ilst(data, islt.1 - 8).map(Some);
}
Ok(None)

View file

@ -8,19 +8,67 @@ use crate::types::picture::PictureInformation;
use crate::types::tag::{Tag, TagType};
use std::fs::File;
use std::io::Read;
#[derive(Default)]
#[derive(Default, PartialEq, Debug)]
/// Vorbis comments
pub struct VorbisComments {
/// An identifier for the encoding software
pub vendor: String,
pub(crate) vendor: String,
/// A collection of key-value pairs
pub items: Vec<(String, String)>,
pub(crate) items: Vec<(String, String)>,
/// A collection of all pictures
pub pictures: Vec<(Picture, PictureInformation)>,
pub(crate) pictures: Vec<(Picture, PictureInformation)>,
}
impl VorbisComments {
pub fn vendor(&self) -> &str {
&self.vendor
}
pub fn set_vendor(&mut self, vendor: String) {
self.vendor = vendor
}
pub fn items(&self) -> &[(String, String)] {
&self.items
}
pub fn get_item(&self, key: &str) -> Option<&str> {
self.items
.iter()
.find(|(k, _)| k == key)
.map(|(_, v)| v.as_str())
}
pub fn insert_item(&mut self, key: String, value: String, replace_all: bool) {
if replace_all {
self.items
.iter()
.position(|(k, _)| k == &key)
.map(|p| self.items.remove(p));
}
self.items.push((key, value))
}
pub fn remove_key(&mut self, key: &str) {
self.items.retain(|(k, _)| k != key);
}
}
impl VorbisComments {
pub fn read_from<R>(reader: &mut R) -> Result<Self>
where
R: Read,
{
let mut tag = Self::default();
super::read::read_comments(reader, &mut tag)?;
Ok(tag)
}
pub fn write_to(&self, file: &mut File) -> Result<()> {
Into::<VorbisCommentsRef>::into(self).write_to(file)
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
tests/tags/assets/test.ilst Normal file

Binary file not shown.

BIN
tests/tags/assets/test.riff Normal file

Binary file not shown.

Binary file not shown.

1
tests/tags/main.rs Normal file
View file

@ -0,0 +1 @@
mod read;

268
tests/tags/read.rs Normal file
View file

@ -0,0 +1,268 @@
use lofty::ape::{ApeItem, ApeTag};
use lofty::id3::v1::Id3v1Tag;
use lofty::id3::v2::{Frame, FrameFlags, FrameValue, Id3v2Tag, LanguageFrame, TextEncoding};
use lofty::iff::RiffInfoList;
use lofty::mp4::{Atom, AtomData, AtomIdent, Ilst};
use lofty::ogg::VorbisComments;
use lofty::ItemValue;
const APE: [u8; 209] = *include_bytes!("assets/test.apev2");
const ID3V1: [u8; 128] = *include_bytes!("assets/test.id3v1");
const ID3V2: [u8; 1168] = *include_bytes!("assets/test.id3v2");
const ILST: [u8; 1024] = *include_bytes!("assets/test.ilst");
const RIFF_INFO: [u8; 100] = *include_bytes!("assets/test.riff");
const VORBIS_COMMENTS: [u8; 152] = *include_bytes!("assets/test.vorbis");
#[test]
fn read_ape() {
let mut expected_tag = ApeTag::default();
let title_item = ApeItem::new(
String::from("TITLE"),
ItemValue::Text(String::from("Foo title")),
)
.unwrap();
let artist_item = ApeItem::new(
String::from("ARTIST"),
ItemValue::Text(String::from("Bar artist")),
)
.unwrap();
let album_item = ApeItem::new(
String::from("ALBUM"),
ItemValue::Text(String::from("Baz album")),
)
.unwrap();
let comment_item = ApeItem::new(
String::from("COMMENT"),
ItemValue::Text(String::from("Qux comment")),
)
.unwrap();
let year_item =
ApeItem::new(String::from("YEAR"), ItemValue::Text(String::from("1984"))).unwrap();
let track_number_item =
ApeItem::new(String::from("TRACK"), ItemValue::Text(String::from("1"))).unwrap();
let genre_item = ApeItem::new(
String::from("GENRE"),
ItemValue::Text(String::from("Classical")),
)
.unwrap();
expected_tag.push_item(title_item);
expected_tag.push_item(artist_item);
expected_tag.push_item(album_item);
expected_tag.push_item(comment_item);
expected_tag.push_item(year_item);
expected_tag.push_item(track_number_item);
expected_tag.push_item(genre_item);
let parsed_tag = ApeTag::read_from(&mut std::io::Cursor::new(APE)).unwrap();
assert_eq!(expected_tag, parsed_tag);
}
#[test]
fn read_id3v1() {
let expected_tag = Id3v1Tag {
title: Some(String::from("Foo title")),
artist: Some(String::from("Bar artist")),
album: Some(String::from("Baz album")),
year: Some(String::from("1984")),
comment: Some(String::from("Qux comment")),
track_number: Some(1),
genre: Some(32),
};
let parsed_tag = Id3v1Tag::read_from(ID3V1);
assert_eq!(expected_tag, parsed_tag);
}
#[test]
fn read_id3v2() {
let mut expected_tag = Id3v2Tag::default();
let encoding = TextEncoding::Latin1;
let flags = FrameFlags::default();
expected_tag.insert(
Frame::new(
"TPE1",
FrameValue::Text {
encoding,
value: String::from("Bar artist"),
},
flags,
)
.unwrap(),
);
expected_tag.insert(
Frame::new(
"TIT2",
FrameValue::Text {
encoding,
value: String::from("Foo title"),
},
flags,
)
.unwrap(),
);
expected_tag.insert(
Frame::new(
"TALB",
FrameValue::Text {
encoding,
value: String::from("Baz album"),
},
flags,
)
.unwrap(),
);
expected_tag.insert(
Frame::new(
"COMM",
FrameValue::Comment(LanguageFrame {
encoding,
language: String::from("eng"),
description: String::new(),
content: String::from("Qux comment"),
}),
flags,
)
.unwrap(),
);
expected_tag.insert(
Frame::new(
"TDRC",
FrameValue::Text {
encoding,
value: String::from("1984"),
},
flags,
)
.unwrap(),
);
expected_tag.insert(
Frame::new(
"TRCK",
FrameValue::Text {
encoding,
value: String::from("1"),
},
flags,
)
.unwrap(),
);
expected_tag.insert(
Frame::new(
"TCON",
FrameValue::Text {
encoding,
value: String::from("Classical"),
},
flags,
)
.unwrap(),
);
let parsed_tag = Id3v2Tag::read_from(&mut &ID3V2[..]).unwrap();
assert_eq!(expected_tag, parsed_tag);
}
#[test]
fn read_mp4_ilst() {
let mut expected_tag = Ilst::default();
// The track number is stored with a code 0,
// meaning the there is no need to indicate the type,
// which is `u64` in this case
expected_tag.insert_atom(Atom::new(
AtomIdent::Fourcc(*b"trkn"),
AtomData::Unknown {
code: 0,
data: vec![0, 0, 0, 1, 0, 0, 0, 0],
},
));
expected_tag.insert_atom(Atom::new(
AtomIdent::Fourcc(*b"\xa9ART"),
AtomData::UTF8(String::from("Bar artist")),
));
expected_tag.insert_atom(Atom::new(
AtomIdent::Fourcc(*b"\xa9alb"),
AtomData::UTF8(String::from("Baz album")),
));
expected_tag.insert_atom(Atom::new(
AtomIdent::Fourcc(*b"\xa9cmt"),
AtomData::UTF8(String::from("Qux comment")),
));
expected_tag.insert_atom(Atom::new(
AtomIdent::Fourcc(*b"\xa9day"),
AtomData::UTF8(String::from("1984")),
));
expected_tag.insert_atom(Atom::new(
AtomIdent::Fourcc(*b"\xa9gen"),
AtomData::UTF8(String::from("Classical")),
));
expected_tag.insert_atom(Atom::new(
AtomIdent::Fourcc(*b"\xa9nam"),
AtomData::UTF8(String::from("Foo title")),
));
let parsed_tag = Ilst::read_from(&mut &ILST[..], ILST.len() as u64).unwrap();
assert_eq!(expected_tag, parsed_tag);
}
#[test]
fn read_riff_info() {
let mut expected_tag = RiffInfoList::default();
expected_tag.insert(String::from("IART"), String::from("Bar artist"));
expected_tag.insert(String::from("ICMT"), String::from("Qux comment"));
expected_tag.insert(String::from("ICRD"), String::from("1984"));
expected_tag.insert(String::from("INAM"), String::from("Foo title"));
expected_tag.insert(String::from("IPRD"), String::from("Baz album"));
expected_tag.insert(String::from("IPRT"), String::from("1"));
let mut reader = std::io::Cursor::new(&RIFF_INFO[..]);
let parsed_tag = RiffInfoList::read_from(&mut reader, (RIFF_INFO.len() - 1) as u64).unwrap();
assert_eq!(expected_tag, parsed_tag);
}
#[test]
fn read_vorbis_comments() {
let mut expected_tag = VorbisComments::default();
expected_tag.set_vendor(String::from("Lavf58.76.100"));
expected_tag.insert_item(String::from("ALBUM"), String::from("Baz album"), false);
expected_tag.insert_item(String::from("ARTIST"), String::from("Bar artist"), false);
expected_tag.insert_item(String::from("COMMENT"), String::from("Qux comment"), false);
expected_tag.insert_item(String::from("DATE"), String::from("1984"), false);
expected_tag.insert_item(String::from("GENRE"), String::from("Classical"), false);
expected_tag.insert_item(String::from("TITLE"), String::from("Foo title"), false);
expected_tag.insert_item(String::from("TRACKNUMBER"), String::from("1"), false);
let parsed_tag = VorbisComments::read_from(&mut &VORBIS_COMMENTS[..]).unwrap();
assert_eq!(expected_tag, parsed_tag);
}