Skip to main content

lofty/mp4/ilst/
read.rs

1use super::constants::WELL_KNOWN_TYPE_SET;
2use super::data_type::DataType;
3use super::{Atom, AtomData, AtomIdent, Ilst};
4use crate::config::{ParseOptions, ParsingMode};
5use crate::error::{LoftyError, Result};
6use crate::id3::v1::constants::GENRES;
7use crate::macros::{err, try_vec};
8use crate::mp4::atom_info::{ATOM_HEADER_LEN, AtomInfo};
9use crate::mp4::ilst::atom::AtomDataStorage;
10use crate::mp4::read::{AtomReader, skip_atom};
11use crate::picture::{MimeType, Picture, PictureType};
12use crate::tag::TagExt;
13use crate::util::text::{utf8_decode, utf16_decode_bytes};
14
15use std::borrow::Cow;
16use std::io::{Cursor, Read, Seek, SeekFrom};
17
18pub(in crate::mp4) fn parse_ilst<R>(
19	reader: &mut AtomReader<R>,
20	parse_options: ParseOptions,
21	len: u64,
22) -> Result<Ilst>
23where
24	R: Read + Seek,
25{
26	let parsing_mode = parse_options.parsing_mode;
27
28	let mut contents = try_vec![0; len as usize];
29	reader.read_exact(&mut contents)?;
30
31	let mut cursor = Cursor::new(contents);
32
33	let mut ilst_reader = AtomReader::new(&mut cursor, parsing_mode)?;
34
35	let mut tag = Ilst::default();
36
37	let mut upgraded_gnres = Vec::new();
38	while let Ok(Some(atom)) = ilst_reader.next() {
39		if let AtomIdent::Fourcc(ref fourcc) = atom.ident {
40			match fourcc {
41				b"free" | b"skip" => {
42					skip_atom(&mut ilst_reader, atom.extended, atom.len)?;
43					continue;
44				},
45				b"covr" => {
46					if parse_options.read_cover_art {
47						handle_covr(&mut ilst_reader, parsing_mode, &mut tag, &atom)?;
48					} else {
49						skip_atom(&mut ilst_reader, atom.extended, atom.len)?;
50					}
51
52					continue;
53				},
54				// Upgrade this to a \xa9gen atom
55				b"gnre" if parse_options.implicit_conversions => {
56					log::warn!("Encountered outdated 'gnre' atom, attempting to upgrade to '©gen'");
57
58					if let Some(atom_data) =
59						parse_data_inner(&mut ilst_reader, parsing_mode, &atom)?
60					{
61						for (_, content) in atom_data {
62							if content.len() >= 2 {
63								let index = content[1] as usize;
64								if index > 0 && index <= GENRES.len() {
65									upgraded_gnres
66										.push(AtomData::UTF8(String::from(GENRES[index - 1])));
67								}
68							}
69						}
70					}
71
72					continue;
73				},
74				// Just insert these normally, the caller will deal with them (hopefully)
75				b"gnre" => {
76					log::warn!("Encountered outdated 'gnre' atom");
77
78					if let Some(atom_data) =
79						parse_data_inner(&mut ilst_reader, parsing_mode, &atom)?
80					{
81						let mut data = Vec::new();
82
83						for (code, content) in atom_data {
84							data.push(AtomData::Unknown {
85								code,
86								data: content,
87							});
88						}
89
90						if let Some(storage) = AtomDataStorage::from_vec(data) {
91							tag.atoms.push(Atom {
92								ident: AtomIdent::Fourcc(*b"gnre"),
93								data: storage,
94							})
95						}
96					}
97
98					continue;
99				},
100				// Special case the "Album ID", as it has the code "BE signed integer" (21), but
101				// must be interpreted as a "BE 64-bit Signed Integer" (74)
102				b"plID" => {
103					if let Some(atom_data) =
104						parse_data_inner(&mut ilst_reader, parsing_mode, &atom)?
105					{
106						let mut data = Vec::new();
107
108						for (code, content) in atom_data {
109							if content.len() == 8 {
110								data.push(AtomData::Unknown {
111									code,
112									data: content,
113								})
114							}
115						}
116
117						if !data.is_empty() {
118							let storage = match data.len() {
119								1 => AtomDataStorage::Single(data.remove(0)),
120								_ => AtomDataStorage::Multiple(data),
121							};
122
123							tag.atoms.push(Atom {
124								ident: AtomIdent::Fourcc(*b"plID"),
125								data: storage,
126							})
127						}
128					}
129
130					continue;
131				},
132				b"cpil" | b"hdvd" | b"pcst" | b"pgap" | b"shwm" => {
133					if let Some(atom_data) =
134						parse_data_inner(&mut ilst_reader, parsing_mode, &atom)?
135					{
136						if let Some((_, content)) = atom_data.first() {
137							// Any size integer is technically valid, we'll correct it on write.
138							let is_true = content.iter().any(|&b| b != 0);
139							let data = AtomData::Bool(is_true);
140
141							tag.atoms.push(Atom {
142								ident: AtomIdent::Fourcc(*fourcc),
143								data: AtomDataStorage::Single(data),
144							})
145						}
146					}
147
148					continue;
149				},
150				_ => {},
151			}
152		}
153
154		parse_data(&mut ilst_reader, parsing_mode, &mut tag, atom)?;
155	}
156
157	if parse_options.implicit_conversions && !upgraded_gnres.is_empty() {
158		if tag.contains(&AtomIdent::Fourcc(*b"\xa9gen")) {
159			log::warn!("Encountered '©gen' atom, discarding upgraded 'gnre' atom(s)");
160			return Ok(tag);
161		}
162
163		if let Some(storage) = AtomDataStorage::from_vec(upgraded_gnres) {
164			tag.atoms.push(Atom {
165				ident: AtomIdent::Fourcc(*b"\xa9gen"),
166				data: storage,
167			})
168		}
169	}
170
171	Ok(tag)
172}
173
174fn parse_data<R>(
175	reader: &mut AtomReader<R>,
176	parsing_mode: ParsingMode,
177	tag: &mut Ilst,
178	atom_info: AtomInfo,
179) -> Result<()>
180where
181	R: Read + Seek,
182{
183	let handle_error = |err: LoftyError, parsing_mode: ParsingMode| -> Result<()> {
184		match parsing_mode {
185			ParsingMode::Strict => Err(err),
186			ParsingMode::BestAttempt | ParsingMode::Relaxed => {
187				log::warn!("Skipping atom with invalid content: {}", err);
188				Ok(())
189			},
190		}
191	};
192
193	if let Some(mut atom_data) = parse_data_inner(reader, parsing_mode, &atom_info)? {
194		// Most atoms we encounter are only going to have 1 value, so store them as such
195		if atom_data.len() == 1 {
196			let (flags, content) = atom_data.remove(0);
197			let data = match interpret_atom_content(flags, content) {
198				Ok(data) => data,
199				Err(err) => return handle_error(err, parsing_mode),
200			};
201
202			tag.atoms.push(Atom {
203				ident: atom_info.ident,
204				data: AtomDataStorage::Single(data),
205			});
206
207			return Ok(());
208		}
209
210		let mut data = Vec::new();
211		for (flags, content) in atom_data {
212			let value = match interpret_atom_content(flags, content) {
213				Ok(data) => data,
214				Err(err) => return handle_error(err, parsing_mode),
215			};
216
217			data.push(value);
218		}
219
220		tag.atoms.push(Atom {
221			ident: atom_info.ident,
222			data: AtomDataStorage::Multiple(data),
223		});
224	}
225
226	Ok(())
227}
228
229const DATA_ATOM_IDENT: AtomIdent<'static> = AtomIdent::Fourcc(*b"data");
230
231fn parse_data_inner<R>(
232	reader: &mut AtomReader<R>,
233	parsing_mode: ParsingMode,
234	atom_info: &AtomInfo,
235) -> Result<Option<Vec<(DataType, Vec<u8>)>>>
236where
237	R: Read + Seek,
238{
239	// An atom can contain multiple data atoms
240	let mut ret = Vec::new();
241
242	let atom_end = atom_info.start + atom_info.len;
243	let position = reader.stream_position()?;
244	assert!(
245		atom_end >= position,
246		"uncaught size mismatch, reader position: {position} (expected <= {atom_end})",
247	);
248
249	let to_read = atom_end - position;
250	let mut pos = 0;
251	while pos < to_read {
252		let Some(next_atom) = reader.next()? else {
253			break;
254		};
255
256		if next_atom.len < 16 {
257			log::warn!(
258				"Expected data atom to be at least 16 bytes, got {}. Stopping",
259				next_atom.len
260			);
261			if parsing_mode == ParsingMode::Strict {
262				err!(BadAtom("Data atom is too small"))
263			}
264
265			break;
266		}
267
268		if next_atom.ident != DATA_ATOM_IDENT {
269			if parsing_mode == ParsingMode::Strict {
270				err!(BadAtom("Expected atom \"data\" to follow name"))
271			}
272
273			log::warn!(
274				"Skipping unexpected atom {actual_ident:?}, expected {expected_ident:?}",
275				actual_ident = next_atom.ident,
276				expected_ident = DATA_ATOM_IDENT
277			);
278
279			pos += next_atom.len;
280			skip_atom(reader, next_atom.extended, next_atom.len)?;
281			continue;
282		}
283
284		let Some(data_type) = parse_type_indicator(reader, parsing_mode)? else {
285			log::warn!("Skipping atom with unknown type set");
286			let remaining_atom_len = next_atom.len - (ATOM_HEADER_LEN + 1);
287
288			reader.seek(SeekFrom::Current(remaining_atom_len as i64))?;
289			pos += remaining_atom_len;
290			continue;
291		};
292
293		// We don't care about the locale
294		reader.seek(SeekFrom::Current(4))?;
295
296		let content_len = (next_atom.len - 16) as usize;
297		if content_len > 0 {
298			let mut content = try_vec![0; content_len];
299			reader.read_exact(&mut content)?;
300			ret.push((data_type, content));
301		} else {
302			log::warn!("Skipping empty \"data\" atom");
303		}
304
305		pos += next_atom.len;
306	}
307
308	let ret = if ret.is_empty() { None } else { Some(ret) };
309	Ok(ret)
310}
311
312fn parse_type_indicator<R>(
313	reader: &mut AtomReader<R>,
314	parsing_mode: ParsingMode,
315) -> Result<Option<DataType>>
316where
317	R: Read + Seek,
318{
319	// The type indicator is formed of four bytes split between two fields. The first byte indicates
320	// the set of types from which the type is drawn. The second through fourth byte forms the second
321	// field and its interpretation depends upon the value in the first field.
322
323	let type_set = reader.read_u8()?;
324	if type_set != WELL_KNOWN_TYPE_SET {
325		if parsing_mode == ParsingMode::Strict {
326			err!(BadAtom("Unknown type set in data atom"))
327		}
328
329		return Ok(None);
330	}
331
332	Ok(Some(DataType::from(reader.read_u24()?)))
333}
334
335fn parse_uint(bytes: &[u8]) -> Result<u32> {
336	Ok(match bytes.len() {
337		1 => u32::from(bytes[0]),
338		2 => u32::from(u16::from_be_bytes([bytes[0], bytes[1]])),
339		3 => u32::from_be_bytes([0, bytes[0], bytes[1], bytes[2]]),
340		4 => u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
341		_ => err!(BadAtom(
342			"Unexpected atom size for type \"BE unsigned integer\""
343		)),
344	})
345}
346
347fn parse_int(bytes: &[u8]) -> Result<i32> {
348	Ok(match bytes.len() {
349		1 => i32::from(bytes[0]),
350		2 => i32::from(i16::from_be_bytes([bytes[0], bytes[1]])),
351		3 => i32::from_be_bytes([0, bytes[0], bytes[1], bytes[2]]),
352		4 => i32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
353		_ => err!(BadAtom(
354			"Unexpected atom size for type \"BE signed integer\""
355		)),
356	})
357}
358
359fn handle_covr<R>(
360	reader: &mut AtomReader<R>,
361	parsing_mode: ParsingMode,
362	tag: &mut Ilst,
363	atom_info: &AtomInfo,
364) -> Result<()>
365where
366	R: Read + Seek,
367{
368	if let Some(atom_data) = parse_data_inner(reader, parsing_mode, atom_info)? {
369		let mut data = Vec::new();
370
371		let len = atom_data.len();
372		for (data_type, value) in atom_data {
373			let mime_type = match data_type {
374				// Type 0 is implicit
375				DataType::Reserved => None,
376				// GIF is deprecated
377				DataType::Gif => Some(MimeType::Gif),
378				DataType::Jpeg => Some(MimeType::Jpeg),
379				DataType::Png => Some(MimeType::Png),
380				DataType::Bmp => Some(MimeType::Bmp),
381				_ => {
382					if parsing_mode == ParsingMode::Strict {
383						err!(BadAtom("\"covr\" atom has an unknown type"))
384					}
385
386					log::warn!(
387						"Encountered \"covr\" atom with an unknown type of `{}`, discarding",
388						Into::<u32>::into(data_type)
389					);
390					return Ok(());
391				},
392			};
393
394			let picture_data = AtomData::Picture(Picture {
395				pic_type: PictureType::Other,
396				mime_type,
397				description: None,
398				data: Cow::from(value),
399			});
400
401			if len == 1 {
402				tag.atoms.push(Atom {
403					ident: AtomIdent::Fourcc(*b"covr"),
404					data: AtomDataStorage::Single(picture_data),
405				});
406
407				return Ok(());
408			}
409
410			data.push(picture_data);
411		}
412
413		tag.atoms.push(Atom {
414			ident: AtomIdent::Fourcc(*b"covr"),
415			data: AtomDataStorage::Multiple(data),
416		});
417	}
418
419	Ok(())
420}
421
422fn interpret_atom_content(flags: DataType, content: Vec<u8>) -> Result<AtomData> {
423	// https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW35
424	Ok(match flags {
425		DataType::Utf8 => AtomData::UTF8(utf8_decode(content)?),
426		DataType::Utf16 => AtomData::UTF16(utf16_decode_bytes(&content, u16::from_be_bytes)?),
427		DataType::BeSignedInteger => AtomData::SignedInteger(parse_int(&content)?),
428		DataType::BeUnsignedInteger => AtomData::UnsignedInteger(parse_uint(&content)?),
429		code => AtomData::Unknown {
430			code,
431			data: content,
432		},
433	})
434}