1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
//! 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);
	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!"
		);
	}
}