symphonia_metadata/id3v2/
mod.rs

1// Symphonia
2// Copyright (c) 2019-2022 The Project Symphonia Developers.
3//
4// This Source Code Form is subject to the terms of the Mozilla Public
5// License, v. 2.0. If a copy of the MPL was not distributed with this
6// file, You can obtain one at https://mozilla.org/MPL/2.0/.
7
8//! An ID3v2 metadata reader.
9
10use symphonia_core::errors::{decode_error, unsupported_error, Result};
11use symphonia_core::io::*;
12use symphonia_core::meta::{MetadataBuilder, MetadataOptions, MetadataReader, MetadataRevision};
13use symphonia_core::probe::{Descriptor, Instantiate, QueryDescriptor};
14use symphonia_core::support_metadata;
15
16use log::{info, trace, warn};
17
18mod frames;
19mod unsync;
20
21use frames::*;
22use unsync::{read_syncsafe_leq32, UnsyncStream};
23
24#[derive(Debug)]
25#[allow(clippy::enum_variant_names)]
26enum TagSizeRestriction {
27    Max128Frames1024KiB,
28    Max64Frames128KiB,
29    Max32Frames40KiB,
30    Max32Frames4KiB,
31}
32
33#[derive(Debug)]
34enum TextEncodingRestriction {
35    None,
36    Utf8OrIso88591,
37}
38
39#[derive(Debug)]
40enum TextFieldSize {
41    None,
42    Max1024Characters,
43    Max128Characters,
44    Max30Characters,
45}
46
47#[derive(Debug)]
48enum ImageEncodingRestriction {
49    None,
50    PngOrJpegOnly,
51}
52
53#[derive(Debug)]
54enum ImageSizeRestriction {
55    None,
56    LessThan256x256,
57    LessThan64x64,
58    Exactly64x64,
59}
60
61#[derive(Debug)]
62#[allow(dead_code)]
63struct Header {
64    major_version: u8,
65    minor_version: u8,
66    size: u32,
67    unsynchronisation: bool,
68    has_extended_header: bool,
69    experimental: bool,
70    has_footer: bool,
71}
72
73#[derive(Debug)]
74#[allow(dead_code)]
75struct Restrictions {
76    tag_size: TagSizeRestriction,
77    text_encoding: TextEncodingRestriction,
78    text_field_size: TextFieldSize,
79    image_encoding: ImageEncodingRestriction,
80    image_size: ImageSizeRestriction,
81}
82
83#[derive(Debug)]
84#[allow(dead_code)]
85struct ExtendedHeader {
86    /// ID3v2.3 only, the number of padding bytes.
87    padding_size: Option<u32>,
88    /// ID3v2.3+, a CRC32 checksum of the Tag.
89    crc32: Option<u32>,
90    /// ID3v2.4 only, is this Tag an update to an earlier Tag.
91    is_update: Option<bool>,
92    /// ID3v2.4 only, Tag modification restrictions.
93    restrictions: Option<Restrictions>,
94}
95
96/// Read the header of an ID3v2 (verions 2.2+) tag.
97fn read_id3v2_header<B: ReadBytes>(reader: &mut B) -> Result<Header> {
98    let marker = reader.read_triple_bytes()?;
99
100    if marker != *b"ID3" {
101        return unsupported_error("id3v2: not an ID3v2 tag");
102    }
103
104    let major_version = reader.read_u8()?;
105    let minor_version = reader.read_u8()?;
106    let flags = reader.read_u8()?;
107    let size = unsync::read_syncsafe_leq32(reader, 28)?;
108
109    let mut header = Header {
110        major_version,
111        minor_version,
112        size,
113        unsynchronisation: false,
114        has_extended_header: false,
115        experimental: false,
116        has_footer: false,
117    };
118
119    // Major and minor version numbers should never equal 0xff as per the specification.
120    if major_version == 0xff || minor_version == 0xff {
121        return decode_error("id3v2: invalid version number(s)");
122    }
123
124    // Only support versions 2.2.x (first version) to 2.4.x (latest version as of May 2019) of the
125    // specification.
126    if major_version < 2 || major_version > 4 {
127        return unsupported_error("id3v2: unsupported ID3v2 version");
128    }
129
130    // Version 2.2 of the standard specifies a compression flag bit, but does not specify a
131    // compression standard. Future versions of the standard remove this feature and repurpose this
132    // bit for other features. Since there is no way to know how to handle the remaining tag data,
133    // return an unsupported error.
134    if major_version == 2 && (flags & 0x40) != 0 {
135        return unsupported_error("id3v2: ID3v2.2 compression is not supported");
136    }
137
138    // With the exception of the compression flag in version 2.2, flags were added sequentially each
139    // major version. Check each bit sequentially as they appear in each version.
140    if major_version >= 2 {
141        header.unsynchronisation = flags & 0x80 != 0;
142    }
143
144    if major_version >= 3 {
145        header.has_extended_header = flags & 0x40 != 0;
146        header.experimental = flags & 0x20 != 0;
147    }
148
149    if major_version >= 4 {
150        header.has_footer = flags & 0x10 != 0;
151    }
152
153    Ok(header)
154}
155
156/// Read the extended header of an ID3v2.3 tag.
157fn read_id3v2p3_extended_header<B: ReadBytes>(reader: &mut B) -> Result<ExtendedHeader> {
158    let size = reader.read_be_u32()?;
159    let flags = reader.read_be_u16()?;
160    let padding_size = reader.read_be_u32()?;
161
162    if !(size == 6 || size == 10) {
163        return decode_error("id3v2: invalid extended header size");
164    }
165
166    let mut header = ExtendedHeader {
167        padding_size: Some(padding_size),
168        crc32: None,
169        is_update: None,
170        restrictions: None,
171    };
172
173    // CRC32 flag.
174    if size == 10 && flags & 0x8000 != 0 {
175        header.crc32 = Some(reader.read_be_u32()?);
176    }
177
178    Ok(header)
179}
180
181/// Read the extended header of an ID3v2.4 tag.
182fn read_id3v2p4_extended_header<B: ReadBytes>(reader: &mut B) -> Result<ExtendedHeader> {
183    let _size = read_syncsafe_leq32(reader, 28)?;
184
185    if reader.read_u8()? != 1 {
186        return decode_error("id3v2: extended flags should have a length of 1");
187    }
188
189    let flags = reader.read_u8()?;
190
191    let mut header = ExtendedHeader {
192        padding_size: None,
193        crc32: None,
194        is_update: Some(false),
195        restrictions: None,
196    };
197
198    // Tag is an update flag.
199    if flags & 0x40 != 0x0 {
200        let len = reader.read_u8()?;
201        if len != 1 {
202            return decode_error("id3v2: is update extended flag has invalid size");
203        }
204
205        header.is_update = Some(true);
206    }
207
208    // CRC32 flag.
209    if flags & 0x20 != 0x0 {
210        let len = reader.read_u8()?;
211        if len != 5 {
212            return decode_error("id3v2: CRC32 extended flag has invalid size");
213        }
214
215        header.crc32 = Some(read_syncsafe_leq32(reader, 32)?);
216    }
217
218    // Restrictions flag.
219    if flags & 0x10 != 0x0 {
220        let len = reader.read_u8()?;
221        if len != 1 {
222            return decode_error("id3v2: restrictions extended flag has invalid size");
223        }
224
225        let restrictions = reader.read_u8()?;
226
227        let tag_size = match (restrictions & 0xc0) >> 6 {
228            0 => TagSizeRestriction::Max128Frames1024KiB,
229            1 => TagSizeRestriction::Max64Frames128KiB,
230            2 => TagSizeRestriction::Max32Frames40KiB,
231            3 => TagSizeRestriction::Max32Frames4KiB,
232            _ => unreachable!(),
233        };
234
235        let text_encoding = match (restrictions & 0x40) >> 5 {
236            0 => TextEncodingRestriction::None,
237            1 => TextEncodingRestriction::Utf8OrIso88591,
238            _ => unreachable!(),
239        };
240
241        let text_field_size = match (restrictions & 0x18) >> 3 {
242            0 => TextFieldSize::None,
243            1 => TextFieldSize::Max1024Characters,
244            2 => TextFieldSize::Max128Characters,
245            3 => TextFieldSize::Max30Characters,
246            _ => unreachable!(),
247        };
248
249        let image_encoding = match (restrictions & 0x04) >> 2 {
250            0 => ImageEncodingRestriction::None,
251            1 => ImageEncodingRestriction::PngOrJpegOnly,
252            _ => unreachable!(),
253        };
254
255        let image_size = match restrictions & 0x03 {
256            0 => ImageSizeRestriction::None,
257            1 => ImageSizeRestriction::LessThan256x256,
258            2 => ImageSizeRestriction::LessThan64x64,
259            3 => ImageSizeRestriction::Exactly64x64,
260            _ => unreachable!(),
261        };
262
263        header.restrictions = Some(Restrictions {
264            tag_size,
265            text_encoding,
266            text_field_size,
267            image_encoding,
268            image_size,
269        })
270    }
271
272    Ok(header)
273}
274
275fn read_id3v2_body<B: ReadBytes + FiniteStream>(
276    reader: &mut B,
277    header: &Header,
278    metadata: &mut MetadataBuilder,
279) -> Result<()> {
280    // If there is an extended header, read and parse it based on the major version of the tag.
281    if header.has_extended_header {
282        let extended = match header.major_version {
283            3 => read_id3v2p3_extended_header(reader)?,
284            4 => read_id3v2p4_extended_header(reader)?,
285            _ => unreachable!(),
286        };
287        trace!("{:#?}", &extended);
288    }
289
290    let min_frame_size = match header.major_version {
291        2 => 6,
292        3 | 4 => 10,
293        _ => unreachable!(),
294    };
295
296    loop {
297        // Read frames based on the major version of the tag.
298        let frame = match header.major_version {
299            2 => read_id3v2p2_frame(reader),
300            3 => read_id3v2p3_frame(reader),
301            4 => read_id3v2p4_frame(reader),
302            _ => break,
303        }?;
304
305        match frame {
306            // The padding has been reached, don't parse any further.
307            FrameResult::Padding => break,
308            // A frame was parsed into a tag, add it to the tag collection.
309            FrameResult::Tag(tag) => {
310                metadata.add_tag(tag);
311            }
312            // A frame was parsed into multiple tags, add them all to the tag collection.
313            FrameResult::MultipleTags(multi_tags) => {
314                for tag in multi_tags {
315                    metadata.add_tag(tag);
316                }
317            }
318            // A frame was parsed into a visual, add it to the visual collection.
319            FrameResult::Visual(visual) => {
320                metadata.add_visual(visual);
321            }
322            // An unknown frame was encountered.
323            FrameResult::UnsupportedFrame(ref id) => {
324                info!("unsupported frame {}", id);
325            }
326            // The frame contained invalid data.
327            FrameResult::InvalidData(ref id) => {
328                warn!("invalid data for {} frame", id);
329            }
330        }
331
332        // Read frames until there is not enough bytes available in the ID3v2 tag for another frame.
333        if reader.bytes_available() < min_frame_size {
334            break;
335        }
336    }
337
338    Ok(())
339}
340
341pub fn read_id3v2<B: ReadBytes>(reader: &mut B, metadata: &mut MetadataBuilder) -> Result<()> {
342    // Read the (sorta) version agnostic tag header.
343    let header = read_id3v2_header(reader)?;
344
345    // If the unsynchronisation flag is set in the header, all tag data must be passed through the
346    // unsynchronisation decoder before being read for verions < 4 of ID3v2.
347    let mut scoped = if header.unsynchronisation && header.major_version < 4 {
348        let mut unsync = UnsyncStream::new(ScopedStream::new(reader, u64::from(header.size)));
349
350        read_id3v2_body(&mut unsync, &header, metadata)?;
351
352        unsync.into_inner()
353    }
354    // Otherwise, read the data as-is. Individual frames may be unsynchronised for major versions
355    // >= 4.
356    else {
357        let mut scoped = ScopedStream::new(reader, u64::from(header.size));
358
359        read_id3v2_body(&mut scoped, &header, metadata)?;
360
361        scoped
362    };
363
364    // Ignore any remaining data in the tag.
365    scoped.ignore()?;
366
367    Ok(())
368}
369
370pub mod util {
371    use symphonia_core::meta::StandardVisualKey;
372
373    /// Try to get a `StandardVisualKey` from the APIC picture type identifier.
374    pub fn apic_picture_type_to_visual_key(apic: u32) -> Option<StandardVisualKey> {
375        match apic {
376            0x01 => Some(StandardVisualKey::FileIcon),
377            0x02 => Some(StandardVisualKey::OtherIcon),
378            0x03 => Some(StandardVisualKey::FrontCover),
379            0x04 => Some(StandardVisualKey::BackCover),
380            0x05 => Some(StandardVisualKey::Leaflet),
381            0x06 => Some(StandardVisualKey::Media),
382            0x07 => Some(StandardVisualKey::LeadArtistPerformerSoloist),
383            0x08 => Some(StandardVisualKey::ArtistPerformer),
384            0x09 => Some(StandardVisualKey::Conductor),
385            0x0a => Some(StandardVisualKey::BandOrchestra),
386            0x0b => Some(StandardVisualKey::Composer),
387            0x0c => Some(StandardVisualKey::Lyricist),
388            0x0d => Some(StandardVisualKey::RecordingLocation),
389            0x0e => Some(StandardVisualKey::RecordingSession),
390            0x0f => Some(StandardVisualKey::Performance),
391            0x10 => Some(StandardVisualKey::ScreenCapture),
392            0x12 => Some(StandardVisualKey::Illustration),
393            0x13 => Some(StandardVisualKey::BandArtistLogo),
394            0x14 => Some(StandardVisualKey::PublisherStudioLogo),
395            _ => None,
396        }
397    }
398}
399
400pub struct Id3v2Reader;
401
402impl QueryDescriptor for Id3v2Reader {
403    fn query() -> &'static [Descriptor] {
404        &[support_metadata!("id3v2", "ID3v2", &[], &[], &[b"ID3"])]
405    }
406
407    fn score(_context: &[u8]) -> u8 {
408        255
409    }
410}
411
412impl MetadataReader for Id3v2Reader {
413    fn new(_options: &MetadataOptions) -> Self {
414        Id3v2Reader {}
415    }
416
417    fn read_all(&mut self, reader: &mut MediaSourceStream) -> Result<MetadataRevision> {
418        let mut builder = MetadataBuilder::new();
419        read_id3v2(reader, &mut builder)?;
420        Ok(builder.metadata())
421    }
422}