lofty-rs/src/resolve.rs

212 lines
5.9 KiB
Rust

//! 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<FileType>;
}
// 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<Arc<Mutex<ResolverMap>>> =
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<T: Seek + Read> 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<FileType>;
// A mask for the `AudioFile::read_from` impl
fn read_from(
&self,
reader: &mut dyn SeekRead,
parse_options: ParseOptions,
) -> Result<TaggedFile>;
}
// A fake `FileResolver` implementer, so we don't need to construct the type in `register_custom_resolver`
pub(crate) struct GhostlyResolver<T: 'static>(PhantomData<T>);
impl<T: FileResolver> ObjectSafeFileResolver for GhostlyResolver<T> {
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<FileType> {
T::guess(buf)
}
fn read_from(
&self,
reader: &mut dyn SeekRead,
parse_options: ParseOptions,
) -> Result<TaggedFile> {
Ok(<T as AudioFile>::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<T: FileResolver + 'static>(name: &'static str) {
let mut res = CUSTOM_RESOLVERS.lock().unwrap();
assert!(
res.iter().all(|(n, _)| *n != name),
"Resolver `{}` already exists!",
name
);
let ghost = GhostlyResolver::<T>(PhantomData::default());
let b: Box<dyn ObjectSafeFileResolver> = 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<ID3v2Tag>,
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<FileType> {
if buf.starts_with(b"myfile") {
return Some(FileType::Custom("MyFile"));
}
None
}
}
impl MyFile {
#[allow(clippy::unnecessary_wraps)]
fn read<R: Read + Seek + ?Sized>(
_reader: &mut R,
_parse_options: ParseOptions,
) -> crate::error::Result<Self> {
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>("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>("MyFile");
})
.is_err(),
"We didn't panic on double register!"
);
}
}