Skip to main content

lofty/id3/v2/write/
frame.rs

1use crate::config::WriteOptions;
2use crate::error::{Id3v2Error, Id3v2ErrorKind, Result};
3use crate::id3::v2::frame::FrameFlags;
4use crate::id3::v2::tag::GenresIter;
5use crate::id3::v2::util::synchsafe::SynchsafeInteger;
6use crate::id3::v2::{Frame, FrameId, KeyValueFrame, TextInformationFrame};
7use crate::tag::items::Timestamp;
8
9use std::borrow::Cow;
10use std::io::Write;
11
12use byteorder::{BigEndian, WriteBytesExt};
13
14/// Adapter to filter out any lingering ID3v2.2 frames
15fn strip_outdated_frames<'a>(
16	frames: &mut dyn Iterator<Item = Frame<'a>>,
17) -> impl Iterator<Item = Frame<'a>> {
18	frames.filter_map(|f| {
19		if f.id().is_valid() {
20			Some(f)
21		} else {
22			log::warn!("Discarding outdated frame: {}", f.id_str());
23			None
24		}
25	})
26}
27
28pub(in crate::id3::v2) fn create_items<W>(
29	writer: &mut W,
30	frames: &mut dyn Iterator<Item = Frame<'_>>,
31	write_options: WriteOptions,
32) -> Result<()>
33where
34	W: Write,
35{
36	for frame in strip_outdated_frames(frames) {
37		verify_frame(&frame)?;
38		let value = frame.as_bytes(write_options)?;
39
40		write_frame(
41			writer,
42			frame.id().as_str(),
43			frame.flags(),
44			&value,
45			write_options,
46		)?;
47	}
48
49	Ok(())
50}
51
52pub(in crate::id3::v2) fn create_items_v3<W>(
53	writer: &mut W,
54	frames: &mut dyn Iterator<Item = Frame<'_>>,
55	write_options: WriteOptions,
56) -> Result<()>
57where
58	W: Write,
59{
60	// These are all frames from ID3v2.4
61	const FRAMES_TO_DISCARD: &[&str] = &[
62		"ASPI", "EQU2", "RVA2", "SEEK", "SIGN", "TDEN", "TDRL", "TDTG", "TMOO", "TPRO", "TSOA",
63		"TSOP", "TSOT", "TSST",
64	];
65
66	const IPLS_ID: &str = "IPLS";
67
68	let mut ipls = None;
69	for mut frame in strip_outdated_frames(frames) {
70		if FRAMES_TO_DISCARD.contains(&frame.id_str()) {
71			log::warn!(
72				"Discarding frame: {}, not supported in ID3v2.3",
73				frame.id_str()
74			);
75			continue;
76		}
77
78		verify_frame(&frame)?;
79
80		match frame.id_str() {
81			// TORY (Original release year) is the only component of TDOR
82			// that is supported in ID3v2.3
83			//
84			// TDRC (Recording time) gets split into three frames: TYER, TDAT, and TIME
85			"TDOR" | "TDRC" => {
86				let is_tdor = frame.id_str() == "TDOR";
87
88				let Frame::Timestamp(f) = &mut frame else {
89					log::warn!(
90						"Discarding frame: {}, not supported in ID3v2.3",
91						frame.id_str()
92					);
93					continue;
94				};
95
96				if f.timestamp.verify().is_err() {
97					log::warn!("Discarding frame: {}, invalid timestamp", frame.id_str());
98					continue;
99				}
100
101				if is_tdor {
102					let year = f.timestamp.year;
103					f.timestamp = Timestamp {
104						year,
105						..Timestamp::default()
106					};
107
108					f.header.id = FrameId::Valid("TORY".into());
109				} else {
110					let mut new_frames = Vec::with_capacity(3);
111
112					let timestamp = f.timestamp;
113
114					let year = timestamp.year;
115					new_frames.push(Frame::Text(TextInformationFrame::new(
116						FrameId::Valid("TYER".into()),
117						f.encoding.to_id3v23(),
118						year.to_string(),
119					)));
120
121					if let (Some(month), Some(day)) = (timestamp.month, timestamp.day) {
122						let date = format!("{:02}{:02}", day, month);
123						new_frames.push(Frame::Text(TextInformationFrame::new(
124							FrameId::Valid("TDAT".into()),
125							f.encoding.to_id3v23(),
126							date,
127						)));
128					}
129
130					if let (Some(hour), Some(minute)) = (timestamp.hour, timestamp.minute) {
131						let time = format!("{:02}{:02}", hour, minute);
132						new_frames.push(Frame::Text(TextInformationFrame::new(
133							FrameId::Valid("TIME".into()),
134							f.encoding.to_id3v23(),
135							time,
136						)));
137					}
138
139					for mut frame in new_frames {
140						frame.set_flags(f.header.flags);
141						let value = frame.as_bytes(write_options)?;
142
143						write_frame(
144							writer,
145							frame.id().as_str(),
146							frame.flags(),
147							&value,
148							write_options,
149						)?;
150					}
151
152					continue;
153				}
154			},
155			// TCON (Content type) cannot be separated by nulls, so we have to wrap its
156			// components in parentheses
157			"TCON" => {
158				let Frame::Text(f) = &mut frame else {
159					log::warn!(
160						"Discarding frame: {}, not supported in ID3v2.3",
161						frame.id_str()
162					);
163					continue;
164				};
165
166				let mut new_genre_string = String::new();
167				let genres = GenresIter::new(&f.value, true).collect::<Vec<_>>();
168				for (i, genre) in genres.iter().enumerate() {
169					match *genre {
170						"Remix" => new_genre_string.push_str("(RX)"),
171						"Cover" => new_genre_string.push_str("(CR)"),
172						_ if i == genres.len() - 1 && genre.parse::<u8>().is_err() => {
173							new_genre_string.push_str(genre);
174						},
175						_ => {
176							new_genre_string = format!("{new_genre_string}({genre})");
177						},
178					}
179				}
180
181				f.value = Cow::Owned(new_genre_string);
182			},
183			// TIPL (Involved people list) and TMCL (Musician credits list) are
184			// both key-value pairs. ID3v2.3 does not distinguish between the two,
185			// so we must merge them into a single IPLS frame.
186			"TIPL" | "TMCL" => {
187				let Frame::KeyValue(KeyValueFrame {
188					key_value_pairs,
189					encoding,
190					..
191				}) = &mut frame
192				else {
193					log::warn!(
194						"Discarding frame: {}, not supported in ID3v2.3",
195						frame.id_str()
196					);
197					continue;
198				};
199
200				let ipls_frame;
201				match ipls {
202					Some(ref mut frame) => {
203						ipls_frame = frame;
204					},
205					None => {
206						ipls = Some(TextInformationFrame::new(
207							FrameId::Valid("IPLS".into()),
208							encoding.to_id3v23(),
209							String::new(),
210						));
211						ipls_frame = ipls.as_mut().unwrap();
212					},
213				}
214
215				for (key, value) in key_value_pairs.drain(..) {
216					if !ipls_frame.value.is_empty() {
217						match &mut ipls_frame.value {
218							Cow::Owned(v) => v.push('\0'),
219							v => {
220								let mut new = String::from(&**v);
221								new.push('\0');
222
223								*v = Cow::Owned(new);
224							},
225						}
226					}
227
228					ipls_frame.value = Cow::Owned(format!("{}{key}\0{value}", ipls_frame.value));
229				}
230
231				continue;
232			},
233			_ => {},
234		}
235
236		let value = frame.as_bytes(write_options)?;
237
238		write_frame(
239			writer,
240			frame.id().as_str(),
241			frame.flags(),
242			&value,
243			write_options,
244		)?;
245	}
246
247	if let Some(ipls) = ipls {
248		let frame = Frame::Text(ipls);
249		let value = frame.as_bytes(write_options)?;
250		write_frame(writer, IPLS_ID, frame.flags(), &value, write_options)?;
251	}
252
253	Ok(())
254}
255
256fn verify_frame(frame: &Frame<'_>) -> Result<()> {
257	match (frame.id().as_str(), frame) {
258		("APIC", Frame::Picture { .. })
259		| ("USLT", Frame::UnsynchronizedText(_))
260		| ("COMM", Frame::Comment(_))
261		| ("TXXX", Frame::UserText(_))
262		| ("WXXX", Frame::UserUrl(_))
263		| (_, Frame::Binary(_))
264		| ("UFID", Frame::UniqueFileIdentifier(_))
265		| ("POPM", Frame::Popularimeter(_))
266		| ("TIPL" | "TMCL", Frame::KeyValue { .. })
267		| ("WFED" | "GRP1" | "MVNM" | "MVIN", Frame::Text { .. })
268		| ("TDEN" | "TDOR" | "TDRC" | "TDRL" | "TDTG", Frame::Timestamp(_))
269		| ("RVA2", Frame::RelativeVolumeAdjustment(_))
270		| ("PRIV", Frame::Private(_)) => Ok(()),
271		(id, Frame::Text { .. }) if id.starts_with('T') => Ok(()),
272		(id, Frame::Url(_)) if id.starts_with('W') => Ok(()),
273		(id, frame_value) => Err(Id3v2Error::new(Id3v2ErrorKind::BadFrame(
274			id.to_string(),
275			frame_value.name(),
276		))
277		.into()),
278	}
279}
280
281fn write_frame<W>(
282	writer: &mut W,
283	name: &str,
284	flags: FrameFlags,
285	value: &[u8],
286	write_options: WriteOptions,
287) -> Result<()>
288where
289	W: Write,
290{
291	if flags.encryption.is_some() {
292		write_encrypted(writer, name, value, flags, write_options)?;
293		return Ok(());
294	}
295
296	let len = value.len() as u32;
297	let is_grouping_identity = flags.grouping_identity.is_some();
298
299	write_frame_header(
300		writer,
301		name,
302		if is_grouping_identity { len + 1 } else { len },
303		flags,
304		write_options,
305	)?;
306
307	if is_grouping_identity {
308		// Guaranteed to be `Some` at this point.
309		writer.write_u8(flags.grouping_identity.unwrap())?;
310	}
311
312	writer.write_all(value)?;
313
314	Ok(())
315}
316
317fn write_encrypted<W>(
318	writer: &mut W,
319	name: &str,
320	value: &[u8],
321	flags: FrameFlags,
322	write_options: WriteOptions,
323) -> Result<()>
324where
325	W: Write,
326{
327	// Guaranteed to be `Some` at this point.
328	let method_symbol = flags.encryption.unwrap();
329
330	if method_symbol > 0x80 {
331		return Err(
332			Id3v2Error::new(Id3v2ErrorKind::InvalidEncryptionMethodSymbol(method_symbol)).into(),
333		);
334	}
335
336	if let Some(mut len) = flags.data_length_indicator {
337		if len > 0 {
338			write_frame_header(writer, name, (value.len() + 1) as u32, flags, write_options)?;
339			if !write_options.use_id3v23 {
340				len = len.synch()?;
341			}
342
343			writer.write_u32::<BigEndian>(len)?;
344			writer.write_u8(method_symbol)?;
345			writer.write_all(value)?;
346
347			return Ok(());
348		}
349	}
350
351	Err(Id3v2Error::new(Id3v2ErrorKind::MissingDataLengthIndicator).into())
352}
353
354fn write_frame_header<W>(
355	writer: &mut W,
356	name: &str,
357	mut len: u32,
358	flags: FrameFlags,
359	write_options: WriteOptions,
360) -> Result<()>
361where
362	W: Write,
363{
364	let flags = if write_options.use_id3v23 {
365		flags.as_id3v23_bytes()
366	} else {
367		flags.as_id3v24_bytes()
368	};
369
370	writer.write_all(name.as_bytes())?;
371	if !write_options.use_id3v23 {
372		len = len.synch()?;
373	}
374
375	writer.write_u32::<BigEndian>(len)?;
376	writer.write_u16::<BigEndian>(flags)?;
377
378	Ok(())
379}