use super::AiffFile;
use super::properties::AiffProperties;
use super::tag::{AiffTextChunks, Comment};
use crate::config::ParseOptions;
use crate::error::{NotEnoughDataError, TagParseError, UnknownFormatError};
use crate::id3::v2::tag::Id3v2Tag;
use crate::iff::aiff::error::AiffParseError;
use crate::iff::aiff::tag::error::AiffTextChunksParseError;
use crate::iff::chunk::{Chunk, Chunks};
use std::io::{Read, Seek, SeekFrom};
use byteorder::{BigEndian, ReadBytesExt};
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub(in crate::iff) enum CompressionPresent {
Yes,
No,
}
pub(in crate::iff) fn verify_aiff<R>(data: &mut R) -> Result<CompressionPresent, AiffParseError>
where
R: Read + Seek,
{
let mut id = [0; 12];
data.read_exact(&mut id)?;
if &id[..4] != b"FORM" {
return Err(UnknownFormatError.into());
}
let compression_present;
match &id[8..] {
b"AIFF" => compression_present = CompressionPresent::No,
b"AIFC" => compression_present = CompressionPresent::Yes,
_ => return Err(UnknownFormatError.into()),
}
log::debug!(
"File verified to be AIFF, compression present: {:?}",
compression_present
);
Ok(compression_present)
}
pub(crate) fn read_from<R>(
data: &mut R,
parse_options: ParseOptions,
) -> Result<AiffFile, AiffParseError>
where
R: Read + Seek,
{
let compression_present = verify_aiff(data)?;
let current_pos = data.stream_position()?;
let file_len = data.seek(SeekFrom::End(0))?.saturating_sub(12);
data.seek(SeekFrom::Start(current_pos))?;
let mut comm = None;
let mut stream_len = 0;
let mut text_chunks = AiffTextChunks::default();
let mut annotations = Vec::new();
let mut comments = Vec::new();
let mut id3v2_tag: Option<Id3v2Tag> = None;
let mut chunks = Chunks::<_, BigEndian>::new(data, file_len);
while let Some(mut chunk) = chunks.next(parse_options.parsing_mode)? {
match &chunk.fourcc {
b"ID3 " | b"id3 " if parse_options.read_tags => {
let Some(tag) = chunk
.id3_chunk(parse_options)
.map_err(TagParseError::from)?
else {
continue;
};
if let Some(existing_tag) = id3v2_tag.as_mut() {
log::warn!("Duplicate ID3v2 tag found, appending frames to previous tag");
for frame in tag.frames {
existing_tag.insert(frame);
}
continue;
}
id3v2_tag = Some(tag);
},
b"COMM" if parse_options.read_properties && comm.is_none() => {
if chunk.size() < 18 {
return Err(NotEnoughDataError::new(Some(18)).into());
}
comm = Some(chunk.content()?);
},
b"SSND" if parse_options.read_properties => {
stream_len = chunk.size();
},
b"ANNO" if parse_options.read_tags => {
annotations.push(chunk.read_string(None)?);
},
b"COMT" if comments.is_empty() && parse_options.read_tags => {
if chunk.size() < 2 {
continue;
}
fn parse_comments<R>(
chunk: &mut Chunk<'_, R>,
comments: &mut Vec<Comment>,
) -> Result<(), AiffTextChunksParseError>
where
R: Read + Seek,
{
let num_comments = chunk.read_u16::<BigEndian>()?;
for _ in 0..num_comments {
let timestamp = chunk.read_u32::<BigEndian>()?;
let marker_id = chunk.read_u16::<BigEndian>()?;
let size = chunk.read_u16::<BigEndian>()?;
let text = chunk.read_string(Some(u32::from(size)))?;
comments.push(Comment {
timestamp,
marker_id,
text,
})
}
Ok(())
}
parse_comments(&mut chunk, &mut comments).map_err(TagParseError::from)?;
},
b"NAME" if text_chunks.name.is_none() && parse_options.read_tags => {
text_chunks.name = Some(
chunk
.read_string(None)
.map_err(AiffTextChunksParseError::from)
.map_err(TagParseError::from)?,
);
},
b"AUTH" if text_chunks.author.is_none() && parse_options.read_tags => {
text_chunks.author = Some(
chunk
.read_string(None)
.map_err(AiffTextChunksParseError::from)
.map_err(TagParseError::from)?,
);
},
b"(c) " if text_chunks.copyright.is_none() && parse_options.read_tags => {
text_chunks.copyright = Some(
chunk
.read_string(None)
.map_err(AiffTextChunksParseError::from)
.map_err(TagParseError::from)?,
);
},
_ => {},
}
}
let data = chunks.into_inner();
if !annotations.is_empty() {
text_chunks.annotations = Some(annotations);
}
if !comments.is_empty() {
text_chunks.comments = Some(comments);
}
let properties;
if parse_options.read_properties {
match comm {
Some(comm) => {
if stream_len == 0 {
return Err(AiffParseError::message(
"file does not contain an \"SSND\" chunk",
));
}
properties = super::properties::read_properties(
&mut &*comm,
compression_present,
stream_len,
data.stream_position()?,
)?;
},
None => {
return Err(AiffParseError::missing_comm());
},
}
} else {
properties = AiffProperties::default();
}
Ok(AiffFile {
properties,
text_chunks_tag: match text_chunks {
AiffTextChunks {
name: None,
author: None,
copyright: None,
annotations: None,
comments: None,
} => None,
_ => Some(text_chunks),
},
id3v2_tag,
})
}