use crate::config::{ParseOptions, WriteOptions};
use crate::error::TooMuchDataError;
use crate::id3::v2::error::{FrameEncodingError, FrameParseError};
use crate::id3::v2::frame::list::FrameList;
use crate::id3::v2::read::read_all_frames_into_list;
use crate::id3::v2::tag::TITLE_ID;
use crate::id3::v2::{Frame, FrameFlags, FrameHeader, FrameId, Id3v2TagFlags, Id3v2Version};
use crate::util::alloc::VecFallibleCapacity;
use crate::util::text::{TextDecodeOptions, TextEncoding, decode_text};
use std::borrow::Cow;
use std::hash::Hash;
use std::io::{Cursor, Read, Write};
use byteorder::{ReadBytesExt, WriteBytesExt};
const FRAME_ID: FrameId<'static> = FrameId::Valid(Cow::Borrowed("CTOC"));
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
pub struct CtocFlags {
pub top_level: bool,
pub ordered: bool,
}
impl From<u8> for CtocFlags {
fn from(val: u8) -> Self {
CtocFlags {
top_level: val & 0b10 == 0b10,
ordered: val & 0b1 == 0b1,
}
}
}
impl From<CtocFlags> for u8 {
fn from(flags: CtocFlags) -> u8 {
let mut ret = 0;
if flags.ordered {
ret |= 0b1;
}
if flags.top_level {
ret |= 0b10;
}
ret
}
}
#[derive(Clone, Debug, Eq)]
pub struct ChapterTableOfContentsFrame<'a> {
pub(crate) header: FrameHeader<'a>,
pub id: Cow<'a, str>,
pub flags: CtocFlags,
pub entries: Cow<'a, [Cow<'a, str>]>,
pub children: FrameList<'a>,
}
impl PartialEq for ChapterTableOfContentsFrame<'_> {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Hash for ChapterTableOfContentsFrame<'_> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
impl<'a> ChapterTableOfContentsFrame<'a> {
pub fn new(
id: impl Into<Cow<'a, str>>,
flags: CtocFlags,
entries: impl Into<Cow<'a, [Cow<'a, str>]>>,
children: FrameList<'a>,
) -> Self {
let header = FrameHeader::new(FRAME_ID, FrameFlags::default());
Self {
header,
id: id.into(),
flags,
entries: entries.into(),
children,
}
}
pub fn id(&self) -> FrameId<'_> {
FRAME_ID
}
pub fn flags(&self) -> FrameFlags {
self.header.flags
}
pub fn set_flags(&mut self, flags: FrameFlags) {
self.header.flags = flags;
}
pub fn toc_flags(&self) -> CtocFlags {
self.flags
}
pub fn set_toc_flags(&mut self, flags: CtocFlags) {
self.flags = flags;
}
pub fn title(&self) -> Option<&str> {
self.children.get_text(&TITLE_ID)
}
pub fn subtitle(&self) -> Option<&str> {
self.children
.get_text(&FrameId::Valid(Cow::Borrowed("TIT3")))
}
pub fn as_bytes(
&self,
version: Id3v2Version,
write_options: WriteOptions,
) -> Result<Vec<u8>, FrameEncodingError> {
if version == Id3v2Version::V2 {
return Err(FrameEncodingError::unsupported_version(version));
}
let Self {
header: _,
id,
flags,
entries,
children,
} = self;
if entries.len() > u8::MAX as usize {
return Err(TooMuchDataError.into());
}
let mut content = Cursor::new(Vec::try_with_capacity_stable(id.len() + 16)?);
content.write_all(&TextEncoding::Latin1.encode(
id,
true,
write_options.lossy_text_encoding,
)?)?;
content.write_u8((*flags).into())?;
content.write_u8(entries.len() as u8)?;
for entry in &**entries {
let entry =
TextEncoding::Latin1.encode(entry, true, write_options.lossy_text_encoding)?;
content.write_all(&entry)?;
}
match version {
Id3v2Version::V4 => crate::id3::v2::write::frame::create_items(
&mut content,
&mut children.iter().map(Frame::borrow),
write_options,
)?,
Id3v2Version::V3 => crate::id3::v2::write::frame::create_items_v3(
&mut content,
&mut children.iter().map(Frame::borrow),
write_options,
)?,
Id3v2Version::V2 => unreachable!(),
}
Ok(content.into_inner())
}
}
impl ChapterTableOfContentsFrame<'static> {
pub fn parse<R>(
reader: &mut R,
parse_options: ParseOptions,
version: Id3v2Version,
frame_flags: FrameFlags,
) -> Result<Self, FrameParseError>
where
R: Read,
{
if version == Id3v2Version::V2 {
return Err(FrameParseError::unsupported_version(version));
}
let id = decode_text(
reader,
TextDecodeOptions::new()
.encoding(TextEncoding::Latin1)
.terminated(true),
)?;
let flags = CtocFlags::from(reader.read_u8()?);
let entry_count = reader.read_u8()?;
let mut entries = Vec::with_capacity(entry_count as usize);
for _ in 0..entry_count {
let entry = decode_text(
reader,
TextDecodeOptions::new()
.encoding(TextEncoding::Latin1)
.terminated(true),
)?;
entries.push(Cow::Owned(entry.content));
}
let children =
read_all_frames_into_list(reader, version, Id3v2TagFlags::default(), parse_options)?;
let header = FrameHeader::new(FRAME_ID, frame_flags);
Ok(ChapterTableOfContentsFrame {
header,
id: Cow::Owned(id.content),
flags,
entries: Cow::Owned(entries),
children,
})
}
}
impl ChapterTableOfContentsFrame<'_> {
pub(crate) fn borrow(&self) -> ChapterTableOfContentsFrame<'_> {
ChapterTableOfContentsFrame {
header: self.header.borrow(),
id: Cow::Borrowed(&self.id),
flags: self.flags,
entries: Cow::Borrowed(&self.entries),
children: self.children.borrow(),
}
}
}
#[cfg(test)]
mod tests {
use crate::TextEncoding;
use crate::config::{ParseOptions, WriteOptions};
use crate::id3::v2::items::table_of_contents_frame::{ChapterTableOfContentsFrame, CtocFlags};
use crate::id3::v2::{
Frame, FrameFlags, FrameId, FrameList, Id3v2Tag, Id3v2Version, TextInformationFrame,
};
use std::borrow::Cow;
fn expected() -> ChapterTableOfContentsFrame<'static> {
let mut children = FrameList::new();
children.push(Frame::Text(TextInformationFrame::new(
FrameId::Valid(Cow::Borrowed("TIT2")),
TextEncoding::Latin1,
"TOC 1 - The Beginning",
)));
ChapterTableOfContentsFrame::new(
"TOC1",
CtocFlags {
top_level: true,
ordered: true,
},
Cow::Owned(vec!["CH1".into(), "CH2".into(), "CH3".into()]),
children,
)
}
#[test_log::test]
fn ctoc_decode() {
let cont = crate::tag::utils::test_utils::read_path("tests/tags/assets/id3v2/test.ctoc");
let parsed_ctoc = ChapterTableOfContentsFrame::parse(
&mut &cont[..],
ParseOptions::default(),
Id3v2Version::V4,
FrameFlags::default(),
)
.unwrap();
let expected = expected();
let ChapterTableOfContentsFrame {
header,
id,
flags,
entries,
children,
} = parsed_ctoc;
assert_eq!(header, expected.header);
assert_eq!(id, expected.id);
assert_eq!(flags, expected.flags);
assert_eq!(entries, expected.entries);
assert_eq!(children, expected.children);
}
#[test_log::test]
fn ctoc_encode() {
let encoded = expected()
.as_bytes(Id3v2Version::V4, WriteOptions::default())
.unwrap();
let expected_bytes =
crate::tag::utils::test_utils::read_path("tests/tags/assets/id3v2/test.ctoc");
assert_eq!(encoded, expected_bytes);
}
#[test_log::test]
fn ctoc_many_roots() {
let mut tag = Id3v2Tag::new();
tag.insert(Frame::TableOfContents(expected()));
for i in 1..=3 {
let mut toc = expected();
toc.id = Cow::Owned(format!("TOC{}", i + 1));
tag.insert(Frame::TableOfContents(toc));
}
let parsed_tag =
crate::id3::v2::tag::tests::dump_and_re_read(&tag, WriteOptions::default());
let toc_frames = parsed_tag
.iter()
.filter_map(|f| match f {
Frame::TableOfContents(toc) => Some(toc),
_ => None,
})
.collect::<Vec<_>>();
assert_eq!(toc_frames.len(), 1);
assert_eq!(toc_frames[0].id, "TOC1");
}
}