lofty/id3/
mod.rs

1//! ID3 specific items
2//!
3//! ID3 does things differently than other tags, making working with them a little more effort than other formats.
4//! Check the other modules for important notes and/or warnings.
5
6pub mod v1;
7pub mod v2;
8
9use crate::error::{ErrorKind, LoftyError, Result};
10use crate::macros::try_vec;
11use crate::util::text::utf8_decode_str;
12use v2::header::Id3v2Header;
13
14use std::io::{Read, Seek, SeekFrom};
15use std::ops::Neg;
16
17pub(crate) struct ID3FindResults<Header, Content>(pub Option<Header>, pub Content);
18
19pub(crate) fn find_lyrics3v2<R>(data: &mut R) -> Result<ID3FindResults<(), u32>>
20where
21	R: Read + Seek,
22{
23	log::debug!("Searching for a Lyrics3v2 tag");
24
25	let mut header = None;
26	let mut size = 0_u32;
27
28	data.seek(SeekFrom::Current(-15))?;
29
30	let mut lyrics3v2 = [0; 15];
31	data.read_exact(&mut lyrics3v2)?;
32
33	if &lyrics3v2[7..] == b"LYRICS200" {
34		log::warn!("Encountered a Lyrics3v2 tag. This is an outdated format, and will be skipped.");
35
36		header = Some(());
37
38		let lyrics_size = utf8_decode_str(&lyrics3v2[..7])?;
39		let lyrics_size = lyrics_size.parse::<u32>().map_err(|_| {
40			LoftyError::new(ErrorKind::TextDecode(
41				"Lyrics3v2 tag has an invalid size string",
42			))
43		})?;
44
45		size += lyrics_size;
46
47		data.seek(SeekFrom::Current(i64::from(lyrics_size + 15).neg()))?;
48	}
49
50	Ok(ID3FindResults(header, size))
51}
52
53#[allow(unused_variables)]
54pub(crate) fn find_id3v1<R>(
55	data: &mut R,
56	read: bool,
57) -> Result<ID3FindResults<(), Option<v1::tag::Id3v1Tag>>>
58where
59	R: Read + Seek,
60{
61	log::debug!("Searching for an ID3v1 tag");
62
63	let mut id3v1 = None;
64	let mut header = None;
65
66	// Reader is too small to contain an ID3v2 tag
67	if data.seek(SeekFrom::End(-128)).is_err() {
68		data.seek(SeekFrom::End(0))?;
69		return Ok(ID3FindResults(header, id3v1));
70	}
71
72	let mut id3v1_header = [0; 3];
73	data.read_exact(&mut id3v1_header)?;
74
75	data.seek(SeekFrom::Current(-3))?;
76
77	// No ID3v1 tag found
78	if &id3v1_header != b"TAG" {
79		data.seek(SeekFrom::End(0))?;
80		return Ok(ID3FindResults(header, id3v1));
81	}
82
83	log::debug!("Found an ID3v1 tag, parsing");
84
85	header = Some(());
86
87	if read {
88		let mut id3v1_tag = [0; 128];
89		data.read_exact(&mut id3v1_tag)?;
90
91		data.seek(SeekFrom::End(-128))?;
92
93		id3v1 = Some(v1::read::parse_id3v1(id3v1_tag))
94	}
95
96	Ok(ID3FindResults(header, id3v1))
97}
98
99#[derive(Copy, Clone, Debug)]
100pub(crate) struct FindId3v2Config {
101	pub(crate) read: bool,
102	pub(crate) allowed_junk_window: Option<u64>,
103}
104
105impl FindId3v2Config {
106	pub(crate) const NO_READ_TAG: Self = Self {
107		read: false,
108		allowed_junk_window: None,
109	};
110
111	pub(crate) const READ_TAG: Self = Self {
112		read: true,
113		allowed_junk_window: None,
114	};
115}
116
117pub(crate) fn find_id3v2<R>(
118	data: &mut R,
119	config: FindId3v2Config,
120) -> Result<ID3FindResults<Id3v2Header, Option<Vec<u8>>>>
121where
122	R: Read + Seek,
123{
124	log::debug!(
125		"Searching for an ID3v2 tag at offset: {}",
126		data.stream_position()?
127	);
128
129	let mut header = None;
130	let mut id3v2 = None;
131
132	if let Some(junk_window) = config.allowed_junk_window {
133		let mut id3v2_search_window = data.by_ref().take(junk_window);
134
135		let Some(id3v2_offset) = find_id3v2_in_junk(&mut id3v2_search_window)? else {
136			return Ok(ID3FindResults(None, None));
137		};
138
139		log::warn!(
140			"Found an ID3v2 tag preceded by junk data, offset: {}",
141			id3v2_offset
142		);
143
144		data.seek(SeekFrom::Current(-3))?;
145	}
146
147	if let Ok(id3v2_header) = Id3v2Header::parse(data) {
148		log::debug!("Found an ID3v2 tag, parsing");
149
150		if config.read {
151			let mut tag = try_vec![0; id3v2_header.size as usize];
152			data.read_exact(&mut tag)?;
153
154			id3v2 = Some(tag)
155		} else {
156			data.seek(SeekFrom::Current(i64::from(id3v2_header.size)))?;
157		}
158
159		if id3v2_header.flags.footer {
160			data.seek(SeekFrom::Current(10))?;
161		}
162
163		header = Some(id3v2_header);
164	} else {
165		data.seek(SeekFrom::Current(-10))?;
166	}
167
168	Ok(ID3FindResults(header, id3v2))
169}
170
171/// Searches for an ID3v2 tag in (potential) junk data between the start
172/// of the file and the first frame
173fn find_id3v2_in_junk<R>(reader: &mut R) -> Result<Option<u64>>
174where
175	R: Read,
176{
177	let bytes = reader.bytes();
178
179	let mut id3v2_header = [0; 3];
180
181	for (index, byte) in bytes.enumerate() {
182		id3v2_header[0] = id3v2_header[1];
183		id3v2_header[1] = id3v2_header[2];
184		id3v2_header[2] = byte?;
185		if id3v2_header == *b"ID3" {
186			return Ok(Some((index - 2) as u64));
187		}
188	}
189
190	Ok(None)
191}