1use crate::{AudioFormat, AudioMetadata, EncodeOptions, Error, Result};
23
24pub(crate) mod id3;
25
26pub const MAX_METADATA_BYTES: usize = 0xff_ffff;
28pub const MAX_METADATA_FIELDS: usize = 4096;
30
31#[derive(Clone, Debug, Eq, PartialEq)]
33pub struct CoverArt {
34 pub mime_type: String,
35 pub data: Vec<u8>,
36}
37
38impl CoverArt {
39 pub fn new(mime_type: impl Into<String>, data: Vec<u8>) -> Result<Self> {
40 let cover = Self {
41 mime_type: mime_type.into(),
42 data,
43 };
44 cover.validate()?;
45 Ok(cover)
46 }
47
48 pub(crate) fn validate(&self) -> Result<()> {
49 if self.data.len() > MAX_METADATA_BYTES {
50 return Err(Error::limit("Cover exceeds metadata limit"));
51 }
52 let valid = match self.mime_type.as_str() {
53 "image/png" => self.data.starts_with(b"\x89PNG\r\n\x1a\n"),
54 "image/jpeg" => self.data.starts_with(b"\xff\xd8\xff"),
55 _ => false,
56 };
57 if !valid {
58 return Err(Error::encode(
59 "A cover needs matching image/jpeg or image/png bytes",
60 ));
61 }
62 Ok(())
63 }
64}
65
66#[derive(Clone, Debug, Default, Eq, PartialEq)]
70pub struct Tags {
71 pub title: Option<String>,
72 pub artist: Option<String>,
73 pub album: Option<String>,
74 pub album_artist: Option<String>,
75 pub track: Option<String>,
76 pub disc: Option<String>,
77 pub year: Option<String>,
78 pub genre: Option<String>,
79 pub comment: Option<String>,
80 pub isrc: Option<String>,
81 pub cover: Option<CoverArt>,
82}
83
84impl Tags {
85 pub fn is_empty(&self) -> bool {
86 self.cover.is_none() && self.fields().all(|(_, value)| value.is_none())
87 }
88
89 pub(crate) fn fields(&self) -> impl Iterator<Item = (&'static str, Option<&String>)> {
90 [
91 ("title", self.title.as_ref()),
92 ("artist", self.artist.as_ref()),
93 ("album", self.album.as_ref()),
94 ("album_artist", self.album_artist.as_ref()),
95 ("track", self.track.as_ref()),
96 ("disc", self.disc.as_ref()),
97 ("year", self.year.as_ref()),
98 ("genre", self.genre.as_ref()),
99 ("comment", self.comment.as_ref()),
100 ("isrc", self.isrc.as_ref()),
101 ]
102 .into_iter()
103 }
104
105 pub(crate) fn from_native(metadata: &AudioMetadata) -> Self {
106 let mut out = Self::default();
107 for (key, value) in metadata {
108 let field = match canonical(key) {
109 Some("title") => &mut out.title,
110 Some("artist") => &mut out.artist,
111 Some("album") => &mut out.album,
112 Some("album_artist") => &mut out.album_artist,
113 Some("track") => &mut out.track,
114 Some("disc") => &mut out.disc,
115 Some("year") => &mut out.year,
116 Some("genre") => &mut out.genre,
117 Some("comment") => &mut out.comment,
118 Some("isrc") => &mut out.isrc,
119 _ => continue,
120 };
121 *field = Some(value.clone());
122 }
123 out
124 }
125}
126
127pub(crate) fn canonical(key: &str) -> Option<&'static str> {
128 match key.to_ascii_lowercase().as_str() {
129 "title" | "inam" => Some("title"),
130 "artist" | "iart" => Some("artist"),
131 "album" | "iprd" => Some("album"),
132 "album_artist" | "albumartist" => Some("album_artist"),
133 "track" | "tracknumber" | "iprt" | "itrk" => Some("track"),
134 "disc" | "discnumber" => Some("disc"),
135 "year" | "date" | "icrd" => Some("year"),
136 "genre" | "ignr" => Some("genre"),
137 "comment" | "description" | "icmt" => Some("comment"),
138 "isrc" => Some("isrc"),
139 _ => None,
140 }
141}
142
143pub(crate) fn validate_options(options: &EncodeOptions, format: AudioFormat) -> Result<()> {
144 if options.wav_channel_mask.is_some() && format != AudioFormat::Wav {
145 return Err(Error::invalid("A WAVE channel mask requires WAV output"));
146 }
147 if format == AudioFormat::Wav
148 && (options.tags.album_artist.is_some()
149 || options.tags.disc.is_some()
150 || options.tags.isrc.is_some())
151 {
152 return Err(Error::encode(
153 "WAV INFO has no supported album artist, disc or ISRC field",
154 ));
155 }
156 if !options.tags.is_empty()
157 && !matches!(
158 format,
159 AudioFormat::Wav | AudioFormat::Flac | AudioFormat::Mp3
160 )
161 {
162 return Err(Error::encode(
163 "Structured tags are supported by WAV, FLAC and MP3",
164 ));
165 }
166 if options.tags.cover.is_some() && !matches!(format, AudioFormat::Flac | AudioFormat::Mp3) {
167 return Err(Error::encode("Front covers are supported by FLAC and MP3"));
168 }
169 let mut budget = Budget::default();
170 for (key, value) in &options.metadata {
171 if key.is_empty() {
172 return Err(Error::encode("Metadata keys cannot be empty"));
173 }
174 text(key)?;
175 text(value)?;
176 budget.field()?;
177 budget.add(key.len())?;
178 budget.add(value.len())?;
179 }
180 for (key, value) in options.tags.fields() {
181 if let Some(value) = value {
182 text(value)?;
183 budget.field()?;
184 budget.add(key.len())?;
185 budget.add(value.len())?;
186 if options
187 .metadata
188 .keys()
189 .any(|native| canonical(native) == Some(key))
190 {
191 return Err(Error::encode(format!(
192 "Tag {key} is supplied in both tags and metadata"
193 )));
194 }
195 }
196 }
197 if let Some(cover) = &options.tags.cover {
198 cover.validate()?;
199 budget.add(cover.data.len())?;
200 budget.add(cover.mime_type.len())?;
201 }
202 Ok(())
203}
204
205pub(crate) fn merged_text(options: &EncodeOptions) -> AudioMetadata {
206 let mut out = options.metadata.clone();
207 for (key, value) in options.tags.fields() {
208 if let Some(value) = value {
209 out.insert(key.into(), value.clone());
210 }
211 }
212 out
213}
214
215pub(crate) fn wav_text(options: &EncodeOptions) -> AudioMetadata {
216 let mut out = options.metadata.clone();
217 for (key, value) in options.tags.fields() {
218 let native = match key {
219 "title" => "INAM",
220 "artist" => "IART",
221 "album" => "IPRD",
222 "track" => "IPRT",
223 "year" => "ICRD",
224 "genre" => "IGNR",
225 "comment" => "ICMT",
226 _ => continue,
227 };
228 if let Some(value) = value {
229 out.insert(native.into(), value.clone());
230 }
231 }
232 out
233}
234
235fn text(value: &str) -> Result<()> {
236 if value.contains('\0') {
237 Err(Error::encode("Metadata text cannot contain NUL"))
238 } else {
239 Ok(())
240 }
241}
242
243pub(crate) fn utf8(bytes: &[u8]) -> Result<&str> {
244 std::str::from_utf8(bytes).map_err(|_| Error::decode("Invalid UTF-8 metadata"))
245}
246
247#[derive(Default)]
248pub(crate) struct Budget {
249 bytes: usize,
250 fields: usize,
251}
252impl Budget {
253 pub fn add(&mut self, bytes: usize) -> Result<()> {
254 self.bytes = self
255 .bytes
256 .checked_add(bytes)
257 .filter(|&n| n <= MAX_METADATA_BYTES)
258 .ok_or_else(|| Error::limit("Metadata exceeds 16 MiB minus one byte"))?;
259 Ok(())
260 }
261 pub fn field(&mut self) -> Result<()> {
262 self.fields += 1;
263 if self.fields > MAX_METADATA_FIELDS {
264 Err(Error::limit("Too many metadata fields"))
265 } else {
266 Ok(())
267 }
268 }
269}
270
271pub(crate) fn flac_comments(options: &EncodeOptions) -> Result<Option<Vec<u8>>> {
272 let mut metadata = options.metadata.clone();
273 for (key, value) in options.tags.fields() {
274 let name = match key {
275 "album_artist" => "albumartist",
276 "track" => "tracknumber",
277 "disc" => "discnumber",
278 "year" => "date",
279 _ => key,
280 };
281 if let Some(value) = value {
282 metadata.insert(name.into(), value.clone());
283 }
284 }
285 if metadata.is_empty() {
286 return Ok(None);
287 }
288 let mut budget = Budget::default();
289 budget.add(16)?; let mut normalized = AudioMetadata::new();
291 for (key, value) in metadata {
292 let name = key.to_ascii_uppercase();
293 if name.is_empty()
294 || !name
295 .bytes()
296 .all(|b| (0x20..=0x7d).contains(&b) && b != b'=')
297 {
298 return Err(Error::encode("Invalid FLAC metadata key"));
299 }
300 if normalized.contains_key(&name) {
301 return Err(Error::encode("Duplicate FLAC metadata alias"));
302 }
303 budget.field()?;
304 budget.add(5 + name.len())?;
305 budget.add(value.len())?;
306 normalized.insert(name, value);
307 }
308 let mut out = Vec::new();
309 crate::io::put32(&mut out, 8, true);
310 out.extend_from_slice(b"audiobox");
311 crate::io::put32(&mut out, normalized.len() as u32, true);
312 for (key, value) in normalized {
313 crate::io::put32(&mut out, (key.len() + 1 + value.len()) as u32, true);
314 out.extend_from_slice(key.as_bytes());
315 out.push(b'=');
316 out.extend_from_slice(value.as_bytes());
317 }
318 Ok(Some(out))
319}
320
321pub(crate) fn flac_picture(cover: &CoverArt) -> Result<Vec<u8>> {
322 cover.validate()?;
323 let size = 32 + cover.mime_type.len() + cover.data.len();
324 Budget::default().add(size)?;
325 let mut out = Vec::with_capacity(size);
326 crate::io::put32(&mut out, 3, false);
327 crate::io::put32(&mut out, cover.mime_type.len() as u32, false);
328 out.extend_from_slice(cover.mime_type.as_bytes());
329 for _ in 0..5 {
330 crate::io::put32(&mut out, 0, false);
331 } crate::io::put32(&mut out, cover.data.len() as u32, false);
333 out.extend_from_slice(&cover.data);
334 Ok(out)
335}
336
337pub(crate) fn read_flac_picture(bytes: &[u8]) -> Result<Option<CoverArt>> {
338 let mut r = crate::io::Reader::new(bytes);
339 let kind = r.u32(false)?;
340 let n = r.u32(false)? as usize;
341 let mime = utf8(r.take(n)?)?;
342 let n = r.u32(false)? as usize;
343 utf8(r.take(n)?)?;
344 r.take(16)?;
345 let n = r.u32(false)? as usize;
346 let data = r.take(n)?;
347 if r.pos != bytes.len() {
348 return Err(Error::decode("Trailing FLAC picture data"));
349 }
350 if kind != 3 || !matches!(mime, "image/jpeg" | "image/png") {
351 return Ok(None);
352 }
353 let cover = CoverArt {
354 mime_type: mime.into(),
355 data: data.to_vec(),
356 };
357 cover
358 .validate()
359 .map_err(|_| Error::decode("Invalid FLAC front cover"))?;
360 Ok(Some(cover))
361}