lofty/id3/v2/frame/
mod.rs

1pub(super) mod content;
2mod conversion;
3pub(super) mod header;
4pub(super) mod read;
5
6use super::header::Id3v2Version;
7use super::items::{
8	AttachedPictureFrame, BinaryFrame, CommentFrame, EventTimingCodesFrame, ExtendedTextFrame,
9	ExtendedUrlFrame, KeyValueFrame, OwnershipFrame, PopularimeterFrame, PrivateFrame,
10	RelativeVolumeAdjustmentFrame, TextInformationFrame, TimestampFrame, UniqueFileIdentifierFrame,
11	UnsynchronizedTextFrame, UrlLinkFrame,
12};
13use crate::error::Result;
14use crate::id3::v2::FrameHeader;
15use crate::util::text::TextEncoding;
16use header::FrameId;
17
18use std::borrow::Cow;
19use std::hash::Hash;
20use std::ops::Deref;
21
22pub(super) const MUSICBRAINZ_UFID_OWNER: &str = "http://musicbrainz.org";
23
24/// Empty content descriptor in text frame
25///
26/// Unspecific [`CommentFrame`]s, [`UnsynchronizedTextFrame`]s, and [`ExtendedTextFrame`] frames
27/// are supposed to have an empty content descriptor. Only those
28/// are currently supported as [`TagItem`]s to avoid ambiguities
29/// and to prevent inconsistencies when writing them.
30pub(super) const EMPTY_CONTENT_DESCRIPTOR: String = String::new();
31
32// TODO: Messy module, rough conversions
33
34macro_rules! define_frames {
35	(
36		$(#[$meta:meta])*
37		pub enum Frame<'a> {
38			$(
39				$(#[$field_meta:meta])+
40				$variant:ident($type:ty),
41			)*
42		}
43	) => {
44		$(#[$meta])*
45		pub enum Frame<'a> {
46			$(
47				$(#[$field_meta])+
48				$variant($type),
49			)*
50		}
51
52		impl Frame<'_> {
53			/// Get the ID of the frame
54			pub fn id(&self) -> &FrameId<'_> {
55				match self {
56					$(
57						Frame::$variant(frame) => &frame.header.id,
58					)*
59				}
60			}
61
62			/// Get the flags for the frame
63			pub fn flags(&self) -> FrameFlags {
64				match self {
65					$(
66						Frame::$variant(frame) => frame.flags(),
67					)*
68				}
69			}
70
71			/// Set the flags for the frame
72			pub fn set_flags(&mut self, flags: FrameFlags) {
73				match self {
74					$(
75						Frame::$variant(frame) => frame.set_flags(flags),
76					)*
77				}
78			}
79		}
80
81		$(
82			impl<'a> From<$type> for Frame<'a> {
83				fn from(value: $type) -> Self {
84					Frame::$variant(value)
85				}
86			}
87		)*
88	}
89}
90
91define_frames! {
92	/// Represents an `ID3v2` frame
93	///
94	/// ## Outdated Frames
95	///
96	/// ### ID3v2.2
97	///
98	/// `ID3v2.2` frame IDs are 3 characters. When reading these tags, [`upgrade_v2`](crate::id3::v2::upgrade_v2) is used, which has a list of all of the common IDs
99	/// that have a mapping to `ID3v2.4`. Any ID that fails to be converted will be stored as [`FrameId::Outdated`], and it must be manually
100	/// upgraded before it can be written. **Lofty** will not write `ID3v2.2` tags.
101	///
102	/// ### ID3v2.3
103	///
104	/// `ID3v2.3`, unlike `ID3v2.2`, stores frame IDs in 4 characters like `ID3v2.4`. There are some IDs that need upgrading (See [`upgrade_v3`](crate::id3::v2::upgrade_v3)),
105	/// but anything that fails to be upgraded **will not** be stored as [`FrameId::Outdated`], as it is likely not an issue to write.
106	#[non_exhaustive]
107	#[derive(Clone, Debug, PartialEq, Eq, Hash)]
108	pub enum Frame<'a> {
109		/// Represents a "COMM" frame
110		Comment(CommentFrame<'a>),
111		/// Represents a "USLT" frame
112		UnsynchronizedText(UnsynchronizedTextFrame<'a>),
113		/// Represents a "T..." (excluding TXXX) frame
114		Text(TextInformationFrame<'a>),
115		/// Represents a "TXXX" frame
116		UserText(ExtendedTextFrame<'a>),
117		/// Represents a "W..." (excluding WXXX) frame
118		Url(UrlLinkFrame<'a>),
119		/// Represents a "WXXX" frame
120		UserUrl(ExtendedUrlFrame<'a>),
121		/// Represents an "APIC" or "PIC" frame
122		Picture(AttachedPictureFrame<'a>),
123		/// Represents a "POPM" frame
124		Popularimeter(PopularimeterFrame<'a>),
125		/// Represents an "IPLS" or "TPIL" frame
126		KeyValue(KeyValueFrame<'a>),
127		/// Represents an "RVA2" frame
128		RelativeVolumeAdjustment(RelativeVolumeAdjustmentFrame<'a>),
129		/// Unique file identifier
130		UniqueFileIdentifier(UniqueFileIdentifierFrame<'a>),
131		/// Represents an "OWNE" frame
132		Ownership(OwnershipFrame<'a>),
133		/// Represents an "ETCO" frame
134		EventTimingCodes(EventTimingCodesFrame<'a>),
135		/// Represents a "PRIV" frame
136		Private(PrivateFrame<'a>),
137		/// Represents a timestamp for the "TDEN", "TDOR", "TDRC", "TDRL", and "TDTG" frames
138		Timestamp(TimestampFrame<'a>),
139		/// Binary data
140		///
141		/// NOTES:
142		///
143		/// * This is used for rare frames, such as GEOB, SYLT, and ATXT to skip additional unnecessary work.
144		///   See [`GeneralEncapsulatedObject::parse`](crate::id3::v2::GeneralEncapsulatedObject::parse), [`SynchronizedText::parse`](crate::id3::v2::SynchronizedTextFrame::parse), and [`AudioTextFrame::parse`](crate::id3::v2::AudioTextFrame::parse) respectively
145		/// * This is used for **all** frames with an ID of [`FrameId::Outdated`]
146		/// * This is used for unknown frames
147		Binary(BinaryFrame<'a>),
148	}
149}
150
151impl<'a> Frame<'a> {
152	/// Extract the string from the [`FrameId`]
153	pub fn id_str(&self) -> &str {
154		self.id().as_str()
155	}
156
157	// Used internally, has no correctness checks
158	pub(crate) fn text(id: Cow<'a, str>, content: String) -> Self {
159		Frame::Text(TextInformationFrame {
160			header: FrameHeader::new(FrameId::Valid(id), FrameFlags::default()),
161			encoding: TextEncoding::UTF8,
162			value: content,
163		})
164	}
165}
166
167impl Frame<'_> {
168	/// Check for empty content
169	///
170	/// Returns `None` if the frame type is not supported.
171	pub(super) fn is_empty(&self) -> Option<bool> {
172		let is_empty = match self {
173			Frame::Text(text) => text.value.is_empty(),
174			Frame::UserText(extended_text) => extended_text.content.is_empty(),
175			Frame::Url(link) => link.content.is_empty(),
176			Frame::UserUrl(extended_url) => extended_url.content.is_empty(),
177			Frame::Comment(comment) => comment.content.is_empty(),
178			Frame::UnsynchronizedText(unsync_text) => unsync_text.content.is_empty(),
179			Frame::Picture(picture) => picture.picture.data.is_empty(),
180			Frame::KeyValue(key_value) => key_value.key_value_pairs.is_empty(),
181			Frame::UniqueFileIdentifier(ufid) => ufid.identifier.is_empty(),
182			Frame::EventTimingCodes(event_timing) => event_timing.events.is_empty(),
183			Frame::Private(private) => private.private_data.is_empty(),
184			Frame::Binary(binary) => binary.data.is_empty(),
185			Frame::Popularimeter(_)
186			| Frame::RelativeVolumeAdjustment(_)
187			| Frame::Ownership(_)
188			| Frame::Timestamp(_) => {
189				// Undefined.
190				return None;
191			},
192		};
193		Some(is_empty)
194	}
195}
196
197impl Frame<'_> {
198	pub(super) fn as_bytes(&self, is_id3v23: bool) -> Result<Vec<u8>> {
199		Ok(match self {
200			Frame::Comment(comment) => comment.as_bytes(is_id3v23)?,
201			Frame::UnsynchronizedText(lf) => lf.as_bytes(is_id3v23)?,
202			Frame::Text(tif) => tif.as_bytes(is_id3v23),
203			Frame::UserText(content) => content.as_bytes(is_id3v23),
204			Frame::UserUrl(content) => content.as_bytes(is_id3v23),
205			Frame::Url(link) => link.as_bytes(),
206			Frame::Picture(attached_picture) => {
207				let version = if is_id3v23 {
208					Id3v2Version::V3
209				} else {
210					Id3v2Version::V4
211				};
212
213				attached_picture.as_bytes(version)?
214			},
215			Frame::Popularimeter(popularimeter) => popularimeter.as_bytes()?,
216			Frame::KeyValue(content) => content.as_bytes(is_id3v23),
217			Frame::RelativeVolumeAdjustment(frame) => frame.as_bytes(),
218			Frame::UniqueFileIdentifier(frame) => frame.as_bytes(),
219			Frame::Ownership(frame) => frame.as_bytes(is_id3v23)?,
220			Frame::EventTimingCodes(frame) => frame.as_bytes(),
221			Frame::Private(frame) => frame.as_bytes()?,
222			Frame::Timestamp(frame) => frame.as_bytes(is_id3v23)?,
223			Frame::Binary(frame) => frame.as_bytes(),
224		})
225	}
226
227	/// Used for errors in write::frame::verify_frame
228	pub(super) fn name(&self) -> &'static str {
229		match self {
230			Frame::Comment(_) => "Comment",
231			Frame::UnsynchronizedText(_) => "UnsynchronizedText",
232			Frame::Text { .. } => "Text",
233			Frame::UserText(_) => "UserText",
234			Frame::Url(_) => "Url",
235			Frame::UserUrl(_) => "UserUrl",
236			Frame::Picture { .. } => "Picture",
237			Frame::Popularimeter(_) => "Popularimeter",
238			Frame::KeyValue(_) => "KeyValue",
239			Frame::UniqueFileIdentifier(_) => "UniqueFileIdentifier",
240			Frame::RelativeVolumeAdjustment(_) => "RelativeVolumeAdjustment",
241			Frame::Ownership(_) => "Ownership",
242			Frame::EventTimingCodes(_) => "EventTimingCodes",
243			Frame::Private(_) => "Private",
244			Frame::Timestamp(_) => "Timestamp",
245			Frame::Binary(_) => "Binary",
246		}
247	}
248}
249
250/// Various flags to describe the content of an item
251#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)]
252#[allow(clippy::struct_excessive_bools)]
253pub struct FrameFlags {
254	/// Preserve frame on tag edit
255	pub tag_alter_preservation: bool,
256	/// Preserve frame on file edit
257	pub file_alter_preservation: bool,
258	/// Item cannot be written to
259	pub read_only: bool,
260	/// The group identifier the frame belongs to
261	///
262	/// All frames with the same group identifier byte belong to the same group.
263	pub grouping_identity: Option<u8>,
264	/// Frame is zlib compressed
265	///
266	/// It is **required** `data_length_indicator` be set if this is set.
267	pub compression: bool,
268	/// Frame encryption method symbol
269	///
270	/// NOTE: Since the encryption method is unknown, lofty cannot do anything with these frames
271	///
272	/// The encryption method symbol **must** be > 0x80.
273	pub encryption: Option<u8>,
274	/// Frame is unsynchronised
275	///
276	/// In short, this makes all "0xFF X (X >= 0xE0)" combinations into "0xFF 0x00 X" to avoid confusion
277	/// with the MPEG frame header, which is often identified by its "frame sync" (11 set bits).
278	/// It is preferred an ID3v2 tag is either *completely* unsynchronised or not unsynchronised at all.
279	///
280	/// NOTE: While unsynchronized data is read, for the sake of simplicity, this flag has no effect when
281	/// writing. There isn't much reason to write unsynchronized data.
282	pub unsynchronisation: bool, /* TODO: Maybe? This doesn't seem very useful, and it is wasted effort if one forgets to make this false when writing. */
283	/// Frame has a data length indicator
284	///
285	/// The data length indicator is the size of the frame if the flags were all zeroed out.
286	/// This is usually used in combination with `compression` and `encryption` (depending on encryption method).
287	///
288	/// If using `encryption`, the final size must be added.
289	pub data_length_indicator: Option<u32>,
290}
291
292impl FrameFlags {
293	/// Parse the flags from an ID3v2.4 frame
294	///
295	/// NOTE: If any of the following flags are set, they will be set to `Some(0)`:
296	/// * `grouping_identity`
297	/// * `encryption`
298	/// * `data_length_indicator`
299	pub fn parse_id3v24(flags: u16) -> Self {
300		FrameFlags {
301			tag_alter_preservation: flags & 0x4000 == 0x4000,
302			file_alter_preservation: flags & 0x2000 == 0x2000,
303			read_only: flags & 0x1000 == 0x1000,
304			grouping_identity: (flags & 0x0040 == 0x0040).then_some(0),
305			compression: flags & 0x0008 == 0x0008,
306			encryption: (flags & 0x0004 == 0x0004).then_some(0),
307			unsynchronisation: flags & 0x0002 == 0x0002,
308			data_length_indicator: (flags & 0x0001 == 0x0001).then_some(0),
309		}
310	}
311
312	/// Parse the flags from an ID3v2.3 frame
313	///
314	/// NOTE: If any of the following flags are set, they will be set to `Some(0)`:
315	/// * `grouping_identity`
316	/// * `encryption`
317	pub fn parse_id3v23(flags: u16) -> Self {
318		FrameFlags {
319			tag_alter_preservation: flags & 0x8000 == 0x8000,
320			file_alter_preservation: flags & 0x4000 == 0x4000,
321			read_only: flags & 0x2000 == 0x2000,
322			grouping_identity: (flags & 0x0020 == 0x0020).then_some(0),
323			compression: flags & 0x0080 == 0x0080,
324			encryption: (flags & 0x0040 == 0x0040).then_some(0),
325			unsynchronisation: false,
326			data_length_indicator: None,
327		}
328	}
329
330	/// Get the ID3v2.4 byte representation of the flags
331	pub fn as_id3v24_bytes(&self) -> u16 {
332		let mut flags = 0;
333
334		if *self == FrameFlags::default() {
335			return flags;
336		}
337
338		if self.tag_alter_preservation {
339			flags |= 0x4000
340		}
341
342		if self.file_alter_preservation {
343			flags |= 0x2000
344		}
345
346		if self.read_only {
347			flags |= 0x1000
348		}
349
350		if self.grouping_identity.is_some() {
351			flags |= 0x0040
352		}
353
354		if self.compression {
355			flags |= 0x0008
356		}
357
358		if self.encryption.is_some() {
359			flags |= 0x0004
360		}
361
362		if self.unsynchronisation {
363			flags |= 0x0002
364		}
365
366		if self.data_length_indicator.is_some() {
367			flags |= 0x0001
368		}
369
370		flags
371	}
372
373	/// Get the ID3v2.3 byte representation of the flags
374	pub fn as_id3v23_bytes(&self) -> u16 {
375		let mut flags = 0;
376
377		if *self == FrameFlags::default() {
378			return flags;
379		}
380
381		if self.tag_alter_preservation {
382			flags |= 0x8000
383		}
384
385		if self.file_alter_preservation {
386			flags |= 0x4000
387		}
388
389		if self.read_only {
390			flags |= 0x2000
391		}
392
393		if self.grouping_identity.is_some() {
394			flags |= 0x0020
395		}
396
397		if self.compression {
398			flags |= 0x0080
399		}
400
401		if self.encryption.is_some() {
402			flags |= 0x0040
403		}
404
405		flags
406	}
407}
408
409#[derive(Clone)]
410pub(crate) struct FrameRef<'a>(pub(crate) Cow<'a, Frame<'a>>);
411
412impl<'a> Deref for FrameRef<'a> {
413	type Target = Frame<'a>;
414
415	fn deref(&self) -> &Self::Target {
416		self.0.as_ref()
417	}
418}
419
420impl<'a> Frame<'a> {
421	pub(crate) fn as_opt_ref(&'a self) -> Option<FrameRef<'a>> {
422		if let FrameId::Valid(_) = self.id() {
423			Some(FrameRef(Cow::Borrowed(self)))
424		} else {
425			None
426		}
427	}
428}