lofty 0.25.4

Audio metadata library
Documentation
use crate::error::FakeTagError;
use crate::id3::v2::error::{Id3v2HeaderError, Id3v2ParseError};
use crate::id3::v2::restrictions::TagRestrictions;
use crate::id3::v2::util::synchsafe::{SynchsafeInteger, UnsynchronizedStream};

use std::fmt::Display;
use std::io::Read;

use byteorder::{BigEndian, ByteOrder, ReadBytesExt};

/// The ID3v2 version
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Id3v2Version {
	/// ID3v2.2
	V2,
	/// ID3v2.3
	V3,
	/// ID3v2.4
	V4,
}

impl Display for Id3v2Version {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		match self {
			Id3v2Version::V2 => write!(f, "ID3v2.2"),
			Id3v2Version::V3 => write!(f, "ID3v2.3"),
			Id3v2Version::V4 => write!(f, "ID3v2.4"),
		}
	}
}

/// Flags that apply to the entire tag
#[derive(Default, Copy, Clone, Debug, PartialEq, Eq)]
#[allow(clippy::struct_excessive_bools)]
pub struct Id3v2TagFlags {
	/// Whether or not all frames are unsynchronised. See [`FrameFlags::unsynchronisation`](crate::id3::v2::FrameFlags::unsynchronisation)
	pub unsynchronisation: bool,
	/// Indicates if the tag is in an experimental stage
	pub experimental: bool,
	/// Indicates that the tag includes a footer
	///
	/// A footer will be created if the tag is written
	pub footer: bool,
	/// Whether or not to include a CRC-32 in the extended header
	///
	/// This is calculated if the tag is written
	pub crc: bool,
	/// Restrictions on the tag, written in the extended header
	///
	/// In addition to being setting this flag, all restrictions must be provided. See [`TagRestrictions`]
	pub restrictions: Option<TagRestrictions>,
}

impl Id3v2TagFlags {
	/// Get the **ID3v2.4** byte representation of the flags
	///
	/// NOTE: This does not include the extended header flags
	pub fn as_id3v24_byte(&self) -> u8 {
		let mut byte = 0;

		if self.unsynchronisation {
			byte |= 0x80;
		}

		if self.experimental {
			byte |= 0x20;
		}

		if self.footer {
			byte |= 0x10;
		}

		byte
	}

	/// Get the **ID3v2.3** byte representation of the flags
	///
	/// NOTE: This does not include the extended header flags
	pub fn as_id3v23_byte(&self) -> u8 {
		let mut byte = 0;

		if self.experimental {
			byte |= 0x40;
		}

		if self.footer {
			byte |= 0x10;
		}

		byte
	}
}

#[derive(Copy, Clone, Debug)]
pub(crate) struct Id3v2Header {
	pub version: Id3v2Version,
	pub flags: Id3v2TagFlags,
	/// The size of the tag contents (**DOES NOT INCLUDE THE HEADER/FOOTER**)
	pub size: u32,
	pub extended_size: u32,
}

impl Id3v2Header {
	pub(crate) fn parse<R>(bytes: &mut R) -> Result<Self, Id3v2ParseError>
	where
		R: Read,
	{
		log::debug!("Parsing ID3v2 header");

		let mut header = [0; 10];
		bytes.read_exact(&mut header)?;

		if &header[..3] != b"ID3" {
			return Err(FakeTagError.into());
		}

		// Version is stored as [major, minor], but here we don't care about minor revisions unless there's an error.
		let version = match header[3] {
			2 => Id3v2Version::V2,
			3 => Id3v2Version::V3,
			4 => Id3v2Version::V4,
			major => {
				return Err(Id3v2HeaderError::BadId3v2Version(major, header[4]).into());
			},
		};

		let flags = header[5];

		// Compression was a flag only used in ID3v2.2 (bit 2).
		// At the time the ID3v2.2 specification was written, a compression scheme wasn't decided.
		// The spec recommends just ignoring the tag in this case.
		if version == Id3v2Version::V2 && flags & 0x40 == 0x40 {
			return Err(Id3v2HeaderError::V2Compression.into());
		}

		let mut flags_parsed = Id3v2TagFlags {
			unsynchronisation: flags & 0x80 == 0x80,
			experimental: (version == Id3v2Version::V4 || version == Id3v2Version::V3)
				&& flags & 0x20 == 0x20,
			footer: (version == Id3v2Version::V4 || version == Id3v2Version::V3)
				&& flags & 0x10 == 0x10,
			crc: false,         // Retrieved later if applicable
			restrictions: None, // Retrieved later if applicable
		};

		let size = BigEndian::read_u32(&header[6..]).unsynch();
		let mut extended_size = 0;

		let extended_header =
			(version == Id3v2Version::V4 || version == Id3v2Version::V3) && flags & 0x40 == 0x40;

		if extended_header {
			match version {
				// In ID3v2.3, the extended header is considered separate from the frame header, and
				// thus subject to unsynchronization
				Id3v2Version::V3 => {
					if flags_parsed.unsynchronisation {
						let mut reader = UnsynchronizedStream::new(bytes);
						extended_size = reader.read_u32::<BigEndian>()?;
						read_extended_header(&mut reader, &mut flags_parsed, extended_size)?;
					} else {
						extended_size = bytes.read_u32::<BigEndian>()?;
						read_extended_header(bytes, &mut flags_parsed, extended_size)?;
					}
				},
				// In ID3v2.4, they seem to be considered one big header (?) so no flags apply to it
				Id3v2Version::V4 => {
					extended_size = bytes.read_u32::<BigEndian>()?.unsynch();
					read_extended_header(bytes, &mut flags_parsed, extended_size)?;
				},
				_ => unreachable!(),
			}
		}

		if extended_size > 0 && extended_size >= size {
			return Err(Id3v2HeaderError::BadExtendedHeaderSize.into());
		}

		Ok(Id3v2Header {
			version,
			flags: flags_parsed,
			size,
			extended_size,
		})
	}

	/// The total size of the tag, including the header, footer, and extended header
	pub(crate) fn full_tag_size(&self) -> u32 {
		self.size + 10 + self.extended_size + if self.flags.footer { 10 } else { 0 }
	}
}

fn read_extended_header<R: Read>(
	reader: &mut R,
	flags: &mut Id3v2TagFlags,
	header_size: u32,
) -> Result<(), Id3v2ParseError> {
	if header_size < 6 {
		return Err(Id3v2HeaderError::BadExtendedHeaderSize.into());
	}

	// Useless byte since there's only 1 byte for flags
	let _num_flag_bytes = reader.read_u8()?;

	let extended_flags = reader.read_u8()?;

	// The only flags we care about here are the CRC and restrictions

	if extended_flags & 0x20 == 0x20 {
		flags.crc = true;

		// We don't care about the existing CRC (5) or its length byte (1)
		let mut crc = [0; 6];
		reader.read_exact(&mut crc)?;
	}

	if extended_flags & 0x10 == 0x10 {
		// We don't care about the length byte, it is always 1
		let _data_length = reader.read_u8()?;

		flags.restrictions = Some(TagRestrictions::from_byte(reader.read_u8()?));
	}

	Ok(())
}