diff --git a/src/error.rs b/src/error.rs index 4c96e007..84100f9c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,27 +1,37 @@ /// Errors that could occur within Lofty. #[derive(thiserror::Error, Debug)] pub enum LoftyError { + // File extension/format related errors /// Unknown file extension. #[error("Failed to guess the metadata format based on the file extension.")] UnknownFileExtension, - + /// Unsupported file extension + #[error("Unsupported format: {0}")] + UnsupportedFormat(String), /// Unable to guess the format #[error("No format could be determined from the provided file.")] UnknownFormat, + + // File data related errors /// Provided an empty file #[error("File contains no data")] EmptyFile, /// Provided a file with invalid/malformed data - #[error("File has invalid data")] - InvalidData, + #[error("File has invalid data: {0}")] + InvalidData(&'static str), + /// Attempting to write an abnormally large amount of data + #[error("An abnormally large amount of data was provided, and an overflow occurred")] + TooMuchData, - /// Unsupported file extension - #[error("Unsupported format: {0}")] - UnsupportedFormat(String), + // Picture related errors /// Picture has an unsupported mime type #[error("Unsupported mime type: {0}")] UnsupportedMimeType(String), + /// Provided an invalid picture + #[error("Picture contains invalid data")] + NotAPicture, + // Tag related errors /// Any error from [`ape`] #[error(transparent)] ApeTag(#[from] ape::Error), @@ -46,26 +56,16 @@ pub enum LoftyError { Ogg(#[from] ogg::OggReadError), /// Errors that arise while reading/writing to wav files #[error("Invalid Riff file: {0}")] - Riff(String), - /// Errors that arise while reading/writing to opus files - #[error("Invalid Opus file: {0}")] - Opus(String), + Riff(&'static str), - /// Arises when provided an invalid picture - #[error("Picture contains invalid data")] - NotAPicture, - - /// If a string isn't Utf8 - #[error(transparent)] - Utf8(#[from] std::str::Utf8Error), + // Conversions for std Errors /// Unable to convert bytes to a String #[error(transparent)] FromUtf8(#[from] std::string::FromUtf8Error), /// Represents all cases of `std::io::Error`. #[error(transparent)] - #[allow(clippy::upper_case_acronyms)] - IO(#[from] std::io::Error), + Io(#[from] std::io::Error), } -/// Type for the result of tag operations. +/// Result of tag operations. pub type Result = std::result::Result; diff --git a/src/lib.rs b/src/lib.rs index 66d59c70..e6e09ce8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -118,7 +118,7 @@ pub use crate::types::{ }; mod tag; -pub use crate::tag::{Id3Format, Tag, TagType, VorbisFormat}; +pub use crate::tag::{Id3Format, OggFormat, Tag, TagType}; mod error; pub use crate::error::{LoftyError, Result}; diff --git a/src/tag.rs b/src/tag.rs index 9008fd7d..c6e95704 100644 --- a/src/tag.rs +++ b/src/tag.rs @@ -1,7 +1,7 @@ #[allow(clippy::wildcard_imports)] use crate::components::tags::*; use crate::{AudioTag, LoftyError, Result}; -use std::io::Seek; +use std::io::{Cursor, Read, Seek, SeekFrom}; use std::path::Path; #[cfg(feature = "format-ape")] @@ -50,24 +50,28 @@ impl Tag { /// /// # Errors /// - /// * `path` either has no extension, or the extension is not valid unicode + /// * `path` does not exist + /// * `path` either has no extension, or the extension is not valid UTF-8 /// * `path` has an unsupported/unknown extension /// /// # Warning /// Using this on a `wav`/`wave`/`riff` file will **always** assume there's an ID3 tag. /// [`read_from_path_signature`](Tag::read_from_path_signature) is recommended, in the event that a RIFF INFO list is present instead. pub fn read_from_path(&self, path: impl AsRef) -> Result> { + let mut c = Cursor::new(std::fs::read(&path)?); + let tag_type = self.0.clone().unwrap_or({ let extension = path .as_ref() .extension() .ok_or(LoftyError::UnknownFileExtension)?; + let extension_str = extension.to_str().ok_or(LoftyError::UnknownFileExtension)?; TagType::try_from_ext(extension_str)? }); - Self::match_tag(path, tag_type) + Self::match_tag(&mut c, tag_type) } /// Attempts to get the tag format based on the file signature @@ -84,30 +88,48 @@ impl Tag { /// # Warning /// In the event that a riff file contains both an ID3 tag *and* a RIFF INFO chunk, the ID3 tag will **always** be chosen. pub fn read_from_path_signature(&self, path: impl AsRef) -> Result> { - let tag_type = self - .0 - .clone() - .unwrap_or(TagType::try_from_sig(&std::fs::read(path.as_ref())?)?); + let mut c = Cursor::new(std::fs::read(&path)?); - Self::match_tag(path, tag_type) + let tag_type = self.0.clone().unwrap_or(TagType::try_from_sig(&mut c)?); + + Self::match_tag(&mut c, tag_type) } - fn match_tag(path: impl AsRef, tag_type: TagType) -> Result> { + /// Attempts to get the tag format based on the data in the reader + /// + /// See [`read_from_path_signature`] for important notes, errors, and warnings. + /// + /// # Errors + /// + /// Same as [`read_from_path_signature`] + pub fn read_from_reader(&self, reader: &mut R) -> Result> + where + R: Read + Seek, + { + let tag_type = self.0.clone().unwrap_or(TagType::try_from_sig(reader)?); + + Self::match_tag(reader, tag_type) + } + + fn match_tag(reader: &mut R, tag_type: TagType) -> Result> + where + R: Read + Seek, + { match tag_type { #[cfg(feature = "format-ape")] - TagType::Ape => Ok(Box::new(ApeTag::read_from_path(path)?)), + TagType::Ape => Ok(Box::new(ApeTag::read_from(reader)?)), #[cfg(feature = "format-id3")] - TagType::Id3v2(format) => Ok(Box::new(Id3v2Tag::read_from_path(path, &format)?)), + TagType::Id3v2(format) => Ok(Box::new(Id3v2Tag::read_from(reader, &format)?)), #[cfg(feature = "format-mp4")] - TagType::Mp4 => Ok(Box::new(Mp4Tag::read_from_path(path)?)), + TagType::Mp4 => Ok(Box::new(Mp4Tag::read_from(reader)?)), #[cfg(feature = "format-riff")] - TagType::RiffInfo => Ok(Box::new(RiffTag::read_from_path(path)?)), + TagType::RiffInfo => Ok(Box::new(RiffTag::read_from(reader)?)), #[cfg(any( feature = "format-vorbis", feature = "format-flac", feature = "format-opus" ))] - TagType::Vorbis(format) => Ok(Box::new(VorbisTag::read_from_path(path, &format)?)), + TagType::Ogg(format) => Ok(Box::new(VorbisTag::read_from(reader, &format)?)), } } } @@ -129,8 +151,8 @@ pub enum TagType { feature = "format-opus", feature = "format-flac" ))] - /// Represents multiple formats, see [`VorbisFormat`](VorbisFormat) for extensions. - Vorbis(VorbisFormat), + /// Represents multiple formats, see [`OggFormat`](OggFormat) for extensions. + Ogg(OggFormat), #[cfg(feature = "format-riff")] /// Metadata stored in a RIFF INFO chunk /// Common file extensions: `.wav, .wave, .riff` @@ -144,10 +166,10 @@ pub enum TagType { feature = "format-flac" ))] /// File formats using vorbis comments -pub enum VorbisFormat { +pub enum OggFormat { #[cfg(feature = "format-vorbis")] /// Common file extensions: `.ogg, .oga` - Ogg, + Vorbis, #[cfg(feature = "format-opus")] /// Common file extensions: `.opus` Opus, @@ -180,32 +202,41 @@ impl TagType { #[cfg(all(feature = "format-riff", feature = "format-id3"))] "wav" | "wave" | "riff" => Ok(Self::Id3v2(Id3Format::Riff)), #[cfg(feature = "format-opus")] - "opus" => Ok(Self::Vorbis(VorbisFormat::Opus)), + "opus" => Ok(Self::Ogg(OggFormat::Opus)), #[cfg(feature = "format-flac")] - "flac" => Ok(Self::Vorbis(VorbisFormat::Flac)), + "flac" => Ok(Self::Ogg(OggFormat::Flac)), #[cfg(feature = "format-vorbis")] - "ogg" | "oga" => Ok(Self::Vorbis(VorbisFormat::Ogg)), + "ogg" | "oga" => Ok(Self::Ogg(OggFormat::Vorbis)), #[cfg(feature = "format-mp4")] "m4a" | "m4b" | "m4p" | "m4v" | "isom" | "mp4" => Ok(Self::Mp4), - _ => Err(LoftyError::UnsupportedFormat(ext.to_owned())), + _ => Err(LoftyError::UnsupportedFormat(ext.to_string())), } } - fn try_from_sig(data: &[u8]) -> Result { - if data.is_empty() { + + fn try_from_sig(data: &mut R) -> Result + where + R: Read + Seek, + { + if data.seek(SeekFrom::End(0))? == 0 { return Err(LoftyError::EmptyFile); } - match data[0] { - #[cfg(feature = "format-ape")] - 77 if data.starts_with(&MAC) => Ok(Self::Ape), - #[cfg(feature = "format-id3")] - 73 if data.starts_with(&ID3) => Ok(Self::Id3v2(Id3Format::Default)), - #[cfg(feature = "format-id3")] - 70 if data.starts_with(&FORM) => { - use byteorder::{BigEndian, LittleEndian, ReadBytesExt}; - use std::io::{Cursor, SeekFrom}; + data.seek(SeekFrom::Start(0))?; + + let mut sig = vec![0; 8]; + data.read_exact(&mut sig)?; + + data.seek(SeekFrom::Start(0))?; + + match sig.first().unwrap() { + #[cfg(feature = "format-ape")] + 77 if sig.starts_with(&MAC) => Ok(Self::Ape), + #[cfg(feature = "format-id3")] + 73 if sig.starts_with(&ID3) => Ok(Self::Id3v2(Id3Format::Default)), + #[cfg(feature = "format-id3")] + 70 if sig.starts_with(&FORM) => { + use byteorder::{BigEndian, LittleEndian, ReadBytesExt}; - let mut data = Cursor::new(data); let mut found_id3 = false; while let (Ok(fourcc), Ok(size)) = ( @@ -227,6 +258,8 @@ impl TagType { ))))?; } + data.seek(SeekFrom::Start(0))?; + if found_id3 { return Ok(Self::Id3v2(Id3Format::Form)); } @@ -235,27 +268,33 @@ impl TagType { Err(LoftyError::UnknownFormat) }, #[cfg(feature = "format-flac")] - 102 if data.starts_with(&FLAC) => Ok(Self::Vorbis(VorbisFormat::Flac)), + 102 if sig.starts_with(&FLAC) => Ok(Self::Ogg(OggFormat::Flac)), #[cfg(any(feature = "format-vorbis", feature = "format-opus"))] - 79 if data.starts_with(&OGGS) => { - if data[29..35] == VORBIS { - return Ok(Self::Vorbis(VorbisFormat::Ogg)); + 79 if sig.starts_with(&OGGS) => { + data.seek(SeekFrom::Start(28))?; + + let mut ident_sig = vec![0; 8]; + data.read_exact(&mut ident_sig)?; + + data.seek(SeekFrom::Start(0))?; + + if ident_sig[1..7] == VORBIS { + return Ok(Self::Ogg(OggFormat::Vorbis)); } - if data[28..36] == OPUSHEAD { - return Ok(Self::Vorbis(VorbisFormat::Opus)); + if ident_sig[..] == OPUSHEAD { + return Ok(Self::Ogg(OggFormat::Opus)); } Err(LoftyError::UnknownFormat) }, #[cfg(feature = "format-riff")] - 82 if data.starts_with(&RIFF) => { + 82 if sig.starts_with(&RIFF) => { #[cfg(feature = "format-id3")] { use byteorder::{LittleEndian, ReadBytesExt}; - use std::io::Cursor; - let mut data = Cursor::new(&data[12..]); + data.seek(SeekFrom::Start(12))?; let mut found_id3 = false; @@ -268,9 +307,11 @@ impl TagType { break; } - data.set_position(data.position() + u64::from(size)) + data.seek(SeekFrom::Current(i64::from(size)))?; } + data.seek(SeekFrom::Start(0))?; + if found_id3 { return Ok(Self::Id3v2(Id3Format::Riff)); } @@ -279,7 +320,7 @@ impl TagType { Ok(Self::RiffInfo) }, #[cfg(feature = "format-mp4")] - _ if data[4..8] == FTYP => Ok(Self::Mp4), + _ if sig[4..8] == FTYP => Ok(Self::Mp4), _ => Err(LoftyError::UnknownFormat), } }