use crate::config::WriteOptions;
use crate::error::TooMuchDataError;
use crate::id3::v2::error::FrameParseError;
use crate::id3::v2::frame::error::FrameEncodingError;
use crate::id3::v2::{FrameFlags, FrameHeader, FrameId};
use crate::util::text::{
DecodeTextResult, TextDecodeOptions, TextEncoding, decode_text,
utf16_decode_terminated_maybe_bom,
};
use std::borrow::Cow;
use std::io::{Cursor, Seek, Write};
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
const FRAME_ID: FrameId<'static> = FrameId::Valid(Cow::Borrowed("SYLT"));
#[derive(Copy, Clone, PartialEq, Debug, Eq, Hash)]
#[repr(u8)]
pub enum TimestampFormat {
MPEG = 1,
MS = 2,
}
#[derive(Debug)]
pub struct BadTimestampFormatError;
impl core::fmt::Display for BadTimestampFormatError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("encountered an invalid timestamp format in a synchronized frame")
}
}
impl core::error::Error for BadTimestampFormatError {}
impl From<BadTimestampFormatError> for FrameParseError {
fn from(input: BadTimestampFormatError) -> Self {
FrameParseError::new(None, Box::new(input))
}
}
impl TryFrom<u8> for TimestampFormat {
type Error = BadTimestampFormatError;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
1 => Ok(TimestampFormat::MPEG),
2 => Ok(TimestampFormat::MS),
_ => Err(BadTimestampFormatError),
}
}
}
#[derive(Copy, Clone, PartialEq, Debug, Eq, Hash)]
#[repr(u8)]
pub enum SyncTextContentType {
Other = 0,
Lyrics = 1,
TextTranscription = 2,
PartName = 3,
Events = 4,
Chord = 5,
Trivia = 6,
WebpageURL = 7,
ImageURL = 8,
}
#[derive(Debug)]
pub struct BadSyncTextContentTypeError;
impl core::fmt::Display for BadSyncTextContentTypeError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("encountered an invalid synchronized text content type")
}
}
impl core::error::Error for BadSyncTextContentTypeError {}
impl From<BadSyncTextContentTypeError> for FrameParseError {
fn from(input: BadSyncTextContentTypeError) -> Self {
FrameParseError::new(None, Box::new(input))
}
}
impl TryFrom<u8> for SyncTextContentType {
type Error = BadSyncTextContentTypeError;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0 => Ok(Self::Other),
1 => Ok(Self::Lyrics),
2 => Ok(Self::TextTranscription),
3 => Ok(Self::PartName),
4 => Ok(Self::Events),
5 => Ok(Self::Chord),
6 => Ok(Self::Trivia),
7 => Ok(Self::WebpageURL),
8 => Ok(Self::ImageURL),
_ => Err(BadSyncTextContentTypeError),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct SynchronizedTextFrame<'a> {
pub(crate) header: FrameHeader<'a>,
pub encoding: TextEncoding,
pub language: [u8; 3],
pub timestamp_format: TimestampFormat,
pub content_type: SyncTextContentType,
pub description: Option<String>,
pub content: Vec<(u32, String)>,
}
impl SynchronizedTextFrame<'_> {
pub fn new(
encoding: TextEncoding,
language: [u8; 3],
timestamp_format: TimestampFormat,
content_type: SyncTextContentType,
description: Option<String>,
content: Vec<(u32, String)>,
) -> Self {
let header = FrameHeader::new(FRAME_ID, FrameFlags::default());
Self {
header,
encoding,
language,
timestamp_format,
content_type,
description,
content,
}
}
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;
}
#[allow(clippy::missing_panics_doc)] pub fn parse(data: &[u8], frame_flags: FrameFlags) -> Result<Self, FrameParseError> {
fn parse_inner<'a>(
data: &[u8],
frame_flags: FrameFlags,
) -> Result<SynchronizedTextFrame<'a>, FrameParseError> {
if data.len() < 7 {
return Err(FrameParseError::undersized(FRAME_ID));
}
let encoding = TextEncoding::try_from(data[0])?;
let language: [u8; 3] = data[1..4].try_into().unwrap();
if language.iter().any(|c| !c.is_ascii_alphabetic()) {
return Err(FrameParseError::invalid_language(language));
}
let timestamp_format = TimestampFormat::try_from(data[4])?;
let content_type = SyncTextContentType::try_from(data[5])?;
let mut cursor = Cursor::new(&data[6..]);
let DecodeTextResult {
content: description,
bom,
..
} = decode_text(
&mut cursor,
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 mut pos = 0;
let total = (data.len() - 6) as u64 - cursor.stream_position()?;
let mut content = Vec::new();
while pos < total {
let text;
if let Some(endianness) = endianness {
let (decoded, bytes_read) =
utf16_decode_terminated_maybe_bom(&mut cursor, endianness)?;
pos += bytes_read as u64;
text = decoded;
} else {
let decoded_text = decode_text(
&mut cursor,
TextDecodeOptions::new().encoding(encoding).terminated(true),
)?;
pos += decoded_text.bytes_read as u64;
text = decoded_text.content;
}
let time = cursor.read_u32::<BigEndian>()?;
pos += 4;
content.push((time, text));
}
let header = FrameHeader::new(FRAME_ID, frame_flags);
Ok(SynchronizedTextFrame {
header,
encoding,
language,
timestamp_format,
content_type,
description: if description.is_empty() {
None
} else {
Some(description)
},
content,
})
}
parse_inner(data, frame_flags).map_err(|mut e| {
e.set_id(FRAME_ID);
e
})
}
pub fn as_bytes(&self, write_options: WriteOptions) -> Result<Vec<u8>, FrameEncodingError> {
if !self.language.iter().all(u8::is_ascii_alphabetic) {
return Err(FrameEncodingError::invalid_language(self.language));
}
let mut encoding = self.encoding;
if write_options.use_id3v23 {
encoding = encoding.to_id3v23();
}
let mut data = vec![self.encoding as u8];
data.write_all(&self.language)?;
data.write_u8(self.timestamp_format as u8)?;
data.write_u8(self.content_type as u8)?;
data.write_all(&encoding.encode(
self.description.as_deref().unwrap_or(""),
true,
write_options.lossy_text_encoding,
)?)?;
for (time, text) in &self.content {
data.write_all(&encoding.encode(text, true, write_options.lossy_text_encoding)?)?;
data.write_u32::<BigEndian>(*time)?;
}
if data.len() as u64 > u64::from(u32::MAX) {
return Err(TooMuchDataError.into());
}
Ok(data)
}
}
#[cfg(test)]
mod tests {
use crate::config::WriteOptions;
use crate::id3::v2::{
FrameFlags, FrameHeader, SyncTextContentType, SynchronizedTextFrame, TimestampFormat,
};
use crate::util::text::TextEncoding;
fn expected(encoding: TextEncoding) -> SynchronizedTextFrame<'static> {
SynchronizedTextFrame {
header: FrameHeader::new(super::FRAME_ID, FrameFlags::default()),
encoding,
language: *b"eng",
timestamp_format: TimestampFormat::MS,
content_type: SyncTextContentType::Lyrics,
description: Some(String::from("Test Sync Text")),
content: vec![
(0, String::from("\nLofty")),
(10000, String::from("\nIs")),
(15000, String::from("\nReading")),
(30000, String::from("\nThis")),
(1_938_000, String::from("\nCorrectly")),
],
}
}
#[test_log::test]
fn sylt_decode() {
let cont = crate::tag::utils::test_utils::read_path("tests/tags/assets/id3v2/test.sylt");
let parsed_sylt = SynchronizedTextFrame::parse(&cont, FrameFlags::default()).unwrap();
assert_eq!(parsed_sylt, expected(TextEncoding::Latin1));
}
#[test_log::test]
fn sylt_encode() {
let encoded = expected(TextEncoding::Latin1)
.as_bytes(WriteOptions::default())
.unwrap();
let expected_bytes =
crate::tag::utils::test_utils::read_path("tests/tags/assets/id3v2/test.sylt");
assert_eq!(encoded, expected_bytes);
}
#[test_log::test]
fn sylt_decode_utf16() {
let cont =
crate::tag::utils::test_utils::read_path("tests/tags/assets/id3v2/test_utf16.sylt");
let parsed_sylt = SynchronizedTextFrame::parse(&cont, FrameFlags::default()).unwrap();
assert_eq!(parsed_sylt, expected(TextEncoding::UTF16));
}
#[test_log::test]
fn sylt_encode_utf_16() {
let encoded = expected(TextEncoding::UTF16)
.as_bytes(WriteOptions::default())
.unwrap();
let expected_bytes =
crate::tag::utils::test_utils::read_path("tests/tags/assets/id3v2/test_utf16.sylt");
assert_eq!(encoded, expected_bytes);
}
}