use crate::storage::{PlainStorage, Storage};
use crate::stream::{frame, unsynch};
use crate::tag::{Tag, Version};
use crate::taglike::TagLike;
use crate::{Error, ErrorKind};
use bitflags::bitflags;
use byteorder::{BigEndian, ByteOrder, ReadBytesExt, WriteBytesExt};
use std::cmp;
use std::fs;
use std::io::{self, Read, Write};
use std::ops::Range;
use std::path::Path;
static DEFAULT_FILE_DISCARD: &[&str] = &[
"AENC", "ETCO", "EQUA", "MLLT", "POSS", "SYLT", "SYTC", "RVAD", "TENC", "TLEN", "TSIZ",
];
bitflags! {
struct Flags: u8 {
const UNSYNCHRONISATION = 0x80; const COMPRESSION = 0x40; const EXTENDED_HEADER = 0x40; const EXPERIMENTAL = 0x20; const FOOTER = 0x10; }
}
struct Header {
version: Version,
flags: Flags,
tag_size: u32,
}
impl Header {
fn size(&self) -> u64 {
10 }
fn frame_bytes(&self) -> u64 {
u64::from(self.tag_size)
}
fn tag_size(&self) -> u64 {
self.size() + self.frame_bytes()
}
}
impl Header {
fn decode(mut reader: impl io::Read) -> crate::Result<Header> {
let mut header = [0; 10];
let nread = reader.read(&mut header)?;
if nread < header.len() || &header[0..3] != b"ID3" {
return Err(Error::new(
ErrorKind::NoTag,
"reader does not contain an id3 tag",
));
}
let (ver_major, ver_minor) = (header[3], header[4]);
let version = match (ver_major, ver_minor) {
(2, _) => Version::Id3v22,
(3, _) => Version::Id3v23,
(4, _) => Version::Id3v24,
(_, _) => {
return Err(Error::new(
ErrorKind::UnsupportedFeature,
format!(
"Unsupported id3 tag version: v2.{}.{}",
ver_major, ver_minor
),
));
}
};
let flags = Flags::from_bits(header[5])
.ok_or_else(|| Error::new(ErrorKind::Parsing, "unknown tag header flags are set"))?;
let tag_size = unsynch::decode_u32(BigEndian::read_u32(&header[6..10]));
if version == Version::Id3v22 && flags.contains(Flags::COMPRESSION) {
return Err(Error::new(
ErrorKind::UnsupportedFeature,
"id3v2.2 compression is not supported",
));
}
if flags.contains(Flags::EXTENDED_HEADER) {
let ext_size = unsynch::decode_u32(reader.read_u32::<BigEndian>()?) as usize;
if ext_size < 6 {
return Err(Error::new(
ErrorKind::Parsing,
"Extended header has a minimum size of 6",
));
}
let ext_remaining_size = ext_size - 4;
let mut ext_header = Vec::with_capacity(cmp::min(ext_remaining_size, 0xffff));
reader
.by_ref()
.take(ext_remaining_size as u64)
.read_to_end(&mut ext_header)?;
if flags.contains(Flags::UNSYNCHRONISATION) {
unsynch::decode_vec(&mut ext_header);
}
}
Ok(Header {
version,
flags,
tag_size,
})
}
}
pub fn decode(mut reader: impl io::Read) -> crate::Result<Tag> {
let header = Header::decode(&mut reader)?;
if header.version == Version::Id3v22 {
let v2_reader = reader.take(header.frame_bytes());
if header.flags.contains(Flags::UNSYNCHRONISATION) {
decode_v2_frames(unsynch::Reader::new(v2_reader))
} else {
decode_v2_frames(v2_reader)
}
} else {
let mut offset = 0;
let mut tag = Tag::with_version(header.version);
while offset < header.frame_bytes() {
let rs = frame::decode(
&mut reader,
header.version,
header.flags.contains(Flags::UNSYNCHRONISATION),
);
let v = match rs {
Ok(v) => v,
Err(err) => return Err(err.with_tag(tag)),
};
let (bytes_read, frame) = match v {
Some(v) => v,
None => break, };
tag.add_frame(frame);
offset += bytes_read as u64;
}
Ok(tag)
}
}
pub fn decode_v2_frames(mut reader: impl io::Read) -> crate::Result<Tag> {
let mut tag = Tag::with_version(Version::Id3v22);
loop {
let v = match frame::v2::decode(&mut reader) {
Ok(v) => v,
Err(err) => return Err(err.with_tag(tag)),
};
match v {
Some((_bytes_read, frame)) => {
tag.add_frame(frame);
}
None => break Ok(tag),
}
}
}
#[derive(Clone, Debug)]
pub struct Encoder {
version: Version,
unsynchronisation: bool,
compression: bool,
file_altered: bool,
padding: Option<usize>,
}
impl Encoder {
pub fn new() -> Self {
Self {
version: Version::Id3v24,
unsynchronisation: false,
compression: false,
file_altered: false,
padding: None,
}
}
pub fn padding(mut self, padding: usize) -> Self {
self.padding = Some(padding);
self
}
pub fn version(mut self, version: Version) -> Self {
self.version = version;
self
}
pub fn unsynchronisation(mut self, unsynchronisation: bool) -> Self {
self.unsynchronisation = unsynchronisation;
self
}
pub fn compression(mut self, compression: bool) -> Self {
self.compression = compression;
self
}
pub fn file_altered(mut self, file_altered: bool) -> Self {
self.file_altered = file_altered;
self
}
pub fn encode(&self, tag: &Tag, mut writer: impl io::Write) -> crate::Result<()> {
let saved_frames = tag
.frames()
.filter(|frame| !frame.tag_alter_preservation())
.filter(|frame| !self.file_altered || !frame.file_alter_preservation())
.filter(|frame| !self.file_altered || !DEFAULT_FILE_DISCARD.contains(&frame.id()));
let mut flags = Flags::empty();
flags.set(Flags::UNSYNCHRONISATION, self.unsynchronisation);
if self.version == Version::Id3v22 {
flags.set(Flags::COMPRESSION, self.compression);
}
let mut frame_data = Vec::new();
for frame in saved_frames {
frame.validate()?;
frame::encode(&mut frame_data, frame, self.version, self.unsynchronisation)?;
}
if self.version == Version::Id3v22 && self.unsynchronisation {
unsynch::encode_vec(&mut frame_data)
}
let tag_size = frame_data.len() + self.padding.unwrap_or(0);
writer.write_all(b"ID3")?;
writer.write_all(&[self.version.minor() as u8, 0])?;
writer.write_u8(flags.bits())?;
writer.write_u32::<BigEndian>(unsynch::encode_u32(tag_size as u32))?;
writer.write_all(&frame_data[..])?;
if let Some(padding) = self.padding {
writer.write_all(&vec![0; padding])?;
}
Ok(())
}
pub fn encode_to_file(&self, tag: &Tag, mut file: &mut fs::File) -> crate::Result<()> {
#[allow(clippy::reversed_empty_ranges)]
let location = locate_id3v2(&mut file)?.unwrap_or(0..0);
let mut storage = PlainStorage::new(file, location);
let mut w = storage.writer()?;
self.encode(tag, &mut w)?;
w.flush()?;
Ok(())
}
pub fn encode_to_path(&self, tag: &Tag, path: impl AsRef<Path>) -> crate::Result<()> {
let mut file = fs::OpenOptions::new().read(true).write(true).open(path)?;
self.encode_to_file(tag, &mut file)?;
file.flush()?;
Ok(())
}
}
impl Default for Encoder {
fn default() -> Self {
Self::new()
}
}
pub fn locate_id3v2(mut reader: impl io::Read + io::Seek) -> crate::Result<Option<Range<u64>>> {
let header = match Header::decode(&mut reader) {
Ok(v) => v,
Err(err) => match err.kind {
ErrorKind::NoTag => return Ok(None),
_ => return Err(err),
},
};
let tag_size = header.tag_size();
reader.seek(io::SeekFrom::Start(tag_size))?;
let num_padding = reader
.bytes()
.take_while(|rs| rs.as_ref().map(|b| *b == 0x00).unwrap_or(false))
.count();
Ok(Some(0..tag_size + num_padding as u64))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::frame::{
Chapter, Content, EncapsulatedObject, Frame, MpegLocationLookupTable,
MpegLocationLookupTableReference, Picture, PictureType, Popularimeter, SynchronisedLyrics,
SynchronisedLyricsType, TimestampFormat, Unknown,
};
use std::fs;
use std::io::{self, Read};
fn make_tag(version: Version) -> Tag {
let mut tag = Tag::new();
tag.set_title("Title");
tag.set_artist("Artist");
tag.set_genre("Genre");
tag.add_frame(Frame::with_content(
"TPE4",
Content::new_text_values(["artist 1", "artist 2", "artist 3"]),
));
tag.set_duration(1337);
tag.add_frame(EncapsulatedObject {
mime_type: "Some Object".to_string(),
filename: "application/octet-stream".to_string(),
description: "".to_string(),
data: b"\xC0\xFF\xEE\x00".to_vec(),
});
let mut image_data = Vec::new();
fs::File::open("testdata/image.jpg")
.unwrap()
.read_to_end(&mut image_data)
.unwrap();
tag.add_frame(Picture {
mime_type: "image/jpeg".to_string(),
picture_type: PictureType::CoverFront,
description: "an image".to_string(),
data: image_data,
});
tag.add_frame(Popularimeter {
user: "user@example.com".to_string(),
rating: 255,
counter: 1337,
});
tag.add_frame(SynchronisedLyrics {
lang: "eng".to_string(),
timestamp_format: TimestampFormat::Ms,
content_type: SynchronisedLyricsType::Lyrics,
content: vec![
(1000, "he".to_string()),
(1100, "llo".to_string()),
(1200, "world".to_string()),
],
description: String::from("description"),
});
if let Version::Id3v23 | Version::Id3v24 = version {
tag.add_frame(Chapter {
element_id: "01".to_string(),
start_time: 1000,
end_time: 2000,
start_offset: 0xff,
end_offset: 0xff,
frames: vec![
Frame::with_content("TIT2", Content::Text("Foo".to_string())),
Frame::with_content("TALB", Content::Text("Bar".to_string())),
Frame::with_content("TCON", Content::Text("Baz".to_string())),
],
});
tag.add_frame(MpegLocationLookupTable {
frames_between_reference: 1,
bytes_between_reference: 418,
millis_between_reference: 12,
bits_for_bytes: 4,
bits_for_millis: 4,
references: vec![
MpegLocationLookupTableReference {
deviate_bytes: 0xa,
deviate_millis: 0xf,
},
MpegLocationLookupTableReference {
deviate_bytes: 0xa,
deviate_millis: 0x0,
},
],
});
}
tag
}
#[test]
fn read_id3v22() {
let mut file = fs::File::open("testdata/id3v22.id3").unwrap();
let tag: Tag = decode(&mut file).unwrap();
assert_eq!("Henry Frottey INTRO", tag.title().unwrap());
assert_eq!("Hörbuch & Gesprochene Inhalte", tag.genre().unwrap());
assert_eq!(1, tag.disc().unwrap());
assert_eq!(27, tag.total_discs().unwrap());
assert_eq!(2015, tag.year().unwrap());
assert_eq!(
PictureType::Other,
tag.pictures().nth(0).unwrap().picture_type
);
assert_eq!("", tag.pictures().nth(0).unwrap().description);
assert_eq!("image/jpeg", tag.pictures().nth(0).unwrap().mime_type);
}
#[test]
fn read_id3v23() {
let mut file = fs::File::open("testdata/id3v23.id3").unwrap();
let tag = decode(&mut file).unwrap();
assert_eq!("Title", tag.title().unwrap());
assert_eq!("Genre", tag.genre().unwrap());
assert_eq!(1, tag.disc().unwrap());
assert_eq!(1, tag.total_discs().unwrap());
assert_eq!(
PictureType::CoverFront,
tag.pictures().nth(0).unwrap().picture_type
);
}
#[test]
fn read_id3v23_geob() {
let mut file = fs::File::open("testdata/id3v23_geob.id3").unwrap();
let tag = decode(&mut file).unwrap();
assert_eq!(tag.encapsulated_objects().count(), 7);
let geob = tag.encapsulated_objects().nth(0).unwrap();
assert_eq!(geob.description, "Serato Overview");
assert_eq!(geob.mime_type, "application/octet-stream");
assert_eq!(geob.filename, "");
assert_eq!(geob.data.len(), 3842);
let geob = tag.encapsulated_objects().nth(1).unwrap();
assert_eq!(geob.description, "Serato Analysis");
assert_eq!(geob.mime_type, "application/octet-stream");
assert_eq!(geob.filename, "");
assert_eq!(geob.data.len(), 2);
let geob = tag.encapsulated_objects().nth(2).unwrap();
assert_eq!(geob.description, "Serato Autotags");
assert_eq!(geob.mime_type, "application/octet-stream");
assert_eq!(geob.filename, "");
assert_eq!(geob.data.len(), 21);
let geob = tag.encapsulated_objects().nth(3).unwrap();
assert_eq!(geob.description, "Serato Markers_");
assert_eq!(geob.mime_type, "application/octet-stream");
assert_eq!(geob.filename, "");
assert_eq!(geob.data.len(), 318);
let geob = tag.encapsulated_objects().nth(4).unwrap();
assert_eq!(geob.description, "Serato Markers2");
assert_eq!(geob.mime_type, "application/octet-stream");
assert_eq!(geob.filename, "");
assert_eq!(geob.data.len(), 470);
let geob = tag.encapsulated_objects().nth(5).unwrap();
assert_eq!(geob.description, "Serato BeatGrid");
assert_eq!(geob.mime_type, "application/octet-stream");
assert_eq!(geob.filename, "");
assert_eq!(geob.data.len(), 39);
let geob = tag.encapsulated_objects().nth(6).unwrap();
assert_eq!(geob.description, "Serato Offsets_");
assert_eq!(geob.mime_type, "application/octet-stream");
assert_eq!(geob.filename, "");
assert_eq!(geob.data.len(), 29829);
}
#[test]
fn read_id3v23_chap() {
let mut file = fs::File::open("testdata/id3v23_chap.id3").unwrap();
let tag = decode(&mut file).unwrap();
assert_eq!(tag.chapters().count(), 7);
let chapter_titles = tag
.chapters()
.map(|chap| chap.frames.first().unwrap().content().text().unwrap())
.collect::<Vec<&str>>();
assert_eq!(
chapter_titles,
&[
"MPU 554",
"Read-it-Later Services?",
"Safari Reading List",
"Third-Party Services",
"What We’re Using",
"David’s Research Workflow",
"Apple’s September"
]
);
}
#[test]
fn read_id3v24() {
let mut file = fs::File::open("testdata/id3v24.id3").unwrap();
let tag = decode(&mut file).unwrap();
assert_eq!("Title", tag.title().unwrap());
assert_eq!(1, tag.disc().unwrap());
assert_eq!(1, tag.total_discs().unwrap());
assert_eq!(
PictureType::CoverFront,
tag.pictures().nth(0).unwrap().picture_type
);
}
#[test]
fn read_id3v24_extended() {
let mut file = fs::File::open("testdata/id3v24_ext.id3").unwrap();
let tag = decode(&mut file).unwrap();
assert_eq!("Title", tag.title().unwrap());
assert_eq!("Genre", tag.genre().unwrap());
assert_eq!("Artist", tag.artist().unwrap());
assert_eq!("Album", tag.album().unwrap());
assert_eq!(2, tag.track().unwrap());
}
#[test]
fn write_id3v22() {
let tag = make_tag(Version::Id3v22);
let mut buffer = Vec::new();
Encoder::new()
.version(Version::Id3v22)
.encode(&tag, &mut buffer)
.unwrap();
let tag_read = decode(&mut io::Cursor::new(buffer)).unwrap();
assert_eq!(tag, tag_read);
}
#[test]
fn write_id3v22_unsynch() {
let tag = make_tag(Version::Id3v22);
let mut buffer = Vec::new();
Encoder::new()
.unsynchronisation(true)
.version(Version::Id3v22)
.encode(&tag, &mut buffer)
.unwrap();
let tag_read = decode(&mut io::Cursor::new(buffer)).unwrap();
assert_eq!(tag, tag_read);
}
#[test]
fn write_id3v22_invalid_id() {
let mut tag = make_tag(Version::Id3v22);
tag.add_frame(Frame::with_content(
"XXX",
Content::Unknown(Unknown {
version: Version::Id3v22,
data: vec![1, 2, 3],
}),
));
tag.add_frame(Frame::with_content(
"YYY",
Content::Unknown(Unknown {
version: Version::Id3v22,
data: vec![4, 5, 6],
}),
));
tag.add_frame(Frame::with_content(
"ZZZ",
Content::Unknown(Unknown {
version: Version::Id3v22,
data: vec![7, 8, 9],
}),
));
let mut buffer = Vec::new();
Encoder::new()
.version(Version::Id3v22)
.encode(&tag, &mut buffer)
.unwrap();
let tag_read = decode(&mut io::Cursor::new(buffer)).unwrap();
assert_eq!(tag, tag_read);
}
#[test]
fn write_id3v23() {
let tag = make_tag(Version::Id3v23);
let mut buffer = Vec::new();
Encoder::new()
.version(Version::Id3v23)
.encode(&tag, &mut buffer)
.unwrap();
let tag_read = decode(&mut io::Cursor::new(buffer)).unwrap();
assert_eq!(tag, tag_read);
}
#[test]
fn write_id3v23_compression() {
let tag = make_tag(Version::Id3v23);
let mut buffer = Vec::new();
Encoder::new()
.compression(true)
.version(Version::Id3v23)
.encode(&tag, &mut buffer)
.unwrap();
let tag_read = decode(&mut io::Cursor::new(buffer)).unwrap();
assert_eq!(tag, tag_read);
}
#[test]
fn write_id3v23_unsynch() {
let tag = make_tag(Version::Id3v23);
let mut buffer = Vec::new();
Encoder::new()
.unsynchronisation(true)
.version(Version::Id3v23)
.encode(&tag, &mut buffer)
.unwrap();
let tag_read = decode(&mut io::Cursor::new(buffer)).unwrap();
assert_eq!(tag, tag_read);
}
#[test]
fn write_id3v24() {
let tag = make_tag(Version::Id3v24);
let mut buffer = Vec::new();
Encoder::new()
.version(Version::Id3v24)
.encode(&tag, &mut buffer)
.unwrap();
let tag_read = decode(&mut io::Cursor::new(buffer)).unwrap();
assert_eq!(tag, tag_read);
}
#[test]
fn write_id3v24_compression() {
let tag = make_tag(Version::Id3v24);
let mut buffer = Vec::new();
Encoder::new()
.compression(true)
.version(Version::Id3v24)
.encode(&tag, &mut buffer)
.unwrap();
let tag_read = decode(&mut io::Cursor::new(buffer)).unwrap();
assert_eq!(tag, tag_read);
}
#[test]
fn write_id3v24_unsynch() {
let tag = make_tag(Version::Id3v24);
let mut buffer = Vec::new();
Encoder::new()
.unsynchronisation(true)
.version(Version::Id3v24)
.encode(&tag, &mut buffer)
.unwrap();
let tag_read = decode(&mut io::Cursor::new(buffer)).unwrap();
assert_eq!(tag, tag_read);
}
#[test]
fn write_id3v24_alter_file() {
let mut tag = Tag::new();
tag.set_duration(1337);
let mut buffer = Vec::new();
Encoder::new()
.version(Version::Id3v24)
.file_altered(true)
.encode(&tag, &mut buffer)
.unwrap();
let tag_read = decode(&mut io::Cursor::new(buffer)).unwrap();
assert!(tag_read.get("TLEN").is_none());
}
#[test]
fn test_locate_id3v22() {
let file = fs::File::open("testdata/id3v22.id3").unwrap();
let location = locate_id3v2(file).unwrap();
assert_eq!(Some(0..0x0000c3ea), location);
}
#[test]
fn test_locate_id3v23() {
let file = fs::File::open("testdata/id3v23.id3").unwrap();
let location = locate_id3v2(file).unwrap();
assert_eq!(Some(0..0x00006c0a), location);
}
#[test]
fn test_locate_id3v24() {
let file = fs::File::open("testdata/id3v24.id3").unwrap();
let location = locate_id3v2(file).unwrap();
assert_eq!(Some(0..0x00006c0a), location);
}
#[test]
fn test_locate_id3v24_ext() {
let file = fs::File::open("testdata/id3v24_ext.id3").unwrap();
let location = locate_id3v2(file).unwrap();
assert_eq!(Some(0..0x0000018d), location);
}
#[test]
fn test_locate_no_tag() {
let file = fs::File::open("testdata/mpeg-header").unwrap();
let location = locate_id3v2(file).unwrap();
assert_eq!(None, location);
}
#[test]
fn read_github_issue_60() {
let mut file = fs::File::open("testdata/github-issue-60.id3").unwrap();
let err = decode(&mut file).err().unwrap();
err.partial_tag.unwrap();
}
#[test]
fn read_github_issue_73() {
let mut file = fs::File::open("testdata/github-issue-73.id3").unwrap();
let mut tag = decode(&mut file).unwrap();
assert_eq!(tag.track(), Some(9));
tag.set_total_tracks(16);
assert_eq!(tag.track(), Some(9));
assert_eq!(tag.total_tracks(), Some(16));
}
}