1use crate::config::WriteOptions;
2use crate::error::{ErrorKind, Id3v2Error, Id3v2ErrorKind, LoftyError, Result};
3use crate::id3::v2::{FrameFlags, FrameHeader, FrameId};
4use crate::macros::err;
5use crate::util::text::{
6 DecodeTextResult, TextDecodeOptions, TextEncoding, decode_text,
7 utf16_decode_terminated_maybe_bom,
8};
9
10use std::borrow::Cow;
11use std::io::{Cursor, Seek, Write};
12
13use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
14
15const FRAME_ID: FrameId<'static> = FrameId::Valid(Cow::Borrowed("SYLT"));
16
17#[derive(Copy, Clone, PartialEq, Debug, Eq, Hash)]
19#[repr(u8)]
20pub enum TimestampFormat {
21 MPEG = 1,
23 MS = 2,
25}
26
27impl TimestampFormat {
28 pub fn from_u8(byte: u8) -> Option<Self> {
30 match byte {
31 1 => Some(Self::MPEG),
32 2 => Some(Self::MS),
33 _ => None,
34 }
35 }
36}
37
38#[derive(Copy, Clone, PartialEq, Debug, Eq, Hash)]
40#[repr(u8)]
41#[allow(missing_docs)]
42pub enum SyncTextContentType {
43 Other = 0,
44 Lyrics = 1,
45 TextTranscription = 2,
46 PartName = 3,
47 Events = 4,
48 Chord = 5,
49 Trivia = 6,
50 WebpageURL = 7,
51 ImageURL = 8,
52}
53
54impl SyncTextContentType {
55 pub fn from_u8(byte: u8) -> Option<Self> {
57 match byte {
58 0 => Some(Self::Other),
59 1 => Some(Self::Lyrics),
60 2 => Some(Self::TextTranscription),
61 3 => Some(Self::PartName),
62 4 => Some(Self::Events),
63 5 => Some(Self::Chord),
64 6 => Some(Self::Trivia),
65 7 => Some(Self::WebpageURL),
66 8 => Some(Self::ImageURL),
67 _ => None,
68 }
69 }
70}
71
72#[derive(Clone, Debug, PartialEq, Eq, Hash)]
74pub struct SynchronizedTextFrame<'a> {
75 pub(crate) header: FrameHeader<'a>,
76 pub encoding: TextEncoding,
78 pub language: [u8; 3],
80 pub timestamp_format: TimestampFormat,
82 pub content_type: SyncTextContentType,
84 pub description: Option<String>,
86 pub content: Vec<(u32, String)>,
88}
89
90impl SynchronizedTextFrame<'_> {
91 pub fn new(
93 encoding: TextEncoding,
94 language: [u8; 3],
95 timestamp_format: TimestampFormat,
96 content_type: SyncTextContentType,
97 description: Option<String>,
98 content: Vec<(u32, String)>,
99 ) -> Self {
100 let header = FrameHeader::new(FRAME_ID, FrameFlags::default());
101 Self {
102 header,
103 encoding,
104 language,
105 timestamp_format,
106 content_type,
107 description,
108 content,
109 }
110 }
111
112 pub fn id(&self) -> FrameId<'_> {
114 FRAME_ID
115 }
116
117 pub fn flags(&self) -> FrameFlags {
119 self.header.flags
120 }
121
122 pub fn set_flags(&mut self, flags: FrameFlags) {
124 self.header.flags = flags;
125 }
126
127 #[allow(clippy::missing_panics_doc)] pub fn parse(data: &[u8], frame_flags: FrameFlags) -> Result<Self> {
136 if data.len() < 7 {
137 return Err(Id3v2Error::new(Id3v2ErrorKind::BadFrameLength).into());
138 }
139
140 let encoding = TextEncoding::from_u8(data[0])
141 .ok_or_else(|| LoftyError::new(ErrorKind::TextDecode("Found invalid encoding")))?;
142 let language: [u8; 3] = data[1..4].try_into().unwrap();
143 if language.iter().any(|c| !c.is_ascii_alphabetic()) {
144 return Err(Id3v2Error::new(Id3v2ErrorKind::BadSyncText).into());
145 }
146 let timestamp_format = TimestampFormat::from_u8(data[4])
147 .ok_or_else(|| Id3v2Error::new(Id3v2ErrorKind::BadTimestampFormat))?;
148 let content_type = SyncTextContentType::from_u8(data[5])
149 .ok_or_else(|| Id3v2Error::new(Id3v2ErrorKind::BadSyncText))?;
150
151 let mut cursor = Cursor::new(&data[6..]);
152 let DecodeTextResult {
153 content: description,
154 bom,
155 ..
156 } = decode_text(
157 &mut cursor,
158 TextDecodeOptions::new().encoding(encoding).terminated(true),
159 )
160 .map_err(|_| Id3v2Error::new(Id3v2ErrorKind::BadSyncText))?;
161
162 let endianness: Option<fn([u8; 2]) -> u16> = if encoding == TextEncoding::UTF16 {
171 match bom {
172 [0xFF, 0xFE] => Some(u16::from_le_bytes),
173 [0xFE, 0xFF] => Some(u16::from_be_bytes),
174 _ => None,
175 }
176 } else {
177 None
178 };
179
180 let mut pos = 0;
181 let total = (data.len() - 6) as u64 - cursor.stream_position()?;
182
183 let mut content = Vec::new();
184
185 while pos < total {
186 let text;
187 if let Some(endianness) = endianness {
188 let (decoded, bytes_read) =
189 utf16_decode_terminated_maybe_bom(&mut cursor, endianness)
190 .map_err(|_| Id3v2Error::new(Id3v2ErrorKind::BadSyncText))?;
191 pos += bytes_read as u64;
192 text = decoded;
193 } else {
194 let decoded_text = decode_text(
195 &mut cursor,
196 TextDecodeOptions::new().encoding(encoding).terminated(true),
197 )
198 .map_err(|_| Id3v2Error::new(Id3v2ErrorKind::BadSyncText))?;
199 pos += decoded_text.bytes_read as u64;
200
201 text = decoded_text.content;
202 }
203
204 let time = cursor
205 .read_u32::<BigEndian>()
206 .map_err(|_| Id3v2Error::new(Id3v2ErrorKind::BadSyncText))?;
207 pos += 4;
208
209 content.push((time, text));
210 }
211
212 let header = FrameHeader::new(FRAME_ID, frame_flags);
213 Ok(Self {
214 header,
215 encoding,
216 language,
217 timestamp_format,
218 content_type,
219 description: if description.is_empty() {
220 None
221 } else {
222 Some(description)
223 },
224 content,
225 })
226 }
227
228 pub fn as_bytes(&self, write_options: WriteOptions) -> Result<Vec<u8>> {
239 let mut data = vec![self.encoding as u8];
240
241 if self.language.len() == 3 && self.language.iter().all(u8::is_ascii_alphabetic) {
242 data.write_all(&self.language)?;
243 data.write_u8(self.timestamp_format as u8)?;
244 data.write_u8(self.content_type as u8)?;
245
246 if let Some(description) = &self.description {
247 data.write_all(&self.encoding.encode(
248 description,
249 true,
250 write_options.lossy_text_encoding,
251 )?)?;
252 } else {
253 data.write_u8(0)?;
254 }
255
256 for (time, text) in &self.content {
257 data.write_all(&self.encoding.encode(
258 text,
259 true,
260 write_options.lossy_text_encoding,
261 )?)?;
262 data.write_u32::<BigEndian>(*time)?;
263 }
264
265 if data.len() as u64 > u64::from(u32::MAX) {
266 err!(TooMuchData);
267 }
268
269 return Ok(data);
270 }
271
272 Err(Id3v2Error::new(Id3v2ErrorKind::BadSyncText).into())
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use crate::config::WriteOptions;
279 use crate::id3::v2::{
280 FrameFlags, FrameHeader, SyncTextContentType, SynchronizedTextFrame, TimestampFormat,
281 };
282 use crate::util::text::TextEncoding;
283
284 fn expected(encoding: TextEncoding) -> SynchronizedTextFrame<'static> {
285 SynchronizedTextFrame {
286 header: FrameHeader::new(super::FRAME_ID, FrameFlags::default()),
287 encoding,
288 language: *b"eng",
289 timestamp_format: TimestampFormat::MS,
290 content_type: SyncTextContentType::Lyrics,
291 description: Some(String::from("Test Sync Text")),
292 content: vec![
293 (0, String::from("\nLofty")),
294 (10000, String::from("\nIs")),
295 (15000, String::from("\nReading")),
296 (30000, String::from("\nThis")),
297 (1_938_000, String::from("\nCorrectly")),
298 ],
299 }
300 }
301
302 #[test_log::test]
303 fn sylt_decode() {
304 let cont = crate::tag::utils::test_utils::read_path("tests/tags/assets/id3v2/test.sylt");
305
306 let parsed_sylt = SynchronizedTextFrame::parse(&cont, FrameFlags::default()).unwrap();
307
308 assert_eq!(parsed_sylt, expected(TextEncoding::Latin1));
309 }
310
311 #[test_log::test]
312 fn sylt_encode() {
313 let encoded = expected(TextEncoding::Latin1)
314 .as_bytes(WriteOptions::default())
315 .unwrap();
316
317 let expected_bytes =
318 crate::tag::utils::test_utils::read_path("tests/tags/assets/id3v2/test.sylt");
319
320 assert_eq!(encoded, expected_bytes);
321 }
322
323 #[test_log::test]
324 fn sylt_decode_utf16() {
325 let cont =
326 crate::tag::utils::test_utils::read_path("tests/tags/assets/id3v2/test_utf16.sylt");
327
328 let parsed_sylt = SynchronizedTextFrame::parse(&cont, FrameFlags::default()).unwrap();
329
330 assert_eq!(parsed_sylt, expected(TextEncoding::UTF16));
331 }
332
333 #[test_log::test]
334 fn sylt_encode_utf_16() {
335 let encoded = expected(TextEncoding::UTF16)
336 .as_bytes(WriteOptions::default())
337 .unwrap();
338
339 let expected_bytes =
340 crate::tag::utils::test_utils::read_path("tests/tags/assets/id3v2/test_utf16.sylt");
341
342 assert_eq!(encoded, expected_bytes);
343 }
344}