use std::fmt;
use std::fs::File;
use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write};
use std::ops::Deref;
use crate::{data, Content, ContentT, Data, DataT, ErrorKind, Tag};
#[rustfmt::skip]
pub const VALID_FILETYPES: [&str; 8] = [
"iso2",
"isom",
"m4a ",
"m4b ",
"m4p ",
"m4v ",
"mp41",
"mp42",
];
pub const FILETYPE: Ident = Ident(*b"ftyp");
pub const MOVIE: Ident = Ident(*b"moov");
pub const TRACK: Ident = Ident(*b"trak");
pub const MEDIA: Ident = Ident(*b"mdia");
pub const MEDIA_HEADER: Ident = Ident(*b"mdhd");
pub const USER_DATA: Ident = Ident(*b"udta");
pub const METADATA: Ident = Ident(*b"meta");
pub const ITEM_LIST: Ident = Ident(*b"ilst");
pub const DATA: Ident = Ident(*b"data");
pub const ALBUM: Ident = Ident(*b"\xa9alb");
pub const ALBUM_ARTIST: Ident = Ident(*b"aART");
pub const ARTIST: Ident = Ident(*b"\xa9ART");
pub const ARTWORK: Ident = Ident(*b"covr");
pub const BPM: Ident = Ident(*b"tmpo");
pub const COMMENT: Ident = Ident(*b"\xa9cmt");
pub const COMPILATION: Ident = Ident(*b"cpil");
pub const COMPOSER: Ident = Ident(*b"\xa9wrt");
pub const COPYRIGHT: Ident = Ident(*b"cprt");
pub const CUSTOM_GENRE: Ident = Ident(*b"\xa9gen");
pub const DISC_NUMBER: Ident = Ident(*b"disk");
pub const ENCODER: Ident = Ident(*b"\xa9too");
pub const ADVISORY_RATING: Ident = Ident(*b"rtng");
pub const STANDARD_GENRE: Ident = Ident(*b"gnre");
pub const TITLE: Ident = Ident(*b"\xa9nam");
pub const TRACK_NUMBER: Ident = Ident(*b"trkn");
pub const YEAR: Ident = Ident(*b"\xa9day");
pub const GROUPING: Ident = Ident(*b"\xa9grp");
pub const MEDIA_TYPE: Ident = Ident(*b"stik");
pub const CATEGORY: Ident = Ident(*b"catg");
pub const KEYWORD: Ident = Ident(*b"keyw");
pub const PODCAST: Ident = Ident(*b"pcst");
pub const PODCAST_EPISODE_GLOBAL_UNIQUE_ID: Ident = Ident(*b"egid");
pub const PODCAST_URL: Ident = Ident(*b"purl");
pub const DESCRIPTION: Ident = Ident(*b"desc");
pub const LYRICS: Ident = Ident(*b"\xa9lyr");
pub const TV_EPISODE: Ident = Ident(*b"tves");
pub const TV_EPISODE_NUMBER: Ident = Ident(*b"tven");
pub const TV_NETWORK_NAME: Ident = Ident(*b"tvnn");
pub const TV_SEASON: Ident = Ident(*b"tvsn");
pub const TV_SHOW_NAME: Ident = Ident(*b"tvsh");
pub const PURCHASE_DATE: Ident = Ident(*b"purd");
pub const GAPLESS_PLAYBACK: Ident = Ident(*b"pgap");
pub const MOVEMENT: Ident = Ident(*b"\xa9mvn");
pub const MOVEMENT_COUNT: Ident = Ident(*b"\xa9mvc");
pub const MOVEMENT_INDEX: Ident = Ident(*b"\xa9mvi");
pub const WORK: Ident = Ident(*b"\xa9wrk");
pub const SHOW_MOVEMENT: Ident = Ident(*b"shwm");
lazy_static! {
pub static ref FILETYPE_ATOM_T: AtomT = filetype_atom_t();
pub static ref ITEM_LIST_ATOM_T: AtomT = item_list_atom_t();
pub static ref METADATA_ATOM_T: AtomT = metadata_atom_t();
}
#[derive(Clone, Copy, Default, Eq, PartialEq)]
pub struct Ident(pub [u8; 4]);
impl Deref for Ident {
type Target = [u8; 4];
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl fmt::Debug for Ident {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Ident({})", self.0.iter().map(|b| char::from(*b)).collect::<String>())
}
}
impl fmt::Display for Ident {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0.iter().map(|b| char::from(*b)).collect::<String>())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AtomData {
pub ident: Ident,
pub data: Data,
}
impl AtomData {
pub const fn new(ident: Ident, data: Data) -> Self {
Self { ident, data }
}
pub fn try_from_raw(atom: Atom) -> Option<Self> {
match atom.content {
Content::RawData(d) => Some(Self::new(atom.ident, d)),
_ => None,
}
}
pub fn try_from_typed(atom: Atom) -> Option<Self> {
if let Some(d) = atom.content.take_child(DATA) {
if let Content::TypedData(data) = d.content {
return Some(Self::new(atom.ident, data));
}
}
None
}
pub fn into_typed(self) -> Atom {
Atom::new(self.ident, 0, Content::data_atom_with(self.data))
}
pub fn to_typed(&self) -> Atom {
Atom::new(self.ident, 0, Content::data_atom_with(self.data.clone()))
}
pub fn into_raw(self) -> Atom {
Atom::new(self.ident, 0, Content::RawData(self.data))
}
pub fn to_raw(&self) -> Atom {
Atom::new(self.ident, 0, Content::RawData(self.data.clone()))
}
}
#[derive(Clone, Default, Eq, PartialEq)]
pub struct Atom {
pub ident: Ident,
pub offset: usize,
pub content: Content,
}
impl fmt::Debug for Atom {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Atom{{ {}, {}, {:#?} }}", self.ident, self.offset, self.content)
}
}
impl Atom {
pub const fn new(ident: Ident, offset: usize, content: Content) -> Self {
Self { ident, offset, content }
}
pub const fn data_atom_with(data: Data) -> Self {
Self::new(DATA, 0, Content::TypedData(data))
}
pub fn len(&self) -> usize {
8 + self.offset + self.content.len()
}
pub fn is_empty(&self) -> bool {
self.offset + self.content.len() == 0
}
pub fn child(&self, ident: Ident) -> Option<&Self> {
self.content.child(ident)
}
pub fn child_mut(&mut self, ident: Ident) -> Option<&mut Self> {
self.content.child_mut(ident)
}
pub fn mut_first_child(&mut self) -> Option<&mut Self> {
self.content.first_child_mut()
}
pub fn take_child(self, ident: Ident) -> Option<Self> {
self.content.take_child(ident)
}
pub fn take_first_child(self) -> Option<Self> {
self.content.take_first_child()
}
pub fn write_to(&self, writer: &mut impl Write) -> crate::Result<()> {
writer.write_all(&(self.len() as u32).to_be_bytes())?;
writer.write_all(&*self.ident)?;
writer.write_all(&vec![0u8; self.offset])?;
self.content.write_to(writer)?;
Ok(())
}
pub fn check_filetype(&self) -> crate::Result<()> {
match &self.content {
Content::RawData(Data::Utf8(s)) => {
let major_brand = s.split('\u{0}').next().unwrap();
if VALID_FILETYPES.iter().any(|e| e.eq_ignore_ascii_case(major_brand)) {
return Ok(());
}
Err(crate::Error::new(
ErrorKind::InvalidFiletype(s.to_string()),
"Invalid filetype.".to_owned(),
))
}
_ => Err(crate::Error::new(ErrorKind::NoTag, "No filetype atom found.".to_owned())),
}
}
}
#[derive(Clone, Default, Eq, PartialEq)]
pub struct AtomT {
pub ident: Ident,
pub offset: usize,
pub content: ContentT,
}
impl fmt::Debug for AtomT {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Atom{{ {}, {}, {:#?} }}", self.ident, self.offset, self.content)
}
}
impl AtomT {
pub const fn new(ident: Ident, offset: usize, content: ContentT) -> Self {
Self { ident, offset, content }
}
pub const fn data_atom() -> Self {
Self::new(DATA, 0, ContentT::TypedData)
}
pub fn child(&self, ident: Ident) -> Option<&Self> {
self.content.child(ident)
}
pub fn first_child(&self) -> Option<&Self> {
self.content.first_child()
}
pub fn child_mut(&mut self, ident: Ident) -> Option<&mut Self> {
self.content.child_mut(ident)
}
pub fn first_child_mut(&mut self) -> Option<&mut Self> {
self.content.first_child_mut()
}
pub fn take_child(self, ident: Ident) -> Option<Self> {
self.content.take_child(ident)
}
pub fn take_first_child(self) -> Option<Self> {
self.content.take_first_child()
}
pub fn parse_next(&self, reader: &mut (impl Read + Seek)) -> crate::Result<Atom> {
let (length, ident) = match parse_head(reader) {
Ok(h) => h,
Err(e) => return Err(e),
};
if ident == self.ident {
match self.parse_content(reader, length) {
Ok(c) => Ok(Atom::new(self.ident, self.offset, c)),
Err(e) => Err(crate::Error::new(
e.kind,
format!("Error reading {}: {}", ident, e.description),
)),
}
} else {
Err(crate::Error::new(
ErrorKind::AtomNotFound(self.ident),
format!("Expected {} found {}", self.ident, ident),
))
}
}
pub fn parse(&self, reader: &mut (impl Read + Seek)) -> crate::Result<Atom> {
let current_position = reader.seek(SeekFrom::Current(0))?;
let complete_length = reader.seek(SeekFrom::End(0))?;
let length = (complete_length - current_position) as usize;
reader.seek(SeekFrom::Start(current_position))?;
let mut parsed_bytes = 0;
while parsed_bytes < length {
let (atom_length, atom_ident) = parse_head(reader)?;
if atom_ident == self.ident {
return match self.parse_content(reader, atom_length) {
Ok(c) => Ok(Atom::new(self.ident, self.offset, c)),
Err(e) => Err(crate::Error::new(
e.kind,
format!("Error reading {}: {}", atom_ident, e.description),
)),
};
} else {
reader.seek(SeekFrom::Current((atom_length - 8) as i64))?;
}
parsed_bytes += atom_length;
}
Err(crate::Error::new(
ErrorKind::AtomNotFound(self.ident),
format!("No {} atom found", self.ident),
))
}
pub fn parse_content(
&self,
reader: &mut (impl Read + Seek),
length: usize,
) -> crate::Result<Content> {
if length > 8 {
if self.offset != 0 {
reader.seek(SeekFrom::Current(self.offset as i64))?;
}
self.content.parse(reader, length - 8 - self.offset)
} else {
Ok(Content::Empty)
}
}
}
pub fn read_tag_from(reader: &mut (impl Read + Seek)) -> crate::Result<Tag> {
let mut tag_atoms = None;
let mut mdhd_data = None;
let ftyp = FILETYPE_ATOM_T.parse_next(reader)?;
ftyp.check_filetype()?;
let ftyp_data = match ftyp.content {
Content::RawData(Data::Utf8(s)) => Some(s),
_ => None,
};
let moov = METADATA_ATOM_T.parse(reader)?;
for a in moov.content.into_iter() {
match a.ident {
TRACK => {
if let Some(mdia) = a.take_child(MEDIA) {
if let Some(mdhd) = mdia.take_child(MEDIA_HEADER) {
if let Content::RawData(Data::Reserved(v)) = mdhd.content {
mdhd_data = Some(v);
}
}
}
}
USER_DATA => {
if let Some(meta) = a.take_child(METADATA) {
if let Some(ilst) = meta.take_child(ITEM_LIST) {
if let Content::Atoms(atoms) = ilst.content {
tag_atoms = Some(
atoms
.into_iter()
.filter(|a| {
if let Some(d) = a.child(DATA) {
if let Content::TypedData(_) = d.content {
return true;
}
}
false
})
.collect(),
);
}
}
}
}
_ => (),
}
}
let tag_atoms = match tag_atoms {
Some(t) => t,
None => Vec::new(),
};
Ok(Tag::new(ftyp_data, mdhd_data, tag_atoms))
}
pub fn write_tag_to(file: &File, atoms: &[Atom]) -> crate::Result<()> {
let mut reader = BufReader::new(file);
let mut writer = BufWriter::new(file);
let mut atom_pos_and_len = Vec::new();
let mut destination = ITEM_LIST_ATOM_T.deref();
let ftyp = FILETYPE_ATOM_T.parse_next(&mut reader)?;
ftyp.check_filetype()?;
while let Ok((length, ident)) = parse_head(&mut reader) {
if ident == destination.ident {
let pos = reader.seek(SeekFrom::Current(0))? as usize - 8;
atom_pos_and_len.push((pos, length));
reader.seek(SeekFrom::Current(destination.offset as i64))?;
match destination.first_child() {
Some(a) => destination = a,
None => break,
}
} else {
reader.seek(SeekFrom::Current(length as i64 - 8))?;
}
}
let old_file_length = reader.seek(SeekFrom::End(0))?;
let metadata_position = atom_pos_and_len[atom_pos_and_len.len() - 1].0 + 8;
let old_metadata_length = atom_pos_and_len[atom_pos_and_len.len() - 1].1 - 8;
let new_metadata_length = atoms.iter().map(|a| a.len()).sum::<usize>();
let metadata_length_difference = new_metadata_length as i32 - old_metadata_length as i32;
let mut additional_data =
Vec::with_capacity(old_file_length as usize - (metadata_position + old_metadata_length));
reader.seek(SeekFrom::Start((metadata_position + old_metadata_length) as u64))?;
reader.read_to_end(&mut additional_data)?;
file.set_len((old_file_length as i64 + metadata_length_difference as i64) as u64)?;
for (pos, len) in atom_pos_and_len {
writer.seek(SeekFrom::Start(pos as u64))?;
writer.write_all(&((len as i32 + metadata_length_difference) as u32).to_be_bytes())?;
}
writer.seek(SeekFrom::Current(4))?;
for a in atoms {
a.write_to(&mut writer)?;
}
writer.write_all(&additional_data)?;
writer.flush()?;
Ok(())
}
pub fn dump_tag_to(writer: &mut impl Write, atoms: Vec<Atom>) -> crate::Result<()> {
#[rustfmt::skip]
let ftyp = Atom::new( FILETYPE, 0, Content::RawData(
Data::Utf8("M4A \u{0}\u{0}\u{2}\u{0}isomiso2".to_owned())),
);
#[rustfmt::skip]
let moov = Atom::new(MOVIE, 0, Content::atom(
Atom::new( USER_DATA, 0, Content::atom(
Atom::new( METADATA, 4, Content::atom(
Atom::new(ITEM_LIST, 0, Content::Atoms(atoms))
)),
)),
));
ftyp.write_to(writer)?;
moov.write_to(writer)?;
Ok(())
}
pub fn parse_atoms(
atoms: &[AtomT],
reader: &mut (impl Read + Seek),
length: usize,
) -> crate::Result<Vec<Atom>> {
let mut parsed_bytes = 0;
let mut parsed_atoms = Vec::with_capacity(atoms.len());
while parsed_bytes < length {
let (atom_length, atom_ident) = parse_head(reader)?;
let mut parsed = false;
for a in atoms {
if atom_ident == a.ident {
match a.parse_content(reader, atom_length) {
Ok(c) => {
parsed_atoms.push(Atom::new(a.ident, a.offset, c));
parsed = true;
}
Err(e) => {
return Err(crate::Error::new(
e.kind,
format!("Error reading {}: {}", atom_ident, e.description),
));
}
}
break;
}
}
if atom_length > 8 && !parsed {
reader.seek(SeekFrom::Current((atom_length - 8) as i64))?;
}
parsed_bytes += atom_length;
}
Ok(parsed_atoms)
}
pub fn parse_head(reader: &mut (impl Read + Seek)) -> crate::Result<(usize, Ident)> {
let length = match data::read_u32(reader) {
Ok(l) => l as usize,
Err(e) => {
return Err(crate::Error::new(e.kind, "Error reading atom length".to_owned()));
}
};
let mut ident = [0u8; 4];
if let Err(e) = reader.read_exact(&mut ident) {
return Err(crate::Error::new(
ErrorKind::Io(e),
"Error reading atom identifier".to_owned(),
));
}
Ok((length, Ident(ident)))
}
fn filetype_atom_t() -> AtomT {
AtomT::new(FILETYPE, 0, ContentT::RawData(DataT::new(data::UTF8)))
}
#[rustfmt::skip]
fn metadata_atom_t() -> AtomT {
AtomT::new( MOVIE, 0, ContentT::Atoms(vec![
AtomT::new( TRACK, 0, ContentT::atom_t(
AtomT::new( MEDIA, 0, ContentT::atom_t(
AtomT::new( MEDIA_HEADER, 0, ContentT::RawData(
DataT::new(data::RESERVED)
)),
)),
)),
AtomT::new(USER_DATA, 0, ContentT::atom_t(
AtomT::new(METADATA, 4, ContentT::atom_t(
AtomT::new(ITEM_LIST, 0, ContentT::Atoms(vec![
AtomT::new(ADVISORY_RATING, 0, ContentT::data_atom_t()),
AtomT::new(ALBUM, 0, ContentT::data_atom_t()),
AtomT::new(ALBUM_ARTIST, 0, ContentT::data_atom_t()),
AtomT::new(ARTIST, 0, ContentT::data_atom_t()),
AtomT::new(BPM, 0, ContentT::data_atom_t()),
AtomT::new(CATEGORY, 0, ContentT::data_atom_t()),
AtomT::new(COMMENT, 0, ContentT::data_atom_t()),
AtomT::new(COMPILATION, 0, ContentT::data_atom_t()),
AtomT::new(COMPOSER, 0, ContentT::data_atom_t()),
AtomT::new(COPYRIGHT, 0, ContentT::data_atom_t()),
AtomT::new(CUSTOM_GENRE, 0, ContentT::data_atom_t()),
AtomT::new(DESCRIPTION, 0, ContentT::data_atom_t()),
AtomT::new(DISC_NUMBER, 0, ContentT::data_atom_t()),
AtomT::new(ENCODER, 0, ContentT::data_atom_t()),
AtomT::new(GAPLESS_PLAYBACK, 0, ContentT::data_atom_t()),
AtomT::new(GROUPING, 0, ContentT::data_atom_t()),
AtomT::new(KEYWORD, 0, ContentT::data_atom_t()),
AtomT::new(LYRICS, 0, ContentT::data_atom_t()),
AtomT::new(MEDIA_TYPE, 0, ContentT::data_atom_t()),
AtomT::new(MOVEMENT_COUNT, 0, ContentT::data_atom_t()),
AtomT::new(MOVEMENT_INDEX, 0, ContentT::data_atom_t()),
AtomT::new(MOVEMENT, 0, ContentT::data_atom_t()),
AtomT::new(PODCAST, 0, ContentT::data_atom_t()),
AtomT::new(PODCAST_EPISODE_GLOBAL_UNIQUE_ID, 0, ContentT::data_atom_t()),
AtomT::new(PODCAST_URL, 0, ContentT::data_atom_t()),
AtomT::new(PURCHASE_DATE, 0, ContentT::data_atom_t()),
AtomT::new(SHOW_MOVEMENT, 0, ContentT::data_atom_t()),
AtomT::new(STANDARD_GENRE, 0, ContentT::data_atom_t()),
AtomT::new(TITLE, 0, ContentT::data_atom_t()),
AtomT::new(TRACK_NUMBER, 0, ContentT::data_atom_t()),
AtomT::new(TV_EPISODE, 0, ContentT::data_atom_t()),
AtomT::new(TV_EPISODE_NUMBER, 0, ContentT::data_atom_t()),
AtomT::new(TV_NETWORK_NAME, 0, ContentT::data_atom_t()),
AtomT::new(TV_SEASON, 0, ContentT::data_atom_t()),
AtomT::new(TV_SHOW_NAME, 0, ContentT::data_atom_t()),
AtomT::new(WORK, 0, ContentT::data_atom_t()),
AtomT::new(YEAR, 0, ContentT::data_atom_t()),
AtomT::new(ARTWORK, 0, ContentT::data_atom_t()),
])),
)),
)),
]))
}
#[rustfmt::skip]
fn item_list_atom_t() -> AtomT {
AtomT::new(MOVIE, 0, ContentT::atom_t(
AtomT::new(USER_DATA, 0, ContentT::atom_t(
AtomT::new(METADATA, 4, ContentT::atom_t(
AtomT::new(ITEM_LIST, 0, ContentT::atoms_t())
))
))
))
}