use crate::config::WriteOptions;
use crate::error::{Id3v2Error, Id3v2ErrorKind, Result};
use crate::id3::v2::frame::content::verify_encoding;
use crate::id3::v2::header::Id3v2Version;
use crate::id3::v2::{FrameFlags, FrameHeader, FrameId};
use crate::tag::items::Lang;
use crate::util::text::{
DecodeTextResult, TextDecodeOptions, TextEncoding, decode_text,
utf16_decode_terminated_maybe_bom,
};
use std::borrow::Cow;
use std::hash::{Hash, Hasher};
use std::io::Read;
use byteorder::ReadBytesExt;
struct LanguageFrame {
pub encoding: TextEncoding,
pub language: Lang,
pub description: String,
pub content: String,
}
impl LanguageFrame {
fn parse<R>(reader: &mut R, version: Id3v2Version) -> Result<Option<Self>>
where
R: Read,
{
let Ok(encoding_byte) = reader.read_u8() else {
return Ok(None);
};
let encoding = verify_encoding(encoding_byte, version)?;
let mut language = [0; 3];
reader.read_exact(&mut language)?;
let DecodeTextResult {
content: description,
bom,
..
} = decode_text(
reader,
TextDecodeOptions::new().encoding(encoding).terminated(true),
)?;
let endianness: Option<fn([u8; 2]) -> u16> = if encoding == TextEncoding::UTF16 {
match bom {
[0xFF, 0xFE] => Some(u16::from_le_bytes),
[0xFE, 0xFF] => Some(u16::from_be_bytes),
_ => None,
}
} else {
None
};
let content;
if let Some(endianness) = endianness {
(content, _) = utf16_decode_terminated_maybe_bom(reader, endianness)?;
} else {
content = decode_text(reader, TextDecodeOptions::new().encoding(encoding))?.content;
}
Ok(Some(Self {
encoding,
language,
description,
content,
}))
}
fn create_bytes(
mut encoding: TextEncoding,
language: [u8; 3],
description: &str,
content: &str,
write_options: WriteOptions,
) -> Result<Vec<u8>> {
if write_options.use_id3v23 {
encoding = encoding.to_id3v23();
}
let mut bytes = vec![encoding as u8];
if language.len() != 3 || language.iter().any(|c| !c.is_ascii_alphabetic()) {
return Err(Id3v2Error::new(Id3v2ErrorKind::InvalidLanguage(language)).into());
}
bytes.extend(language);
bytes.extend(
encoding
.encode(description, true, write_options.lossy_text_encoding)?
.iter(),
);
bytes.extend(encoding.encode(content, false, write_options.lossy_text_encoding)?);
Ok(bytes)
}
}
#[derive(Clone, Debug, Eq)]
pub struct CommentFrame<'a> {
pub(crate) header: FrameHeader<'a>,
pub encoding: TextEncoding,
pub language: Lang,
pub description: Cow<'a, str>,
pub content: Cow<'a, str>,
}
impl PartialEq for CommentFrame<'_> {
fn eq(&self, other: &Self) -> bool {
self.language == other.language && self.description == other.description
}
}
impl Hash for CommentFrame<'_> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.language.hash(state);
self.description.hash(state);
}
}
impl<'a> CommentFrame<'a> {
const FRAME_ID: FrameId<'static> = FrameId::Valid(Cow::Borrowed("COMM"));
pub fn new(
encoding: TextEncoding,
language: Lang,
description: impl Into<Cow<'a, str>>,
content: impl Into<Cow<'a, str>>,
) -> Self {
let header = FrameHeader::new(Self::FRAME_ID, FrameFlags::default());
Self {
header,
encoding,
language,
description: description.into(),
content: content.into(),
}
}
pub fn id(&self) -> FrameId<'_> {
Self::FRAME_ID
}
pub fn flags(&self) -> FrameFlags {
self.header.flags
}
pub fn set_flags(&mut self, flags: FrameFlags) {
self.header.flags = flags;
}
pub fn parse<R>(
reader: &mut R,
frame_flags: FrameFlags,
version: Id3v2Version,
) -> Result<Option<Self>>
where
R: Read,
{
let Some(language_frame) = LanguageFrame::parse(reader, version)? else {
return Ok(None);
};
let header = FrameHeader::new(Self::FRAME_ID, frame_flags);
Ok(Some(Self {
header,
encoding: language_frame.encoding,
language: language_frame.language,
description: Cow::Owned(language_frame.description),
content: Cow::Owned(language_frame.content),
}))
}
pub fn as_bytes(&self, write_options: WriteOptions) -> Result<Vec<u8>> {
LanguageFrame::create_bytes(
self.encoding,
self.language,
&self.description,
&self.content,
write_options,
)
}
}
impl CommentFrame<'static> {
pub(crate) fn downgrade(&self) -> CommentFrame<'_> {
CommentFrame {
header: self.header.downgrade(),
encoding: self.encoding,
language: self.language,
description: Cow::Borrowed(&self.description),
content: Cow::Borrowed(&self.content),
}
}
}
#[derive(Clone, Debug, Eq)]
pub struct UnsynchronizedTextFrame<'a> {
pub(crate) header: FrameHeader<'a>,
pub encoding: TextEncoding,
pub language: Lang,
pub description: Cow<'a, str>,
pub content: Cow<'a, str>,
}
impl PartialEq for UnsynchronizedTextFrame<'_> {
fn eq(&self, other: &Self) -> bool {
self.language == other.language && self.description == other.description
}
}
impl Hash for UnsynchronizedTextFrame<'_> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.language.hash(state);
self.description.hash(state);
}
}
impl<'a> UnsynchronizedTextFrame<'a> {
const FRAME_ID: FrameId<'static> = FrameId::Valid(Cow::Borrowed("USLT"));
pub fn new(
encoding: TextEncoding,
language: Lang,
description: impl Into<Cow<'a, str>>,
content: impl Into<Cow<'a, str>>,
) -> Self {
let header = FrameHeader::new(Self::FRAME_ID, FrameFlags::default());
Self {
header,
encoding,
language,
description: description.into(),
content: content.into(),
}
}
pub fn id(&self) -> FrameId<'_> {
Self::FRAME_ID
}
pub fn flags(&self) -> FrameFlags {
self.header.flags
}
pub fn set_flags(&mut self, flags: FrameFlags) {
self.header.flags = flags;
}
pub fn parse<R>(
reader: &mut R,
frame_flags: FrameFlags,
version: Id3v2Version,
) -> Result<Option<Self>>
where
R: Read,
{
let Some(language_frame) = LanguageFrame::parse(reader, version)? else {
return Ok(None);
};
let header = FrameHeader::new(Self::FRAME_ID, frame_flags);
Ok(Some(Self {
header,
encoding: language_frame.encoding,
language: language_frame.language,
description: Cow::Owned(language_frame.description),
content: Cow::Owned(language_frame.content),
}))
}
pub fn as_bytes(&self, write_options: WriteOptions) -> Result<Vec<u8>> {
LanguageFrame::create_bytes(
self.encoding,
self.language,
&self.description,
&self.content,
write_options,
)
}
}
impl UnsynchronizedTextFrame<'static> {
pub(crate) fn downgrade(&self) -> UnsynchronizedTextFrame<'_> {
UnsynchronizedTextFrame {
header: self.header.downgrade(),
encoding: self.encoding,
language: self.language,
description: Cow::Borrowed(&self.description),
content: Cow::Borrowed(&self.content),
}
}
}