//! Tools to create custom file resolvers //! //! For a full example of a custom resolver, see [this](https://github.com/Serial-ATA/lofty-rs/tree/main/examples/custom_resolver). use crate::error::Result; use crate::file::{AudioFile, FileType, TaggedFile}; use crate::probe::ParseOptions; use crate::tag::TagType; use std::collections::HashMap; use std::io::{Read, Seek}; use std::marker::PhantomData; use std::sync::{Arc, Mutex}; use once_cell::sync::Lazy; /// A custom file resolver /// /// This trait allows for the creation of custom [`FileType`]s, that can make use of /// lofty's API. Registering a `FileResolver` ([`register_custom_resolver`]) makes it possible /// to detect and read files using [`Probe`](crate::Probe). pub trait FileResolver: Send + Sync + AudioFile { /// The extension associated with the [`FileType`] without the '.' fn extension() -> Option<&'static str>; /// The primary [`TagType`] for the [`FileType`] fn primary_tag_type() -> TagType; /// The [`FileType`]'s supported [`TagType`]s fn supported_tag_types() -> &'static [TagType]; /// Attempts to guess the [`FileType`] from a portion of the file content /// /// NOTE: This will only provide (up to) the first 36 bytes of the file. /// This number is subject to change in the future, but it will never decrease. /// Such a change will **not** be considered breaking. fn guess(buf: &[u8]) -> Option; } // Just broken out to its own type to make `CUSTOM_RESOLVER`'s type shorter :) type ResolverMap = HashMap<&'static str, &'static dyn ObjectSafeFileResolver>; pub(crate) static CUSTOM_RESOLVERS: Lazy>> = Lazy::new(|| Arc::new(Mutex::new(HashMap::new()))); pub(crate) fn lookup_resolver(name: &'static str) -> &'static dyn ObjectSafeFileResolver { let res = CUSTOM_RESOLVERS.lock().unwrap(); if let Some(resolver) = res.get(name).copied() { return resolver; } panic!( "Encountered an unregistered custom `FileType` named `{}`", name ); } // A `Read + Seek` supertrait for use in [`ObjectSafeFileResolver::read_from`] pub(crate) trait SeekRead: Read + Seek {} impl SeekRead for T {} // `FileResolver` isn't object safe itself, so we need this wrapper trait pub(crate) trait ObjectSafeFileResolver: Send + Sync { fn extension(&self) -> Option<&'static str>; fn primary_tag_type(&self) -> TagType; fn supported_tag_types(&self) -> &'static [TagType]; fn guess(&self, buf: &[u8]) -> Option; // A mask for the `AudioFile::read_from` impl fn read_from( &self, reader: &mut dyn SeekRead, parse_options: ParseOptions, ) -> Result; } // A fake `FileResolver` implementer, so we don't need to construct the type in `register_custom_resolver` pub(crate) struct GhostlyResolver(PhantomData); impl ObjectSafeFileResolver for GhostlyResolver { fn extension(&self) -> Option<&'static str> { T::extension() } fn primary_tag_type(&self) -> TagType { T::primary_tag_type() } fn supported_tag_types(&self) -> &'static [TagType] { T::supported_tag_types() } fn guess(&self, buf: &[u8]) -> Option { T::guess(buf) } fn read_from( &self, reader: &mut dyn SeekRead, parse_options: ParseOptions, ) -> Result { Ok(::read_from(&mut Box::new(reader), parse_options)?.into()) } } /// Register a custom file resolver /// /// Provided a type and a name to associate it with, this will attempt /// to load them into the resolver collection. /// /// Conditions: /// * Both the resolver and name *must* be static. /// * `name` **must** match the name of your custom [`FileType`] variant (case sensitive!) /// /// # Panics /// /// * Attempting to register an existing name or type /// * See [`Mutex::lock`] pub fn register_custom_resolver(name: &'static str) { let mut res = CUSTOM_RESOLVERS.lock().unwrap(); assert!( res.iter().all(|(n, _)| *n != name), "Resolver `{}` already exists!", name ); let ghost = GhostlyResolver::(PhantomData::default()); let b: Box = Box::new(ghost); res.insert(name, Box::leak::<'static>(b)); } #[cfg(test)] mod tests { use crate::file::{FileType, TaggedFileExt}; use crate::id3::v2::ID3v2Tag; use crate::probe::ParseOptions; use crate::properties::FileProperties; use crate::resolve::{register_custom_resolver, FileResolver}; use crate::tag::TagType; use crate::traits::Accessor; use std::fs::File; use std::io::{Read, Seek}; use std::panic; use lofty_attr::LoftyFile; #[derive(LoftyFile, Default)] #[lofty(read_fn = "Self::read")] #[lofty(file_type = "MyFile")] struct MyFile { #[lofty(tag_type = "ID3v2")] id3v2_tag: Option, properties: FileProperties, } impl FileResolver for MyFile { fn extension() -> Option<&'static str> { Some("myfile") } fn primary_tag_type() -> TagType { TagType::ID3v2 } fn supported_tag_types() -> &'static [TagType] { &[TagType::ID3v2] } fn guess(buf: &[u8]) -> Option { if buf.starts_with(b"myfile") { return Some(FileType::Custom("MyFile")); } None } } impl MyFile { #[allow(clippy::unnecessary_wraps)] fn read( _reader: &mut R, _parse_options: ParseOptions, ) -> crate::error::Result { let mut tag = ID3v2Tag::default(); tag.set_artist(String::from("All is well!")); Ok(Self { id3v2_tag: Some(tag), properties: FileProperties::default(), }) } } #[test] fn custom_resolver() { register_custom_resolver::("MyFile"); let path = "examples/custom_resolver/test_asset.myfile"; let read = crate::read_from_path(path).unwrap(); assert_eq!(read.file_type(), FileType::Custom("MyFile")); let read_content = crate::read_from(&mut File::open(path).unwrap()).unwrap(); assert_eq!(read_content.file_type(), FileType::Custom("MyFile")); assert!( panic::catch_unwind(|| { register_custom_resolver::("MyFile"); }) .is_err(), "We didn't panic on double register!" ); } }