1pub 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 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 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
171fn 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}