#![crate_name = "infer"]
#![doc(html_root_url = "https://docs.rs/infer/latest")]
#![forbid(unsafe_code)]
#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(feature = "alloc")]
extern crate alloc;
mod map;
mod matchers;
#[cfg(feature = "alloc")]
use alloc::vec::Vec;
use core::fmt;
#[cfg(feature = "std")]
use std::fs::File;
#[cfg(feature = "std")]
use std::io::{self, Read};
#[cfg(feature = "std")]
use std::path::Path;
pub use map::MatcherType;
use map::{WrapMatcher, MATCHER_MAP};
pub use matchers::*;
pub type Matcher = fn(buf: &[u8]) -> bool;
#[derive(Copy, Clone)]
pub struct Type {
matcher_type: MatcherType,
mime_type: &'static str,
extension: &'static str,
matcher: WrapMatcher,
}
impl Type {
pub(crate) const fn new_static(
matcher_type: MatcherType,
mime_type: &'static str,
extension: &'static str,
matcher: WrapMatcher,
) -> Self {
Self {
matcher_type,
mime_type,
extension,
matcher,
}
}
pub fn new(
matcher_type: MatcherType,
mime_type: &'static str,
extension: &'static str,
matcher: Matcher,
) -> Self {
Self::new_static(matcher_type, mime_type, extension, WrapMatcher(matcher))
}
pub const fn matcher_type(&self) -> MatcherType {
self.matcher_type
}
pub const fn mime_type(&self) -> &'static str {
self.mime_type
}
pub const fn extension(&self) -> &'static str {
self.extension
}
fn matches(&self, buf: &[u8]) -> bool {
(self.matcher.0)(buf)
}
}
impl fmt::Debug for Type {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Type")
.field("matcher_type", &self.matcher_type)
.field("mime_type", &self.mime_type)
.field("extension", &self.extension)
.finish()
}
}
impl fmt::Display for Type {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Display::fmt(self.mime_type, f)
}
}
impl PartialEq for Type {
fn eq(&self, other: &Self) -> bool {
self.matcher_type == other.matcher_type
&& self.mime_type == other.mime_type
&& self.extension == other.extension
}
}
pub struct Infer {
#[cfg(feature = "alloc")]
mmap: Vec<Type>,
}
impl Infer {
pub const fn new() -> Infer {
#[cfg(feature = "alloc")]
return Infer { mmap: Vec::new() };
#[cfg(not(feature = "alloc"))]
return Infer {};
}
fn iter_matchers(&self) -> impl Iterator<Item = &Type> {
let mmap = MATCHER_MAP.iter();
#[cfg(feature = "alloc")]
return self.mmap.iter().chain(mmap);
#[cfg(not(feature = "alloc"))]
return mmap;
}
pub fn get(&self, buf: &[u8]) -> Option<Type> {
self.iter_matchers().find(|kind| kind.matches(buf)).copied()
}
#[cfg(feature = "std")]
pub fn get_from_path<P: AsRef<Path>>(&self, path: P) -> io::Result<Option<Type>> {
let file = File::open(path)?;
let limit = file
.metadata()
.map(|m| std::cmp::min(m.len(), 8192) as usize + 1)
.unwrap_or(0);
let mut bytes = Vec::with_capacity(limit);
file.take(8192).read_to_end(&mut bytes)?;
Ok(self.get(&bytes))
}
pub fn is(&self, buf: &[u8], extension: &str) -> bool {
self.iter_matchers()
.any(|kind| kind.extension() == extension && kind.matches(buf))
}
pub fn is_mime(&self, buf: &[u8], mime_type: &str) -> bool {
self.iter_matchers()
.any(|kind| kind.mime_type() == mime_type && kind.matches(buf))
}
pub fn is_supported(&self, extension: &str) -> bool {
self.iter_matchers()
.any(|kind| kind.extension() == extension)
}
pub fn is_mime_supported(&self, mime_type: &str) -> bool {
self.iter_matchers()
.any(|kind| kind.mime_type() == mime_type)
}
pub fn is_app(&self, buf: &[u8]) -> bool {
self.is_type(buf, MatcherType::App)
}
pub fn is_archive(&self, buf: &[u8]) -> bool {
self.is_type(buf, MatcherType::Archive)
}
pub fn is_audio(&self, buf: &[u8]) -> bool {
self.is_type(buf, MatcherType::Audio)
}
pub fn is_book(&self, buf: &[u8]) -> bool {
self.is_type(buf, MatcherType::Book)
}
pub fn is_document(&self, buf: &[u8]) -> bool {
self.is_type(buf, MatcherType::Doc)
}
pub fn is_font(&self, buf: &[u8]) -> bool {
self.is_type(buf, MatcherType::Font)
}
pub fn is_image(&self, buf: &[u8]) -> bool {
self.is_type(buf, MatcherType::Image)
}
pub fn is_video(&self, buf: &[u8]) -> bool {
self.is_type(buf, MatcherType::Video)
}
pub fn is_custom(&self, buf: &[u8]) -> bool {
self.is_type(buf, MatcherType::Custom)
}
#[cfg(feature = "alloc")]
pub fn add(&mut self, mime_type: &'static str, extension: &'static str, m: Matcher) {
self.mmap.push(Type::new_static(
MatcherType::Custom,
mime_type,
extension,
WrapMatcher(m),
));
}
fn is_type(&self, buf: &[u8], matcher_type: MatcherType) -> bool {
self.iter_matchers()
.any(|kind| kind.matcher_type() == matcher_type && kind.matches(buf))
}
}
impl Default for Infer {
fn default() -> Self {
Infer::new()
}
}
static INFER: Infer = Infer::new();
pub fn get(buf: &[u8]) -> Option<Type> {
INFER.get(buf)
}
#[cfg(feature = "std")]
pub fn get_from_path<P: AsRef<Path>>(path: P) -> io::Result<Option<Type>> {
INFER.get_from_path(path)
}
pub fn is(buf: &[u8], extension: &str) -> bool {
INFER.is(buf, extension)
}
pub fn is_mime(buf: &[u8], mime_type: &str) -> bool {
INFER.is_mime(buf, mime_type)
}
pub fn is_supported(extension: &str) -> bool {
INFER.is_supported(extension)
}
pub fn is_mime_supported(mime_type: &str) -> bool {
INFER.is_mime_supported(mime_type)
}
pub fn is_app(buf: &[u8]) -> bool {
INFER.is_app(buf)
}
pub fn is_archive(buf: &[u8]) -> bool {
INFER.is_archive(buf)
}
pub fn is_audio(buf: &[u8]) -> bool {
INFER.is_audio(buf)
}
pub fn is_book(buf: &[u8]) -> bool {
INFER.is_book(buf)
}
pub fn is_document(buf: &[u8]) -> bool {
INFER.is_document(buf)
}
pub fn is_font(buf: &[u8]) -> bool {
INFER.is_font(buf)
}
pub fn is_image(buf: &[u8]) -> bool {
INFER.is_image(buf)
}
pub fn is_video(buf: &[u8]) -> bool {
INFER.is_video(buf)
}
#[cfg(test)]
mod tests {
#[cfg(feature = "alloc")]
use super::Infer;
#[test]
fn test_get_unknown() {
let buf = [];
assert!(crate::get(&buf).is_none());
}
#[test]
fn test_get_jpeg() {
let buf = [0xFF, 0xD8, 0xFF, 0xAA];
let kind = crate::get(&buf).expect("file type is known");
assert_eq!(kind.extension(), "jpg");
assert_eq!(kind.mime_type(), "image/jpeg");
}
#[test]
fn test_matcher_type() {
let buf = [0xFF, 0xD8, 0xFF, 0xAA];
let kind = crate::get(&buf).expect("file type is known");
assert_eq!(kind.matcher_type(), crate::MatcherType::Image);
}
#[cfg(feature = "alloc")]
#[test]
fn test_custom_matcher_ordering() {
fn foo_matcher(buf: &[u8]) -> bool {
buf.len() > 2 && buf[0] == 0xFF && buf[1] == 0xD8 && buf[2] == 0xFF
}
fn bar_matcher(buf: &[u8]) -> bool {
buf.len() > 3 && buf[0] == 0x89 && buf[1] == 0x50 && buf[2] == 0x4E && buf[3] == 0x47
}
let mut info = Infer::new();
info.add("custom/foo", "foo", foo_matcher);
info.add("custom/bar", "bar", bar_matcher);
let buf_foo = &[0xFF, 0xD8, 0xFF];
let typ = info.get(buf_foo).expect("type is matched");
assert_eq!(typ.mime_type(), "custom/foo");
assert_eq!(typ.extension(), "foo");
let buf_bar = &[0x89, 0x50, 0x4E, 0x47];
let typ = info.get(buf_bar).expect("type is matched");
assert_eq!(typ.mime_type(), "custom/bar");
assert_eq!(typ.extension(), "bar");
}
}