Skip to main content

lofty/id3/v2/items/
key_value_frame.rs

1use crate::config::WriteOptions;
2use crate::error::Result;
3use crate::id3::v2::frame::content::verify_encoding;
4use crate::id3::v2::header::Id3v2Version;
5use crate::id3::v2::{FrameFlags, FrameHeader, FrameId};
6use crate::util::text::{TextDecodeOptions, TextEncoding, decode_text};
7
8use std::borrow::Cow;
9use std::io::Read;
10
11use byteorder::ReadBytesExt;
12
13/// An `ID3v2` key-value frame
14#[derive(Clone, Debug, Eq, PartialEq, Hash)]
15pub struct KeyValueFrame<'a> {
16	pub(crate) header: FrameHeader<'a>,
17	/// The encoding of the text
18	pub encoding: TextEncoding,
19	/// The key value pairs. Keys can be specified multiple times
20	pub key_value_pairs: Vec<(Cow<'a, str>, Cow<'a, str>)>,
21}
22
23impl<'a> KeyValueFrame<'a> {
24	/// Create a new [`KeyValueFrame`]
25	pub fn new(
26		id: FrameId<'a>,
27		encoding: TextEncoding,
28		key_value_pairs: Vec<(Cow<'a, str>, Cow<'a, str>)>,
29	) -> Self {
30		let header = FrameHeader::new(id, FrameFlags::default());
31		Self {
32			header,
33			encoding,
34			key_value_pairs,
35		}
36	}
37
38	/// Get the ID for the frame
39	pub fn id(&self) -> &FrameId<'_> {
40		&self.header.id
41	}
42
43	/// Get the flags for the frame
44	pub fn flags(&self) -> FrameFlags {
45		self.header.flags
46	}
47
48	/// Set the flags for the frame
49	pub fn set_flags(&mut self, flags: FrameFlags) {
50		self.header.flags = flags;
51	}
52
53	/// Read an [`KeyValueFrame`] from a slice
54	///
55	/// NOTE: This expects the frame header to have already been skipped
56	///
57	/// # Errors
58	///
59	/// * Unable to decode the text
60	///
61	/// ID3v2.2:
62	///
63	/// * The encoding is not [`TextEncoding::Latin1`] or [`TextEncoding::UTF16`]
64	pub fn parse<R>(
65		reader: &mut R,
66		id: FrameId<'a>,
67		frame_flags: FrameFlags,
68		version: Id3v2Version,
69	) -> Result<Option<Self>>
70	where
71		R: Read,
72	{
73		let Ok(encoding_byte) = reader.read_u8() else {
74			return Ok(None);
75		};
76
77		let encoding = verify_encoding(encoding_byte, version)?;
78
79		let mut values = Vec::new();
80
81		let mut text_decode_options = TextDecodeOptions::new().encoding(encoding).terminated(true);
82
83		// We have to read the first key/value pair separately because it may be the only string with a BOM
84
85		let first_key = decode_text(reader, text_decode_options)?;
86
87		if first_key.bytes_read == 0 {
88			return Ok(None);
89		}
90
91		if encoding == TextEncoding::UTF16 {
92			text_decode_options = text_decode_options.bom(first_key.bom);
93		}
94
95		values.push((
96			Cow::Owned(first_key.content),
97			Cow::Owned(decode_text(reader, text_decode_options)?.content),
98		));
99
100		loop {
101			let key = decode_text(reader, text_decode_options)?;
102			let value = decode_text(reader, text_decode_options)?;
103			if key.bytes_read == 0 || value.bytes_read == 0 {
104				break;
105			}
106
107			values.push((Cow::Owned(key.content), Cow::Owned(value.content)));
108		}
109
110		let header = FrameHeader::new(id, frame_flags);
111		Ok(Some(Self {
112			header,
113			encoding,
114			key_value_pairs: values,
115		}))
116	}
117
118	/// Convert a [`KeyValueFrame`] to a byte vec
119	///
120	/// # Errors
121	///
122	/// * [`WriteOptions::lossy_text_encoding()`] is disabled and the content cannot be encoded in the specified [`TextEncoding`].
123	pub fn as_bytes(&self, write_options: WriteOptions) -> Result<Vec<u8>> {
124		let mut encoding = self.encoding;
125		if write_options.use_id3v23 {
126			encoding = encoding.to_id3v23();
127		}
128
129		let mut content = vec![encoding as u8];
130
131		for (key, value) in &self.key_value_pairs {
132			content.append(&mut encoding.encode(key, true, write_options.lossy_text_encoding)?);
133			content.append(&mut encoding.encode(value, true, write_options.lossy_text_encoding)?);
134		}
135		Ok(content)
136	}
137}
138
139impl KeyValueFrame<'static> {
140	pub(crate) fn downgrade(&self) -> KeyValueFrame<'_> {
141		KeyValueFrame {
142			header: self.header.downgrade(),
143			encoding: self.encoding,
144			// TODO: not ideal
145			key_value_pairs: self.key_value_pairs.clone(),
146		}
147	}
148}