,
{
let ext = ext.as_ref().to_str()?.to_ascii_lowercase();
// Give custom resolvers priority
if unsafe { global_options().use_custom_resolvers } {
if let Some((ty, _)) = custom_resolvers()
.lock()
.ok()?
.iter()
.find(|(_, f)| f.extension() == Some(ext.as_str()))
{
return Some(Self::Custom(ty));
}
}
match ext.as_str() {
"aac" => Some(Self::Aac),
"ape" => Some(Self::Ape),
"aiff" | "aif" | "afc" | "aifc" => Some(Self::Aiff),
"mp3" | "mp2" | "mp1" => Some(Self::Mpeg),
"wav" | "wave" => Some(Self::Wav),
"wv" => Some(Self::WavPack),
"opus" => Some(Self::Opus),
"flac" => Some(Self::Flac),
"ogg" => Some(Self::Vorbis),
"mp4" | "m4a" | "m4b" | "m4p" | "m4r" | "m4v" | "3gp" => Some(Self::Mp4),
"mpc" | "mp+" | "mpp" => Some(Self::Mpc),
"spx" => Some(Self::Speex),
_ => None,
}
}
/// Attempts to determine a [`FileType`] from a path
///
/// # Examples
///
/// ```rust
/// use lofty::file::FileType;
/// use std::path::Path;
///
/// let path = Path::new("path/to/my.mp3");
/// assert_eq!(FileType::from_path(path), Some(FileType::Mpeg));
/// ```
pub fn from_path(path: P) -> Option
where
P: AsRef,
{
let ext = path.as_ref().extension();
ext.and_then(Self::from_ext)
}
/// Attempts to extract a [`FileType`] from a buffer
///
/// NOTES:
///
/// * This is for use in [`Probe::guess_file_type`], it is recommended to use it that way
/// * This **will not** search past tags at the start of the buffer.
/// For this behavior, use [`Probe::guess_file_type`].
///
/// [`Probe::guess_file_type`]: crate::probe::Probe::guess_file_type
///
/// # Examples
///
/// ```rust
/// use lofty::file::FileType;
/// use std::fs::File;
/// use std::io::Read;
///
/// # fn main() -> lofty::error::Result<()> {
/// # let path_to_opus = "tests/files/assets/minimal/full_test.opus";
/// let mut file = File::open(path_to_opus)?;
///
/// let mut buf = [0; 50]; // Search the first 50 bytes of the file
/// file.read_exact(&mut buf)?;
///
/// assert_eq!(FileType::from_buffer(&buf), Some(FileType::Opus));
/// # Ok(()) }
/// ```
pub fn from_buffer(buf: &[u8]) -> Option {
match Self::from_buffer_inner(buf) {
Some(FileTypeGuessResult::Determined(file_ty)) => Some(file_ty),
// We make no attempt to search past an ID3v2 tag or junk here, since
// we only provided a fixed-sized buffer to search from.
//
// That case is handled in `Probe::guess_file_type`
_ => None,
}
}
// TODO: APE tags in the beginning of the file
pub(crate) fn from_buffer_inner(buf: &[u8]) -> Option {
use crate::id3::v2::util::synchsafe::SynchsafeInteger;
// Start out with an empty return
let mut ret = None;
if buf.is_empty() {
return ret;
}
match Self::quick_type_guess(buf) {
Some(f_ty) => ret = Some(FileTypeGuessResult::Determined(f_ty)),
// Special case for ID3, gets checked in `Probe::guess_file_type`
// The bare minimum size for an ID3v2 header is 10 bytes
None if buf.len() >= 10 && &buf[..3] == b"ID3" => {
// This is infallible, but preferable to an unwrap
if let Ok(arr) = buf[6..10].try_into() {
// Set the ID3v2 size
ret = Some(FileTypeGuessResult::MaybePrecededById3(
u32::from_be_bytes(arr).unsynch(),
));
}
},
None => ret = Some(FileTypeGuessResult::MaybePrecededByJunk),
}
ret
}
fn quick_type_guess(buf: &[u8]) -> Option {
use crate::mpeg::header::verify_frame_sync;
// Safe to index, since we return early on an empty buffer
match buf[0] {
77 if buf.starts_with(b"MAC") => Some(Self::Ape),
255 if buf.len() >= 2 && verify_frame_sync([buf[0], buf[1]]) => {
// ADTS and MPEG frame headers are way too similar
// ADTS (https://wiki.multimedia.cx/index.php/ADTS#Header):
//
// AAAAAAAA AAAABCCX
//
// Letter Length (bits) Description
// A 12 Syncword, all bits must be set to 1.
// B 1 MPEG Version, set to 0 for MPEG-4 and 1 for MPEG-2.
// C 2 Layer, always set to 0.
// MPEG (http://www.mp3-tech.org/programmer/frame_header.html):
//
// AAAAAAAA AAABBCCX
//
// Letter Length (bits) Description
// A 11 Syncword, all bits must be set to 1.
// B 2 MPEG Audio version ID
// C 2 Layer description
// The subtle overlap in the ADTS header's frame sync and MPEG's version ID
// is the first condition to check. However, since 0b10 and 0b11 are valid versions
// in MPEG, we have to also check the layer.
// So, if we have a version 1 (0b11) or version 2 (0b10) MPEG frame AND a layer of 0b00,
// we can assume we have an ADTS header. Awesome!
if buf[1] & 0b10000 > 0 && buf[1] & 0b110 == 0 {
return Some(Self::Aac);
}
Some(Self::Mpeg)
},
70 if buf.len() >= 12 && &buf[..4] == b"FORM" => {
let id = &buf[8..12];
if id == b"AIFF" || id == b"AIFC" {
return Some(Self::Aiff);
}
None
},
79 if buf.len() >= 36 && &buf[..4] == b"OggS" => {
if &buf[29..35] == b"vorbis" {
return Some(Self::Vorbis);
} else if &buf[28..36] == b"OpusHead" {
return Some(Self::Opus);
} else if &buf[28..36] == b"Speex " {
return Some(Self::Speex);
}
None
},
102 if buf.starts_with(b"fLaC") => Some(Self::Flac),
82 if buf.len() >= 12 && &buf[..4] == b"RIFF" => {
if &buf[8..12] == b"WAVE" {
return Some(Self::Wav);
}
None
},
119 if buf.len() >= 4 && &buf[..4] == b"wvpk" => Some(Self::WavPack),
_ if buf.len() >= 8 && &buf[4..8] == b"ftyp" => Some(Self::Mp4),
_ if buf.starts_with(b"MPCK") || buf.starts_with(b"MP+") => Some(Self::Mpc),
_ => None,
}
}
}
/// The result of a `FileType` guess
///
/// External callers of `FileType::from_buffer()` will only ever see `Determined` cases.
/// The remaining cases are used internally in `Probe::guess_file_type()`.
pub(crate) enum FileTypeGuessResult {
/// The `FileType` was guessed
Determined(FileType),
/// The stream starts with an ID3v2 tag
MaybePrecededById3(u32),
/// The stream starts with potential junk data
MaybePrecededByJunk,
}
lofty-0.21.1/src/file/mod.rs 0000644 0000000 0000000 00000000405 10461020230 0013674 0 ustar 0000000 0000000 //! Generic file handling utilities
mod audio_file;
mod file_type;
mod tagged_file;
pub use audio_file::AudioFile;
pub use file_type::FileType;
pub use tagged_file::{BoundTaggedFile, TaggedFile, TaggedFileExt};
pub(crate) use file_type::FileTypeGuessResult;
lofty-0.21.1/src/file/tagged_file.rs 0000644 0000000 0000000 00000043225 10461020230 0015356 0 ustar 0000000 0000000 use super::audio_file::AudioFile;
use super::file_type::FileType;
use crate::config::{ParseOptions, WriteOptions};
use crate::error::{LoftyError, Result};
use crate::properties::FileProperties;
use crate::tag::{Tag, TagExt, TagType};
use crate::util::io::{FileLike, Length, Truncate};
use std::fs::File;
use std::io::{Read, Seek};
/// Provides a common interface between [`TaggedFile`] and [`BoundTaggedFile`]
pub trait TaggedFileExt {
/// Returns the file's [`FileType`]
///
/// # Examples
///
/// ```rust
/// use lofty::file::{FileType, TaggedFileExt};
///
/// # fn main() -> lofty::error::Result<()> {
/// # let path_to_mp3 = "tests/files/assets/minimal/full_test.mp3";
/// let mut tagged_file = lofty::read_from_path(path_to_mp3)?;
///
/// assert_eq!(tagged_file.file_type(), FileType::Mpeg);
/// # Ok(()) }
/// ```
fn file_type(&self) -> FileType;
/// Returns all tags
///
/// # Examples
///
/// ```rust
/// use lofty::file::{FileType, TaggedFileExt};
///
/// # fn main() -> lofty::error::Result<()> {
/// # let path_to_mp3 = "tests/files/assets/minimal/full_test.mp3";
/// // An MP3 file with 3 tags
/// let mut tagged_file = lofty::read_from_path(path_to_mp3)?;
///
/// let tags = tagged_file.tags();
///
/// assert_eq!(tags.len(), 3);
/// # Ok(()) }
/// ```
fn tags(&self) -> &[Tag];
/// Returns the file type's primary [`TagType`]
///
/// See [`FileType::primary_tag_type`]
///
/// # Examples
///
/// ```rust
/// use lofty::file::TaggedFileExt;
/// use lofty::tag::TagType;
///
/// # fn main() -> lofty::error::Result<()> {
/// # let path_to_mp3 = "tests/files/assets/minimal/full_test.mp3";
/// let mut tagged_file = lofty::read_from_path(path_to_mp3)?;
///
/// assert_eq!(tagged_file.primary_tag_type(), TagType::Id3v2);
/// # Ok(()) }
/// ```
fn primary_tag_type(&self) -> TagType {
self.file_type().primary_tag_type()
}
/// Determines whether the file supports the given [`TagType`]
///
/// # Examples
///
/// ```rust
/// use lofty::file::TaggedFileExt;
/// use lofty::tag::TagType;
///
/// # fn main() -> lofty::error::Result<()> {
/// # let path_to_mp3 = "tests/files/assets/minimal/full_test.mp3";
/// let mut tagged_file = lofty::read_from_path(path_to_mp3)?;
///
/// assert!(tagged_file.supports_tag_type(TagType::Id3v2));
/// # Ok(()) }
/// ```
fn supports_tag_type(&self, tag_type: TagType) -> bool {
self.file_type().supports_tag_type(tag_type)
}
/// Get a reference to a specific [`TagType`]
///
/// # Examples
///
/// ```rust
/// use lofty::file::TaggedFileExt;
/// use lofty::tag::TagType;
///
/// # fn main() -> lofty::error::Result<()> {
/// # let path_to_mp3 = "tests/files/assets/minimal/full_test.mp3";
/// // Read an MP3 file with an ID3v2 tag
/// let mut tagged_file = lofty::read_from_path(path_to_mp3)?;
///
/// // An ID3v2 tag
/// let tag = tagged_file.tag(TagType::Id3v2);
///
/// assert!(tag.is_some());
/// assert_eq!(tag.unwrap().tag_type(), TagType::Id3v2);
/// # Ok(()) }
/// ```
fn tag(&self, tag_type: TagType) -> Option<&Tag>;
/// Get a mutable reference to a specific [`TagType`]
///
/// # Examples
///
/// ```rust
/// use lofty::file::TaggedFileExt;
/// use lofty::tag::TagType;
///
/// # fn main() -> lofty::error::Result<()> {
/// # let path_to_mp3 = "tests/files/assets/minimal/full_test.mp3";
/// // Read an MP3 file with an ID3v2 tag
/// let mut tagged_file = lofty::read_from_path(path_to_mp3)?;
///
/// // An ID3v2 tag
/// let tag = tagged_file.tag(TagType::Id3v2);
///
/// assert!(tag.is_some());
/// assert_eq!(tag.unwrap().tag_type(), TagType::Id3v2);
///
/// // Alter the tag...
/// # Ok(()) }
/// ```
fn tag_mut(&mut self, tag_type: TagType) -> Option<&mut Tag>;
/// Returns the primary tag
///
/// See [`FileType::primary_tag_type`]
///
/// # Examples
///
/// ```rust
/// use lofty::file::TaggedFileExt;
/// use lofty::tag::TagType;
///
/// # fn main() -> lofty::error::Result<()> {
/// # let path_to_mp3 = "tests/files/assets/minimal/full_test.mp3";
/// // Read an MP3 file with an ID3v2 tag
/// let mut tagged_file = lofty::read_from_path(path_to_mp3)?;
///
/// // An ID3v2 tag
/// let tag = tagged_file.primary_tag();
///
/// assert!(tag.is_some());
/// assert_eq!(tag.unwrap().tag_type(), TagType::Id3v2);
/// # Ok(()) }
/// ```
fn primary_tag(&self) -> Option<&Tag> {
self.tag(self.primary_tag_type())
}
/// Gets a mutable reference to the file's "Primary tag"
///
/// See [`FileType::primary_tag_type`]
///
/// # Examples
///
/// ```rust
/// use lofty::file::TaggedFileExt;
/// use lofty::tag::TagType;
///
/// # fn main() -> lofty::error::Result<()> {
/// # let path_to_mp3 = "tests/files/assets/minimal/full_test.mp3";
/// // Read an MP3 file with an ID3v2 tag
/// let mut tagged_file = lofty::read_from_path(path_to_mp3)?;
///
/// // An ID3v2 tag
/// let tag = tagged_file.primary_tag_mut();
///
/// assert!(tag.is_some());
/// assert_eq!(tag.unwrap().tag_type(), TagType::Id3v2);
///
/// // Alter the tag...
/// # Ok(()) }
/// ```
fn primary_tag_mut(&mut self) -> Option<&mut Tag> {
self.tag_mut(self.primary_tag_type())
}
/// Gets the first tag, if there are any
///
/// NOTE: This will grab the first available tag, you cannot rely on the result being
/// a specific type
///
/// # Examples
///
/// ```rust
/// use lofty::file::TaggedFileExt;
///
/// # fn main() -> lofty::error::Result<()> {
/// # let path = "tests/files/assets/minimal/full_test.mp3";
/// // A file we know has tags
/// let mut tagged_file = lofty::read_from_path(path)?;
///
/// // A tag of a (currently) unknown type
/// let tag = tagged_file.first_tag();
/// assert!(tag.is_some());
/// # Ok(()) }
/// ```
fn first_tag(&self) -> Option<&Tag> {
self.tags().first()
}
/// Gets a mutable reference to the first tag, if there are any
///
/// NOTE: This will grab the first available tag, you cannot rely on the result being
/// a specific type
///
/// # Examples
///
/// ```rust
/// use lofty::file::TaggedFileExt;
///
/// # fn main() -> lofty::error::Result<()> {
/// # let path = "tests/files/assets/minimal/full_test.mp3";
/// // A file we know has tags
/// let mut tagged_file = lofty::read_from_path(path)?;
///
/// // A tag of a (currently) unknown type
/// let tag = tagged_file.first_tag_mut();
/// assert!(tag.is_some());
///
/// // Alter the tag...
/// # Ok(()) }
/// ```
fn first_tag_mut(&mut self) -> Option<&mut Tag>;
/// Inserts a [`Tag`]
///
/// NOTE: This will do nothing if the [`FileType`] does not support
/// the [`TagType`]. See [`FileType::supports_tag_type`]
///
/// If a tag is replaced, it will be returned
///
/// # Examples
///
/// ```rust
/// use lofty::file::{AudioFile, TaggedFileExt};
/// use lofty::tag::{Tag, TagType};
///
/// # fn main() -> lofty::error::Result<()> {
/// # let path_to_mp3 = "tests/files/assets/minimal/full_test.mp3";
/// // Read an MP3 file without an ID3v2 tag
/// let mut tagged_file = lofty::read_from_path(path_to_mp3)?;
/// # let _ = tagged_file.remove(TagType::Id3v2); // sneaky
///
/// assert!(!tagged_file.contains_tag_type(TagType::Id3v2));
///
/// // Insert the ID3v2 tag
/// let new_id3v2_tag = Tag::new(TagType::Id3v2);
/// tagged_file.insert_tag(new_id3v2_tag);
///
/// assert!(tagged_file.contains_tag_type(TagType::Id3v2));
/// # Ok(()) }
/// ```
fn insert_tag(&mut self, tag: Tag) -> Option;
/// Removes a specific [`TagType`] and returns it
///
/// # Examples
///
/// ```rust
/// use lofty::file::{AudioFile, TaggedFileExt};
/// use lofty::tag::TagType;
///
/// # fn main() -> lofty::error::Result<()> {
/// # let path_to_mp3 = "tests/files/assets/minimal/full_test.mp3";
/// // Read an MP3 file containing an ID3v2 tag
/// let mut tagged_file = lofty::read_from_path(path_to_mp3)?;
///
/// assert!(tagged_file.contains_tag_type(TagType::Id3v2));
///
/// // Take the ID3v2 tag
/// let id3v2 = tagged_file.remove(TagType::Id3v2);
///
/// assert!(!tagged_file.contains_tag_type(TagType::Id3v2));
/// # Ok(()) }
/// ```
fn remove(&mut self, tag_type: TagType) -> Option;
/// Removes all tags from the file
///
/// # Examples
///
/// ```rust
/// use lofty::file::TaggedFileExt;
///
/// # fn main() -> lofty::error::Result<()> {
/// # let path = "tests/files/assets/minimal/full_test.mp3";
/// let mut tagged_file = lofty::read_from_path(path)?;
///
/// tagged_file.clear();
///
/// assert!(tagged_file.tags().is_empty());
/// # Ok(()) }
/// ```
fn clear(&mut self);
}
/// A generic representation of a file
///
/// This is used when the [`FileType`] has to be guessed
pub struct TaggedFile {
/// The file's type
pub(crate) ty: FileType,
/// The file's audio properties
pub(crate) properties: FileProperties,
/// A collection of the file's tags
pub(crate) tags: Vec,
}
impl TaggedFile {
#[doc(hidden)]
/// This exists for use in `lofty_attr`, there's no real use for this externally
#[must_use]
pub const fn new(ty: FileType, properties: FileProperties, tags: Vec) -> Self {
Self {
ty,
properties,
tags,
}
}
/// Changes the [`FileType`]
///
/// NOTES:
///
/// * This will remove any tag the format does not support. See [`FileType::supports_tag_type`]
/// * This will reset the [`FileProperties`]
///
/// # Examples
///
/// ```rust
/// use lofty::file::{AudioFile, FileType, TaggedFileExt};
/// use lofty::tag::TagType;
///
/// # fn main() -> lofty::error::Result<()> {
/// # let path_to_mp3 = "tests/files/assets/minimal/full_test.mp3";
/// // Read an MP3 file containing an ID3v2 tag
/// let mut tagged_file = lofty::read_from_path(path_to_mp3)?;
///
/// assert!(tagged_file.contains_tag_type(TagType::Id3v2));
///
/// // Remap our MP3 file to WavPack, which doesn't support ID3v2
/// tagged_file.change_file_type(FileType::WavPack);
///
/// assert!(!tagged_file.contains_tag_type(TagType::Id3v2));
/// # Ok(()) }
/// ```
pub fn change_file_type(&mut self, file_type: FileType) {
self.ty = file_type;
self.properties = FileProperties::default();
self.tags
.retain(|t| self.ty.supports_tag_type(t.tag_type()));
}
}
impl TaggedFileExt for TaggedFile {
fn file_type(&self) -> FileType {
self.ty
}
fn tags(&self) -> &[Tag] {
self.tags.as_slice()
}
fn tag(&self, tag_type: TagType) -> Option<&Tag> {
self.tags.iter().find(|i| i.tag_type() == tag_type)
}
fn tag_mut(&mut self, tag_type: TagType) -> Option<&mut Tag> {
self.tags.iter_mut().find(|i| i.tag_type() == tag_type)
}
fn first_tag_mut(&mut self) -> Option<&mut Tag> {
self.tags.first_mut()
}
fn insert_tag(&mut self, tag: Tag) -> Option {
let tag_type = tag.tag_type();
if self.supports_tag_type(tag_type) {
let ret = self.remove(tag_type);
self.tags.push(tag);
return ret;
}
None
}
fn remove(&mut self, tag_type: TagType) -> Option {
self.tags
.iter()
.position(|t| t.tag_type() == tag_type)
.map(|pos| self.tags.remove(pos))
}
fn clear(&mut self) {
self.tags.clear()
}
}
impl AudioFile for TaggedFile {
type Properties = FileProperties;
fn read_from(reader: &mut R, parse_options: ParseOptions) -> Result
where
R: Read + Seek,
Self: Sized,
{
crate::probe::Probe::new(reader)
.guess_file_type()?
.options(parse_options)
.read()
}
fn save_to(&self, file: &mut F, write_options: WriteOptions) -> Result<()>
where
F: FileLike,
LoftyError: From<::Error>,
LoftyError: From<::Error>,
{
for tag in &self.tags {
// TODO: This is a temporary solution. Ideally we should probe once and use
// the format-specific writing to avoid these rewinds.
file.rewind()?;
tag.save_to(file, write_options)?;
}
Ok(())
}
fn properties(&self) -> &Self::Properties {
&self.properties
}
fn contains_tag(&self) -> bool {
!self.tags.is_empty()
}
fn contains_tag_type(&self, tag_type: TagType) -> bool {
self.tags.iter().any(|t| t.tag_type() == tag_type)
}
}
impl From for TaggedFile {
fn from(input: BoundTaggedFile) -> Self {
input.inner
}
}
/// A variant of [`TaggedFile`] that holds a [`File`] handle, and reflects changes
/// such as tag removals.
///
/// For example:
///
/// ```rust,no_run
/// use lofty::config::WriteOptions;
/// use lofty::file::{AudioFile, TaggedFileExt};
/// use lofty::tag::{Tag, TagType};
/// # fn main() -> lofty::error::Result<()> {
/// # let path = "tests/files/assets/minimal/full_test.mp3";
///
/// // We create an empty tag
/// let tag = Tag::new(TagType::Id3v2);
///
/// let mut tagged_file = lofty::read_from_path(path)?;
///
/// // Push our empty tag into the TaggedFile
/// tagged_file.insert_tag(tag);
///
/// // After saving, our file still "contains" the ID3v2 tag, but if we were to read
/// // "foo.mp3", it would not have an ID3v2 tag. Lofty does not write empty tags, but this
/// // change will not be reflected in `TaggedFile`.
/// tagged_file.save_to_path("foo.mp3", WriteOptions::default())?;
/// assert!(tagged_file.contains_tag_type(TagType::Id3v2));
/// # Ok(()) }
/// ```
///
/// However, when using `BoundTaggedFile`:
///
/// ```rust,no_run
/// use lofty::config::{ParseOptions, WriteOptions};
/// use lofty::file::{AudioFile, BoundTaggedFile, TaggedFileExt};
/// use lofty::tag::{Tag, TagType};
/// use std::fs::OpenOptions;
/// # fn main() -> lofty::error::Result<()> {
/// # let path = "tests/files/assets/minimal/full_test.mp3";
///
/// // We create an empty tag
/// let tag = Tag::new(TagType::Id3v2);
///
/// // We'll need to open our file for reading *and* writing
/// let file = OpenOptions::new().read(true).write(true).open(path)?;
/// let parse_options = ParseOptions::new();
///
/// let mut bound_tagged_file = BoundTaggedFile::read_from(file, parse_options)?;
///
/// // Push our empty tag into the TaggedFile
/// bound_tagged_file.insert_tag(tag);
///
/// // Now when saving, we no longer have to specify a path, and the tags in the `BoundTaggedFile`
/// // reflect those in the actual file on disk.
/// bound_tagged_file.save(WriteOptions::default())?;
/// assert!(!bound_tagged_file.contains_tag_type(TagType::Id3v2));
/// # Ok(()) }
/// ```
pub struct BoundTaggedFile {
inner: TaggedFile,
file_handle: File,
}
impl BoundTaggedFile {
/// Create a new [`BoundTaggedFile`]
///
/// # Errors
///
/// See [`AudioFile::read_from`]
///
/// # Examples
///
/// ```rust
/// use lofty::config::ParseOptions;
/// use lofty::file::{AudioFile, BoundTaggedFile, TaggedFileExt};
/// use lofty::tag::{Tag, TagType};
/// use std::fs::OpenOptions;
/// # fn main() -> lofty::error::Result<()> {
/// # let path = "tests/files/assets/minimal/full_test.mp3";
///
/// // We'll need to open our file for reading *and* writing
/// let file = OpenOptions::new().read(true).write(true).open(path)?;
/// let parse_options = ParseOptions::new();
///
/// let bound_tagged_file = BoundTaggedFile::read_from(file, parse_options)?;
/// # Ok(()) }
/// ```
pub fn read_from(mut file: File, parse_options: ParseOptions) -> Result {
let inner = TaggedFile::read_from(&mut file, parse_options)?;
file.rewind()?;
Ok(Self {
inner,
file_handle: file,
})
}
/// Save the tags to the file stored internally
///
/// # Errors
///
/// See [`TaggedFile::save_to`]
///
/// # Examples
///
/// ```rust,no_run
/// use lofty::config::{ParseOptions, WriteOptions};
/// use lofty::file::{AudioFile, BoundTaggedFile, TaggedFileExt};
/// use lofty::tag::{Tag, TagType};
/// use std::fs::OpenOptions;
/// # fn main() -> lofty::error::Result<()> {
/// # let path = "tests/files/assets/minimal/full_test.mp3";
///
/// // We'll need to open our file for reading *and* writing
/// let file = OpenOptions::new().read(true).write(true).open(path)?;
/// let parse_options = ParseOptions::new();
///
/// let mut bound_tagged_file = BoundTaggedFile::read_from(file, parse_options)?;
///
/// // Do some work to the tags...
///
/// // This will save the tags to the file we provided to `read_from`
/// bound_tagged_file.save(WriteOptions::default())?;
/// # Ok(()) }
/// ```
pub fn save(&mut self, write_options: WriteOptions) -> Result<()> {
self.inner.save_to(&mut self.file_handle, write_options)?;
self.inner.tags.retain(|tag| !tag.is_empty());
Ok(())
}
/// Consume this tagged file and return the internal file "buffer".
/// This allows you to reuse the internal file.
///
/// Any changes that haven't been commited will be discarded once you
/// call this function.
pub fn into_inner(self) -> File {
self.file_handle
}
}
impl TaggedFileExt for BoundTaggedFile {
fn file_type(&self) -> FileType {
self.inner.file_type()
}
fn tags(&self) -> &[Tag] {
self.inner.tags()
}
fn tag(&self, tag_type: TagType) -> Option<&Tag> {
self.inner.tag(tag_type)
}
fn tag_mut(&mut self, tag_type: TagType) -> Option<&mut Tag> {
self.inner.tag_mut(tag_type)
}
fn first_tag_mut(&mut self) -> Option<&mut Tag> {
self.inner.first_tag_mut()
}
fn insert_tag(&mut self, tag: Tag) -> Option {
self.inner.insert_tag(tag)
}
fn remove(&mut self, tag_type: TagType) -> Option {
self.inner.remove(tag_type)
}
fn clear(&mut self) {
self.inner.clear()
}
}
impl AudioFile for BoundTaggedFile {
type Properties = FileProperties;
fn read_from(_: &mut R, _: ParseOptions) -> Result
where
R: Read + Seek,
Self: Sized,
{
unimplemented!(
"BoundTaggedFile can only be constructed through `BoundTaggedFile::read_from`"
)
}
fn save_to(&self, file: &mut F, write_options: WriteOptions) -> Result<()>
where
F: FileLike,
LoftyError: From<::Error>,
LoftyError: From<::Error>,
{
self.inner.save_to(file, write_options)
}
fn properties(&self) -> &Self::Properties {
self.inner.properties()
}
fn contains_tag(&self) -> bool {
self.inner.contains_tag()
}
fn contains_tag_type(&self, tag_type: TagType) -> bool {
self.inner.contains_tag_type(tag_type)
}
}
lofty-0.21.1/src/flac/block.rs 0000644 0000000 0000000 00000002514 10461020230 0014200 0 ustar 0000000 0000000 #![allow(dead_code)]
use crate::error::Result;
use crate::macros::try_vec;
use std::io::{Read, Seek, SeekFrom};
use byteorder::{BigEndian, ReadBytesExt};
pub(in crate::flac) const BLOCK_ID_STREAMINFO: u8 = 0;
pub(in crate::flac) const BLOCK_ID_PADDING: u8 = 1;
pub(in crate::flac) const BLOCK_ID_SEEKTABLE: u8 = 3;
pub(in crate::flac) const BLOCK_ID_VORBIS_COMMENTS: u8 = 4;
pub(in crate::flac) const BLOCK_ID_PICTURE: u8 = 6;
const BLOCK_HEADER_SIZE: u64 = 4;
pub(crate) struct Block {
pub(super) byte: u8,
pub(super) ty: u8,
pub(super) last: bool,
pub(crate) content: Vec,
pub(super) start: u64,
pub(super) end: u64,
}
impl Block {
pub(crate) fn read(data: &mut R, mut predicate: P) -> Result
where
R: Read + Seek,
P: FnMut(u8) -> bool,
{
let start = data.stream_position()?;
let byte = data.read_u8()?;
let last = (byte & 0x80) != 0;
let ty = byte & 0x7F;
let size = data.read_u24::()?;
log::trace!("Reading FLAC block, type: {ty}, size: {size}");
let mut content;
if predicate(ty) {
content = try_vec![0; size as usize];
data.read_exact(&mut content)?;
} else {
content = Vec::new();
data.seek(SeekFrom::Current(i64::from(size)))?;
}
let end = start + u64::from(size) + BLOCK_HEADER_SIZE;
Ok(Self {
byte,
ty,
last,
content,
start,
end,
})
}
}
lofty-0.21.1/src/flac/mod.rs 0000644 0000000 0000000 00000007526 10461020230 0013675 0 ustar 0000000 0000000 //! Items for FLAC
//!
//! ## File notes
//!
//! * See [`FlacFile`]
pub(crate) mod block;
pub(crate) mod properties;
mod read;
pub(crate) mod write;
use crate::config::WriteOptions;
use crate::error::{LoftyError, Result};
use crate::file::{FileType, TaggedFile};
use crate::id3::v2::tag::Id3v2Tag;
use crate::ogg::tag::VorbisCommentsRef;
use crate::ogg::{OggPictureStorage, VorbisComments};
use crate::picture::{Picture, PictureInformation};
use crate::tag::TagExt;
use crate::util::io::{FileLike, Length, Truncate};
use std::borrow::Cow;
use lofty_attr::LoftyFile;
// Exports
pub use properties::FlacProperties;
/// A FLAC file
///
/// ## Notes
///
/// * The ID3v2 tag is **read only**, and it's use is discouraged by spec
/// * Pictures are stored in the `FlacFile` itself, rather than the tag. Any pictures inside the tag will
/// be extracted out and stored in their own picture blocks.
/// * It is possible to put pictures inside of the tag, that will not be accessible using the available
/// methods on `FlacFile` ([`FlacFile::pictures`], [`FlacFile::remove_picture_type`], etc.)
/// * When converting to [`TaggedFile`], all pictures will be put inside of a [`VorbisComments`] tag, even if the
/// file did not originally contain one.
#[derive(LoftyFile)]
#[lofty(read_fn = "read::read_from")]
#[lofty(write_fn = "Self::write_to")]
#[lofty(no_into_taggedfile_impl)]
pub struct FlacFile {
/// An ID3v2 tag
#[lofty(tag_type = "Id3v2")]
pub(crate) id3v2_tag: Option,
/// The vorbis comments contained in the file
#[lofty(tag_type = "VorbisComments")]
pub(crate) vorbis_comments_tag: Option,
pub(crate) pictures: Vec<(Picture, PictureInformation)>,
/// The file's audio properties
pub(crate) properties: FlacProperties,
}
impl FlacFile {
// We need a special write fn to append our pictures into a `VorbisComments` tag
fn write_to(&self, file: &mut F, write_options: WriteOptions) -> Result<()>
where
F: FileLike,
LoftyError: From<::Error>,
LoftyError: From<::Error>,
{
if let Some(ref id3v2) = self.id3v2_tag {
id3v2.save_to(file, write_options)?;
file.rewind()?;
}
// We have an existing vorbis comments tag, we can just append our pictures to it
if let Some(ref vorbis_comments) = self.vorbis_comments_tag {
return VorbisCommentsRef {
vendor: Cow::from(vorbis_comments.vendor.as_str()),
items: vorbis_comments
.items
.iter()
.map(|(k, v)| (k.as_str(), v.as_str())),
pictures: vorbis_comments
.pictures
.iter()
.map(|(p, i)| (p, *i))
.chain(self.pictures.iter().map(|(p, i)| (p, *i))),
}
.write_to(file, write_options);
}
// We have pictures, but no vorbis comments tag, we'll need to create a dummy one
if !self.pictures.is_empty() {
return VorbisCommentsRef {
vendor: Cow::from(""),
items: std::iter::empty(),
pictures: self.pictures.iter().map(|(p, i)| (p, *i)),
}
.write_to(file, write_options);
}
Ok(())
}
}
impl OggPictureStorage for FlacFile {
fn pictures(&self) -> &[(Picture, PictureInformation)] {
&self.pictures
}
}
impl From for TaggedFile {
fn from(mut value: FlacFile) -> Self {
TaggedFile {
ty: FileType::Flac,
properties: value.properties.into(),
tags: {
let mut tags = Vec::with_capacity(2);
if let Some(id3v2) = value.id3v2_tag {
tags.push(id3v2.into());
}
// Move our pictures into a `VorbisComments` tag, creating one if necessary
match value.vorbis_comments_tag {
Some(mut vorbis_comments) => {
vorbis_comments.pictures.append(&mut value.pictures);
tags.push(vorbis_comments.into());
},
None if !value.pictures.is_empty() => tags.push(
VorbisComments {
vendor: String::new(),
items: Vec::new(),
pictures: value.pictures,
}
.into(),
),
_ => {},
}
tags
},
}
}
}
lofty-0.21.1/src/flac/properties.rs 0000644 0000000 0000000 00000005572 10461020230 0015311 0 ustar 0000000 0000000 use crate::error::Result;
use crate::properties::FileProperties;
use std::io::Read;
use std::time::Duration;
use byteorder::{BigEndian, ReadBytesExt};
/// A FLAC file's audio properties
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct FlacProperties {
pub(crate) duration: Duration,
pub(crate) overall_bitrate: u32,
pub(crate) audio_bitrate: u32,
pub(crate) sample_rate: u32,
pub(crate) bit_depth: u8,
pub(crate) channels: u8,
pub(crate) signature: u128,
}
impl From for FileProperties {
fn from(input: FlacProperties) -> Self {
Self {
duration: input.duration,
overall_bitrate: Some(input.overall_bitrate),
audio_bitrate: Some(input.audio_bitrate),
sample_rate: Some(input.sample_rate),
bit_depth: Some(input.bit_depth),
channels: Some(input.channels),
channel_mask: None,
}
}
}
impl FlacProperties {
/// Duration of the audio
pub fn duration(&self) -> Duration {
self.duration
}
/// Overall bitrate (kbps)
pub fn overall_bitrate(&self) -> u32 {
self.overall_bitrate
}
/// Audio bitrate (kbps)
pub fn audio_bitrate(&self) -> u32 {
self.audio_bitrate
}
/// Sample rate (Hz)
pub fn sample_rate(&self) -> u32 {
self.sample_rate
}
/// Bits per sample (usually 16 or 24 bit)
pub fn bit_depth(&self) -> u8 {
self.bit_depth
}
/// Channel count
pub fn channels(&self) -> u8 {
self.channels
}
/// MD5 signature of the unencoded audio data
pub fn signature(&self) -> u128 {
self.signature
}
}
pub(crate) fn read_properties(
stream_info: &mut R,
stream_length: u64,
file_length: u64,
) -> Result
where
R: Read,
{
// Skip 4 bytes
// Minimum block size (2)
// Maximum block size (2)
stream_info.read_u32::()?;
// Skip 6 bytes
// Minimum frame size (3)
// Maximum frame size (3)
stream_info.read_uint::(6)?;
// Read 4 bytes
// Sample rate (20 bits)
// Number of channels (3 bits)
// Bits per sample (5 bits)
// Total samples (first 4 bits)
let info = stream_info.read_u32::()?;
let sample_rate = info >> 12;
let bits_per_sample = ((info >> 4) & 0b11111) + 1;
let channels = ((info >> 9) & 7) + 1;
// Read the remaining 32 bits of the total samples
let total_samples = stream_info.read_u32::()? | (info << 28);
let signature = stream_info.read_u128::()?;
let mut properties = FlacProperties {
sample_rate,
bit_depth: bits_per_sample as u8,
channels: channels as u8,
signature,
..FlacProperties::default()
};
if sample_rate > 0 && total_samples > 0 {
let length = (u64::from(total_samples) * 1000) / u64::from(sample_rate);
properties.duration = Duration::from_millis(length);
if length > 0 && file_length > 0 && stream_length > 0 {
properties.overall_bitrate = ((file_length * 8) / length) as u32;
properties.audio_bitrate = ((stream_length * 8) / length) as u32;
}
}
Ok(properties)
}
lofty-0.21.1/src/flac/read.rs 0000644 0000000 0000000 00000010234 10461020230 0014017 0 ustar 0000000 0000000 use super::block::Block;
use super::properties::FlacProperties;
use super::FlacFile;
use crate::config::{ParseOptions, ParsingMode};
use crate::error::Result;
use crate::flac::block::{BLOCK_ID_PICTURE, BLOCK_ID_STREAMINFO, BLOCK_ID_VORBIS_COMMENTS};
use crate::id3::v2::read::parse_id3v2;
use crate::id3::{find_id3v2, FindId3v2Config, ID3FindResults};
use crate::macros::{decode_err, err};
use crate::ogg::read::read_comments;
use crate::picture::Picture;
use std::io::{Read, Seek, SeekFrom};
pub(super) fn verify_flac(data: &mut R) -> Result
where
R: Read + Seek,
{
let mut marker = [0; 4];
data.read_exact(&mut marker)?;
if &marker != b"fLaC" {
decode_err!(@BAIL Flac, "File missing \"fLaC\" stream marker");
}
let block = Block::read(data, |_| true)?;
if block.ty != BLOCK_ID_STREAMINFO {
decode_err!(@BAIL Flac, "File missing mandatory STREAMINFO block");
}
log::debug!("File verified to be FLAC");
Ok(block)
}
pub(crate) fn read_from(data: &mut R, parse_options: ParseOptions) -> Result
where
R: Read + Seek,
{
let mut flac_file = FlacFile {
id3v2_tag: None,
vorbis_comments_tag: None,
pictures: Vec::new(),
properties: FlacProperties::default(),
};
let find_id3v2_config = if parse_options.read_tags {
FindId3v2Config::READ_TAG
} else {
FindId3v2Config::NO_READ_TAG
};
// It is possible for a FLAC file to contain an ID3v2 tag
if let ID3FindResults(Some(header), Some(content)) = find_id3v2(data, find_id3v2_config)? {
log::warn!("Encountered an ID3v2 tag. This tag cannot be rewritten to the FLAC file!");
let reader = &mut &*content;
let id3v2 = parse_id3v2(reader, header, parse_options)?;
flac_file.id3v2_tag = Some(id3v2);
}
let stream_info = verify_flac(data)?;
let stream_info_len = (stream_info.end - stream_info.start) as u32;
if stream_info_len < 18 {
decode_err!(@BAIL Flac, "File has an invalid STREAMINFO block size (< 18)");
}
let mut last_block = stream_info.last;
while !last_block {
let block = Block::read(data, |block_type| {
(block_type == BLOCK_ID_VORBIS_COMMENTS && parse_options.read_tags)
|| (block_type == BLOCK_ID_PICTURE && parse_options.read_cover_art)
})?;
last_block = block.last;
if block.content.is_empty() {
continue;
}
if block.ty == BLOCK_ID_VORBIS_COMMENTS && parse_options.read_tags {
log::debug!("Encountered a Vorbis Comments block, parsing");
// NOTE: According to the spec
//
// :
// "There may be only one VORBIS_COMMENT block in a stream."
//
// But of course, we can't ever expect any spec compliant inputs, so we just
// take whatever happens to be the latest block in the stream. This is safe behavior,
// as when writing to a file with multiple tags, we end up removing all `VORBIS_COMMENT`
// blocks anyway.
if flac_file.vorbis_comments_tag.is_some()
&& parse_options.parsing_mode == ParsingMode::Strict
{
decode_err!(@BAIL Flac, "Streams are only allowed one Vorbis Comments block per stream");
}
let vorbis_comments = read_comments(
&mut &*block.content,
block.content.len() as u64,
parse_options,
)?;
flac_file.vorbis_comments_tag = Some(vorbis_comments);
continue;
}
if block.ty == BLOCK_ID_PICTURE && parse_options.read_cover_art {
log::debug!("Encountered a FLAC picture block, parsing");
match Picture::from_flac_bytes(&block.content, false, parse_options.parsing_mode) {
Ok(picture) => flac_file.pictures.push(picture),
Err(e) => {
if parse_options.parsing_mode == ParsingMode::Strict {
return Err(e);
}
log::warn!("Unable to read FLAC picture block, discarding");
continue;
},
}
}
}
if !parse_options.read_properties {
return Ok(flac_file);
}
let (stream_length, file_length) = {
let current = data.stream_position()?;
let end = data.seek(SeekFrom::End(0))?;
// In the event that a block lies about its size, the current position could be
// completely wrong.
if current > end {
err!(SizeMismatch);
}
(end - current, end)
};
flac_file.properties =
super::properties::read_properties(&mut &*stream_info.content, stream_length, file_length)?;
Ok(flac_file)
}
lofty-0.21.1/src/flac/write.rs 0000644 0000000 0000000 00000014517 10461020230 0014246 0 ustar 0000000 0000000 use super::block::{Block, BLOCK_ID_PADDING, BLOCK_ID_PICTURE, BLOCK_ID_VORBIS_COMMENTS};
use super::read::verify_flac;
use crate::config::WriteOptions;
use crate::error::{LoftyError, Result};
use crate::macros::{err, try_vec};
use crate::ogg::tag::VorbisCommentsRef;
use crate::ogg::write::create_comments;
use crate::picture::{Picture, PictureInformation};
use crate::tag::{Tag, TagType};
use crate::util::io::{FileLike, Length, Truncate};
use std::borrow::Cow;
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
const BLOCK_HEADER_SIZE: usize = 4;
const MAX_BLOCK_SIZE: u32 = 16_777_215;
pub(crate) fn write_to(file: &mut F, tag: &Tag, write_options: WriteOptions) -> Result<()>
where
F: FileLike,
LoftyError: From<::Error>,
LoftyError: From<::Error>,
{
match tag.tag_type() {
TagType::VorbisComments => {
let (vendor, items, pictures) = crate::ogg::tag::create_vorbis_comments_ref(tag);
let mut comments_ref = VorbisCommentsRef {
vendor: Cow::from(vendor),
items,
pictures,
};
write_to_inner(file, &mut comments_ref, write_options)
},
// This tag can *only* be removed in this format
TagType::Id3v2 => crate::id3::v2::tag::Id3v2TagRef::empty().write_to(file, write_options),
_ => err!(UnsupportedTag),
}
}
pub(crate) fn write_to_inner<'a, F, II, IP>(
file: &mut F,
tag: &mut VorbisCommentsRef<'a, II, IP>,
write_options: WriteOptions,
) -> Result<()>
where
F: FileLike,
LoftyError: From<::Error>,
II: Iterator- ,
IP: Iterator
- ,
{
let stream_info = verify_flac(file)?;
let mut last_block = stream_info.last;
let mut file_bytes = Vec::new();
file.read_to_end(&mut file_bytes)?;
let mut cursor = Cursor::new(file_bytes);
// TODO: We need to actually use padding (https://github.com/Serial-ATA/lofty-rs/issues/445)
let mut end_padding_exists = false;
let mut last_block_info = (
stream_info.byte,
stream_info.start as usize,
stream_info.end as usize,
);
let mut blocks_to_remove = Vec::new();
while !last_block {
let block = Block::read(&mut cursor, |block_ty| block_ty == BLOCK_ID_VORBIS_COMMENTS)?;
let start = block.start;
let end = block.end;
let block_type = block.ty;
last_block = block.last;
if last_block {
last_block_info = (block.byte, (end - start) as usize, end as usize)
}
match block_type {
BLOCK_ID_VORBIS_COMMENTS => {
blocks_to_remove.push((start, end));
// Retain the original vendor string
let reader = &mut &block.content[..];
let vendor_len = reader.read_u32::()?;
let mut vendor = try_vec![0; vendor_len as usize];
reader.read_exact(&mut vendor)?;
// TODO: Error on strict?
let Ok(vendor_str) = String::from_utf8(vendor) else {
log::warn!("FLAC vendor string is not valid UTF-8, not re-using");
tag.vendor = Cow::Borrowed("");
continue;
};
tag.vendor = Cow::Owned(vendor_str);
},
BLOCK_ID_PICTURE => blocks_to_remove.push((start, end)),
BLOCK_ID_PADDING => {
if last_block {
end_padding_exists = true
} else {
blocks_to_remove.push((start, end))
}
},
_ => {},
}
}
let mut file_bytes = cursor.into_inner();
if !end_padding_exists {
if let Some(preferred_padding) = write_options.preferred_padding {
log::warn!("File is missing a PADDING block. Adding one");
let mut first_byte = 0_u8;
first_byte |= last_block_info.0 & 0x7F;
file_bytes[last_block_info.1] = first_byte;
let block_size = core::cmp::min(preferred_padding, MAX_BLOCK_SIZE);
let mut padding_block = try_vec![0; BLOCK_HEADER_SIZE + block_size as usize];
let mut padding_byte = 0;
padding_byte |= 0x80;
padding_byte |= 1 & 0x7F;
padding_block[0] = padding_byte;
padding_block[1..4].copy_from_slice(&block_size.to_be_bytes()[1..]);
file_bytes.splice(last_block_info.2..last_block_info.2, padding_block);
}
}
let mut comment_blocks = Cursor::new(Vec::new());
create_comment_block(&mut comment_blocks, &tag.vendor, &mut tag.items)?;
let mut comment_blocks = comment_blocks.into_inner();
create_picture_blocks(&mut comment_blocks, &mut tag.pictures)?;
if blocks_to_remove.is_empty() {
file_bytes.splice(0..0, comment_blocks);
} else {
blocks_to_remove.sort_unstable();
blocks_to_remove.reverse();
let first = blocks_to_remove.pop().unwrap(); // Infallible
for (s, e) in &blocks_to_remove {
file_bytes.drain(*s as usize..*e as usize);
}
file_bytes.splice(first.0 as usize..first.1 as usize, comment_blocks);
}
file.seek(SeekFrom::Start(stream_info.end))?;
file.truncate(stream_info.end)?;
file.write_all(&file_bytes)?;
Ok(())
}
fn create_comment_block(
writer: &mut Cursor>,
vendor: &str,
items: &mut dyn Iterator
- ,
) -> Result<()> {
let mut peek = items.peekable();
if peek.peek().is_some() {
let mut byte = 0_u8;
byte |= 4 & 0x7F;
writer.write_u8(byte)?;
writer.write_u32::(vendor.len() as u32)?;
writer.write_all(vendor.as_bytes())?;
let item_count_pos = writer.stream_position()?;
let mut count = 0;
writer.write_u32::(count)?;
create_comments(writer, &mut count, &mut peek)?;
let len = (writer.get_ref().len() - 1) as u32;
if len > MAX_BLOCK_SIZE {
err!(TooMuchData);
}
let comment_end = writer.stream_position()?;
writer.seek(SeekFrom::Start(item_count_pos))?;
writer.write_u32::(count)?;
writer.seek(SeekFrom::Start(comment_end))?;
writer
.get_mut()
.splice(1..1, len.to_be_bytes()[1..].to_vec());
// size = block type + vendor length + vendor + item count + items
log::trace!(
"Wrote a comment block, size: {}",
1 + 4 + vendor.len() + 4 + (len as usize)
);
}
Ok(())
}
fn create_picture_blocks(
writer: &mut Vec,
pictures: &mut dyn Iterator
- ,
) -> Result<()> {
let mut byte = 0_u8;
byte |= 6 & 0x7F;
for (pic, info) in pictures {
writer.write_u8(byte)?;
let pic_bytes = pic.as_flac_bytes(info, false);
let pic_len = pic_bytes.len() as u32;
if pic_len > MAX_BLOCK_SIZE {
err!(TooMuchData);
}
writer.write_all(&pic_len.to_be_bytes()[1..])?;
writer.write_all(pic_bytes.as_slice())?;
// size = block type + block length + data
log::trace!("Wrote a picture block, size: {}", 1 + 3 + pic_len);
}
Ok(())
}
lofty-0.21.1/src/id3/mod.rs 0000644 0000000 0000000 00000010330 10461020230 0013432 0 ustar 0000000 0000000 //! ID3 specific items
//!
//! ID3 does things differently than other tags, making working with them a little more effort than other formats.
//! Check the other modules for important notes and/or warnings.
pub mod v1;
pub mod v2;
use crate::error::{ErrorKind, LoftyError, Result};
use crate::macros::try_vec;
use crate::util::text::utf8_decode_str;
use v2::header::Id3v2Header;
use std::io::{Read, Seek, SeekFrom};
use std::ops::Neg;
pub(crate) struct ID3FindResults(pub Option, pub Content);
pub(crate) fn find_lyrics3v2(data: &mut R) -> Result>
where
R: Read + Seek,
{
log::debug!("Searching for a Lyrics3v2 tag");
let mut header = None;
let mut size = 0_u32;
data.seek(SeekFrom::Current(-15))?;
let mut lyrics3v2 = [0; 15];
data.read_exact(&mut lyrics3v2)?;
if &lyrics3v2[7..] == b"LYRICS200" {
log::warn!("Encountered a Lyrics3v2 tag. This is an outdated format, and will be skipped.");
header = Some(());
let lyrics_size = utf8_decode_str(&lyrics3v2[..7])?;
let lyrics_size = lyrics_size.parse::().map_err(|_| {
LoftyError::new(ErrorKind::TextDecode(
"Lyrics3v2 tag has an invalid size string",
))
})?;
size += lyrics_size;
data.seek(SeekFrom::Current(i64::from(lyrics_size + 15).neg()))?;
}
Ok(ID3FindResults(header, size))
}
#[allow(unused_variables)]
pub(crate) fn find_id3v1(
data: &mut R,
read: bool,
) -> Result>>
where
R: Read + Seek,
{
log::debug!("Searching for an ID3v1 tag");
let mut id3v1 = None;
let mut header = None;
// Reader is too small to contain an ID3v2 tag
if data.seek(SeekFrom::End(-128)).is_err() {
data.seek(SeekFrom::End(0))?;
return Ok(ID3FindResults(header, id3v1));
}
let mut id3v1_header = [0; 3];
data.read_exact(&mut id3v1_header)?;
data.seek(SeekFrom::Current(-3))?;
// No ID3v1 tag found
if &id3v1_header != b"TAG" {
data.seek(SeekFrom::End(0))?;
return Ok(ID3FindResults(header, id3v1));
}
log::debug!("Found an ID3v1 tag, parsing");
header = Some(());
if read {
let mut id3v1_tag = [0; 128];
data.read_exact(&mut id3v1_tag)?;
data.seek(SeekFrom::End(-128))?;
id3v1 = Some(v1::read::parse_id3v1(id3v1_tag))
}
Ok(ID3FindResults(header, id3v1))
}
#[derive(Copy, Clone, Debug)]
pub(crate) struct FindId3v2Config {
pub(crate) read: bool,
pub(crate) allowed_junk_window: Option,
}
impl FindId3v2Config {
pub(crate) const NO_READ_TAG: Self = Self {
read: false,
allowed_junk_window: None,
};
pub(crate) const READ_TAG: Self = Self {
read: true,
allowed_junk_window: None,
};
}
pub(crate) fn find_id3v2(
data: &mut R,
config: FindId3v2Config,
) -> Result>>>
where
R: Read + Seek,
{
log::debug!(
"Searching for an ID3v2 tag at offset: {}",
data.stream_position()?
);
let mut header = None;
let mut id3v2 = None;
if let Some(junk_window) = config.allowed_junk_window {
let mut id3v2_search_window = data.by_ref().take(junk_window);
let Some(id3v2_offset) = find_id3v2_in_junk(&mut id3v2_search_window)? else {
return Ok(ID3FindResults(None, None));
};
log::warn!(
"Found an ID3v2 tag preceded by junk data, offset: {}",
id3v2_offset
);
data.seek(SeekFrom::Current(-3))?;
}
if let Ok(id3v2_header) = Id3v2Header::parse(data) {
log::debug!("Found an ID3v2 tag, parsing");
if config.read {
let mut tag = try_vec![0; id3v2_header.size as usize];
data.read_exact(&mut tag)?;
id3v2 = Some(tag)
} else {
data.seek(SeekFrom::Current(i64::from(id3v2_header.size)))?;
}
if id3v2_header.flags.footer {
data.seek(SeekFrom::Current(10))?;
}
header = Some(id3v2_header);
} else {
data.seek(SeekFrom::Current(-10))?;
}
Ok(ID3FindResults(header, id3v2))
}
/// Searches for an ID3v2 tag in (potential) junk data between the start
/// of the file and the first frame
fn find_id3v2_in_junk(reader: &mut R) -> Result