use crate::aac::AacFile; use crate::ape::ApeFile; use crate::error::Result; use crate::file::{AudioFile, FileType, TaggedFile}; use crate::flac::FlacFile; use crate::iff::aiff::AiffFile; use crate::iff::wav::WavFile; use crate::macros::err; use crate::mp4::Mp4File; use crate::mpeg::header::search_for_frame_sync; use crate::mpeg::MpegFile; use crate::ogg::opus::OpusFile; use crate::ogg::speex::SpeexFile; use crate::ogg::vorbis::VorbisFile; use crate::resolve::CUSTOM_RESOLVERS; use crate::wavpack::WavPackFile; use std::fs::File; use std::io::{BufReader, Cursor, Read, Seek, SeekFrom}; use std::path::Path; /// Options to control how Lofty parses a file #[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] #[non_exhaustive] pub struct ParseOptions { pub(crate) read_properties: bool, pub(crate) use_custom_resolvers: bool, pub(crate) parsing_mode: ParsingMode, } impl Default for ParseOptions { /// The default implementation for `ParseOptions` /// /// The defaults are as follows: /// /// ```rust,ignore /// ParseOptions { /// read_properties: true, /// use_custom_resolvers: true, /// parsing_mode: ParsingMode::Strict, /// } /// ``` fn default() -> Self { Self::new() } } impl ParseOptions { /// Creates a new `ParseOptions`, alias for `Default` implementation /// /// See also: [`ParseOptions::default`] /// /// # Examples /// /// ```rust /// use lofty::ParseOptions; /// /// let parsing_options = ParseOptions::new(); /// ``` #[must_use] pub const fn new() -> Self { Self { read_properties: true, use_custom_resolvers: true, parsing_mode: ParsingMode::BestAttempt, } } /// Whether or not to read the audio properties /// /// # Examples /// /// ```rust /// use lofty::ParseOptions; /// /// // By default, `read_properties` is enabled. Here, we don't want to read them. /// let parsing_options = ParseOptions::new().read_properties(false); /// ``` pub fn read_properties(&mut self, read_properties: bool) -> Self { self.read_properties = read_properties; *self } /// Whether or not to check registered custom resolvers /// /// See also: [`crate::resolve`] /// /// # Examples /// /// ```rust /// use lofty::ParseOptions; /// /// // By default, `use_custom_resolvers` is enabled. Here, we don't want to use them. /// let parsing_options = ParseOptions::new().use_custom_resolvers(false); /// ``` pub fn use_custom_resolvers(&mut self, use_custom_resolvers: bool) -> Self { self.use_custom_resolvers = use_custom_resolvers; *self } /// The parsing mode to use, see [`ParsingMode`] for details /// /// # Examples /// /// ```rust /// use lofty::{ParseOptions, ParsingMode}; /// /// // By default, `parsing_mode` is ParsingMode::Strict. Here, we don't need absolute correctness. /// let parsing_options = ParseOptions::new().parsing_mode(ParsingMode::Relaxed); /// ``` pub fn parsing_mode(&mut self, parsing_mode: ParsingMode) -> Self { self.parsing_mode = parsing_mode; *self } } /// The parsing strictness mode #[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Default)] #[non_exhaustive] pub enum ParsingMode { /// Will eagerly error on invalid input /// /// This mode will eagerly error on any non spec-compliant input. Strict, /// Default mode, less eager to error on recoverably malformed input /// /// This mode will attempt to fill in any holes where possible in otherwise valid, spec-compliant input. /// /// NOTE: A readable input does *not* necessarily make it writeable. #[default] BestAttempt, /// Least eager to error, may produce invalid output /// /// This mode will discard any invalid fields, and ignore the majority of non-fatal errors. /// /// Tags and properties read using this mode may come out missing items if the input is malformed. Relaxed, } /// A format agnostic reader /// /// This provides a way to determine the [`FileType`] of a reader, for when a concrete /// type is not known. /// /// ## Usage /// /// When reading from a path, the [`FileType`] will be inferred from the path, rather than the /// open file. /// /// ```rust,no_run /// # use lofty::{LoftyError, Probe}; /// # fn main() -> Result<(), LoftyError> { /// use lofty::FileType; /// /// let probe = Probe::open("path/to/my.mp3")?; /// /// // Inferred from the `mp3` extension /// assert_eq!(probe.file_type(), Some(FileType::Mpeg)); /// # Ok(()) /// # } /// ``` /// /// When a path isn't available, or is unreliable, content-based detection is also possible. /// /// ```rust,no_run /// # use lofty::{LoftyError, Probe}; /// # fn main() -> Result<(), LoftyError> { /// use lofty::FileType; /// /// // Our same path probe with a guessed file type /// let probe = Probe::open("path/to/my.mp3")?.guess_file_type()?; /// /// // Inferred from the file's content /// assert_eq!(probe.file_type(), Some(FileType::Mpeg)); /// # Ok(()) /// # } /// ``` /// /// Or with another reader /// /// ```rust /// # use lofty::{LoftyError, Probe}; /// # fn main() -> Result<(), LoftyError> { /// use lofty::FileType; /// use std::io::Cursor; /// /// static MAC_HEADER: &[u8; 3] = b"MAC"; /// /// let probe = Probe::new(Cursor::new(MAC_HEADER)).guess_file_type()?; /// /// // Inferred from the MAC header /// assert_eq!(probe.file_type(), Some(FileType::Ape)); /// # Ok(()) /// # } /// ``` pub struct Probe { inner: R, options: Option, f_ty: Option, } impl Probe { /// Create a new `Probe` /// /// Before creating a `Probe`, consider wrapping it in a [`BufReader`](std::io::BufReader) for better /// performance. /// /// # Examples /// /// ```rust /// use lofty::Probe; /// use std::fs::File; /// use std::io::BufReader; /// /// # fn main() -> lofty::Result<()> { /// # let path = "tests/files/assets/minimal/full_test.mp3"; /// let file = File::open(path)?; /// let reader = BufReader::new(file); /// /// let probe = Probe::new(reader); /// # Ok(()) } /// ``` #[must_use] pub const fn new(reader: R) -> Self { Self { inner: reader, options: None, f_ty: None, } } /// Create a new `Probe` with a specified [`FileType`] /// /// Before creating a `Probe`, consider wrapping it in a [`BufReader`](std::io::BufReader) for better /// performance. /// /// # Examples /// /// ```rust /// use lofty::{FileType, Probe}; /// use std::fs::File; /// use std::io::BufReader; /// /// # fn main() -> lofty::Result<()> { /// # let my_mp3_path = "tests/files/assets/minimal/full_test.mp3"; /// // We know the file is going to be an MP3, /// // so we can skip the format detection /// let file = File::open(my_mp3_path)?; /// let reader = BufReader::new(file); /// /// let probe = Probe::with_file_type(reader, FileType::Mpeg); /// # Ok(()) } /// ``` pub fn with_file_type(reader: R, file_type: FileType) -> Self { Self { inner: reader, options: None, f_ty: Some(file_type), } } /// Returns the current [`FileType`] /// /// # Examples /// /// ```rust /// use lofty::{FileType, Probe}; /// /// # fn main() -> lofty::Result<()> { /// # let reader = std::io::Cursor::new(&[]); /// let probe = Probe::new(reader); /// /// let file_type = probe.file_type(); /// # Ok(()) } /// ``` pub fn file_type(&self) -> Option { self.f_ty } /// Set the [`FileType`] with which to read the file /// /// # Examples /// /// ```rust /// use lofty::{FileType, Probe}; /// /// # fn main() -> lofty::Result<()> { /// # let reader = std::io::Cursor::new(&[]); /// let mut probe = Probe::new(reader); /// assert_eq!(probe.file_type(), None); /// /// probe.set_file_type(FileType::Mpeg); /// /// assert_eq!(probe.file_type(), Some(FileType::Mpeg)); /// # Ok(()) } /// ``` pub fn set_file_type(&mut self, file_type: FileType) { self.f_ty = Some(file_type) } /// Set the [`ParseOptions`] for the Probe /// /// # Examples /// /// ```rust /// use lofty::{ParseOptions, Probe}; /// /// # fn main() -> lofty::Result<()> { /// # let reader = std::io::Cursor::new(&[]); /// // By default, properties will be read. /// // In this example, we want to turn this off. /// let options = ParseOptions::new().read_properties(false); /// /// let probe = Probe::new(reader).options(options); /// # Ok(()) } /// ``` #[must_use] pub fn options(mut self, options: ParseOptions) -> Self { self.options = Some(options); self } /// Extract the reader /// /// # Examples /// /// ```rust /// use lofty::{FileType, Probe}; /// /// # fn main() -> lofty::Result<()> { /// # let reader = std::io::Cursor::new(&[]); /// let probe = Probe::new(reader); /// /// let reader = probe.into_inner(); /// # Ok(()) } /// ``` pub fn into_inner(self) -> R { self.inner } } impl Probe> { /// Opens a file for reading /// /// This will initially guess the [`FileType`] from the path, but /// this can be overwritten with [`Probe::guess_file_type`] or [`Probe::set_file_type`] /// /// # Errors /// /// * `path` does not exist /// /// # Examples /// /// ```rust,no_run /// use lofty::{FileType, Probe}; /// /// # fn main() -> lofty::Result<()> { /// let probe = Probe::open("path/to/my.mp3")?; /// /// // Guessed from the "mp3" extension, see `FileType::from_ext` /// assert_eq!(probe.file_type(), Some(FileType::Mpeg)); /// # Ok(()) } /// ``` pub fn open

(path: P) -> Result where P: AsRef, { let path = path.as_ref(); Ok(Self { inner: BufReader::new(File::open(path)?), options: None, f_ty: FileType::from_path(path), }) } } impl Probe { /// Attempts to get the [`FileType`] based on the data in the reader /// /// On success, the file type will be replaced /// /// # Errors /// /// All errors that occur within this function are [`std::io::Error`]. /// If an error does occur, there is likely an issue with the provided /// reader, and the entire `Probe` should be discarded. /// /// # Examples /// /// ```rust /// use lofty::{FileType, Probe}; /// /// # fn main() -> lofty::Result<()> { /// # let path = "tests/files/assets/minimal/full_test.mp3"; /// # let file = std::fs::File::open(path)?; /// # let reader = std::io::BufReader::new(file); /// let probe = Probe::new(reader).guess_file_type()?; /// /// // Determined the file is MP3 from the content /// assert_eq!(probe.file_type(), Some(FileType::Mpeg)); /// # Ok(()) } /// ``` pub fn guess_file_type(mut self) -> std::io::Result { let f_ty = self.guess_inner()?; self.f_ty = f_ty.or(self.f_ty); Ok(self) } #[allow(clippy::shadow_unrelated)] fn guess_inner(&mut self) -> std::io::Result> { // temporary buffer for storing 36 bytes // (36 is just a guess as to how long the data for estimating the file type might be) let mut buf = [0; 36]; let starting_position = self.inner.stream_position()?; // Read (up to) 36 bytes let buf_len = std::io::copy( &mut self.inner.by_ref().take(buf.len() as u64), &mut Cursor::new(&mut buf[..]), )? as usize; self.inner.seek(SeekFrom::Start(starting_position))?; // Guess the file type by using these 36 bytes match FileType::from_buffer_inner(&buf[..buf_len]) { // We were able to determine a file type (Some(f_ty), _) => Ok(Some(f_ty)), // The file starts with an ID3v2 tag; this means other data can follow (e.g. APE or MP3 frames) (None, Some(id3_len)) => { // `id3_len` is the size of the tag, not including the header (10 bytes) let position_after_id3_block = self .inner .seek(SeekFrom::Current(i64::from(10 + id3_len)))?; // try to guess the file type after the ID3 block by inspecting the first 4 bytes let mut ident = [0; 4]; std::io::copy( &mut self.inner.by_ref().take(ident.len() as u64), &mut Cursor::new(&mut ident[..]), )?; self.inner.seek(SeekFrom::Start(position_after_id3_block))?; let file_type_after_id3_block = match &ident { [b'M', b'A', b'C', ..] => Ok(Some(FileType::Ape)), b"fLaC" => Ok(Some(FileType::Flac)), // Search for a frame sync, which may be preceded by junk _ if search_for_frame_sync(&mut self.inner)?.is_some() => { // Seek back to the start of the frame sync to check if we are dealing with // an AAC or MPEG file. See `FileType::quick_type_guess` for explanation. self.inner.seek(SeekFrom::Current(-2))?; let mut buf = [0; 2]; self.inner.read_exact(&mut buf)?; if buf[1] & 0b10000 > 0 && buf[1] & 0b110 == 0 { Ok(Some(FileType::Aac)) } else { Ok(Some(FileType::Mpeg)) } }, _ => Ok(None), }; // before returning any result for a file type, seek back to the front self.inner.seek(SeekFrom::Start(starting_position))?; file_type_after_id3_block }, _ => { if let Ok(lock) = CUSTOM_RESOLVERS.lock() { #[allow(clippy::significant_drop_in_scrutinee)] for (_, resolve) in lock.iter() { if let ret @ Some(_) = resolve.guess(&buf[..buf_len]) { return Ok(ret); } } } Ok(None) }, } } /// Attempts to extract a [`TaggedFile`] from the reader /// /// If `read_properties` is false, the properties will be zeroed out. /// /// # Errors /// /// * No file type /// - This expects the file type to have been set already, either with /// [`Probe::guess_file_type`] or [`Probe::set_file_type`]. When reading from /// paths, this is not necessary. /// * The reader contains invalid data /// /// # Panics /// /// If an unregistered `FileType` ([`FileType::Custom`]) is encountered. See [`register_custom_resolver`](crate::resolve::register_custom_resolver). /// /// # Examples /// /// ```rust /// use lofty::{FileType, Probe}; /// /// # fn main() -> lofty::Result<()> { /// # let path = "tests/files/assets/minimal/full_test.mp3"; /// # let file = std::fs::File::open(path)?; /// # let reader = std::io::BufReader::new(file); /// let probe = Probe::new(reader).guess_file_type()?; /// /// let parsed_file = probe.read()?; /// # Ok(()) } /// ``` pub fn read(mut self) -> Result { let reader = &mut self.inner; let options = self.options.unwrap_or_default(); match self.f_ty { Some(f_type) => Ok(match f_type { FileType::Aac => AacFile::read_from(reader, options)?.into(), FileType::Aiff => AiffFile::read_from(reader, options)?.into(), FileType::Ape => ApeFile::read_from(reader, options)?.into(), FileType::Flac => FlacFile::read_from(reader, options)?.into(), FileType::Mpeg => MpegFile::read_from(reader, options)?.into(), FileType::Opus => OpusFile::read_from(reader, options)?.into(), FileType::Vorbis => VorbisFile::read_from(reader, options)?.into(), FileType::Wav => WavFile::read_from(reader, options)?.into(), FileType::Mp4 => Mp4File::read_from(reader, options)?.into(), FileType::Speex => SpeexFile::read_from(reader, options)?.into(), FileType::WavPack => WavPackFile::read_from(reader, options)?.into(), FileType::Custom(c) => { if !options.use_custom_resolvers { err!(UnknownFormat) } let resolver = crate::resolve::lookup_resolver(c); resolver.read_from(reader, options)? }, }), None => err!(UnknownFormat), } } } /// Read a [`TaggedFile`] from a [File] /// /// # Errors /// /// See: /// /// * [`Probe::guess_file_type`] /// * [`Probe::read`] /// /// # Examples /// /// ```rust /// use lofty::read_from; /// use std::fs::File; /// /// # fn main() -> lofty::Result<()> { /// # let path = "tests/files/assets/minimal/full_test.mp3"; /// let mut file = File::open(path)?; /// /// let parsed_file = read_from(&mut file)?; /// # Ok(()) } /// ``` pub fn read_from(file: &mut File) -> Result { Probe::new(BufReader::new(file)).guess_file_type()?.read() } /// Read a [`TaggedFile`] from a path /// /// NOTE: This will determine the [`FileType`] from the extension /// /// # Errors /// /// See: /// /// * [`Probe::open`] /// * [`Probe::read`] /// /// # Examples /// /// ```rust /// use lofty::read_from_path; /// /// # fn main() -> lofty::Result<()> { /// # let path = "tests/files/assets/minimal/full_test.mp3"; /// let parsed_file = read_from_path(path)?; /// # Ok(()) } /// ``` pub fn read_from_path

(path: P) -> Result where P: AsRef, { Probe::open(path)?.read() } #[cfg(test)] mod tests { use crate::{FileType, Probe}; use std::fs::File; #[test] fn mp3_id3v2_trailing_junk() { // test data that contains 4 bytes of junk (0x20) between the ID3 portion and the first MP3 frame let data: [&[u8]; 4] = [ // ID3v2.3 header (10 bytes) &[0x49, 0x44, 0x33, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23], // TALB frame &[ 0x54, 0x41, 0x4C, 0x42, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x01, 0xFF, 0xFE, 0x61, 0x00, 0x61, 0x00, 0x61, 0x00, 0x61, 0x00, 0x61, 0x00, 0x61, 0x00, 0x61, 0x00, 0x61, 0x00, 0x61, 0x00, 0x61, 0x00, 0x61, 0x00, ], // 4 bytes of junk &[0x20, 0x20, 0x20, 0x20], // start of MP3 frame (not all bytes are shown in this slice) &[ 0xFF, 0xFB, 0x50, 0xC4, 0x00, 0x03, 0xC0, 0x00, 0x01, 0xA4, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x34, 0x80, 0x00, 0x00, 0x04, ], ]; let data: Vec = data.into_iter().flatten().copied().collect(); let data = std::io::Cursor::new(&data); let probe = Probe::new(data).guess_file_type().unwrap(); assert_eq!(probe.file_type(), Some(FileType::Mpeg)); } fn test_probe(path: &str, expected_file_type_guess: FileType) { test_probe_file(path, expected_file_type_guess); test_probe_path(path, expected_file_type_guess); } // Test from file contents fn test_probe_file(path: &str, expected_file_type_guess: FileType) { let mut f = File::open(path).unwrap(); let probe = Probe::new(&mut f).guess_file_type().unwrap(); assert_eq!(probe.file_type(), Some(expected_file_type_guess)); } // Test from file extension fn test_probe_path(path: &str, expected_file_type_guess: FileType) { let probe = Probe::open(path).unwrap(); assert_eq!(probe.file_type(), Some(expected_file_type_guess)); } #[test] fn probe_aac() { test_probe("tests/files/assets/minimal/untagged.aac", FileType::Aac); } #[test] fn probe_aac_with_id3v2() { test_probe("tests/files/assets/minimal/full_test.aac", FileType::Aac); } #[test] fn probe_aiff() { test_probe("tests/files/assets/minimal/full_test.aiff", FileType::Aiff); } #[test] fn probe_ape_with_id3v2() { test_probe("tests/files/assets/minimal/full_test.ape", FileType::Ape); } #[test] fn probe_flac() { test_probe("tests/files/assets/minimal/full_test.flac", FileType::Flac); } #[test] fn probe_flac_with_id3v2() { test_probe("tests/files/assets/flac_with_id3v2.flac", FileType::Flac); } #[test] fn probe_mp3_with_id3v2() { test_probe("tests/files/assets/minimal/full_test.mp3", FileType::Mpeg); } #[test] fn probe_vorbis() { test_probe("tests/files/assets/minimal/full_test.ogg", FileType::Vorbis); } #[test] fn probe_opus() { test_probe("tests/files/assets/minimal/full_test.opus", FileType::Opus); } #[test] fn probe_speex() { test_probe("tests/files/assets/minimal/full_test.spx", FileType::Speex); } #[test] fn probe_mp4() { test_probe( "tests/files/assets/minimal/m4a_codec_aac.m4a", FileType::Mp4, ); } #[test] fn probe_wav() { test_probe( "tests/files/assets/minimal/wav_format_pcm.wav", FileType::Wav, ); } }