use linkme::distributed_slice;
use std::{
collections::HashMap,
error::Error,
fmt::{Display, Formatter},
hash::Hash,
str::FromStr,
sync::OnceLock,
};
#[cfg_attr(feature = "diesel", derive(diesel::AsExpression, diesel::FromSqlRow))]
#[cfg_attr(feature = "diesel", diesel(sql_type = diesel::sql_types::Text))]
#[derive(Clone, Copy, Debug)]
pub struct MimeType {
pub mime: &'static str,
pub exts: &'static [&'static str],
}
impl MimeType {
#[inline]
pub fn as_str(self) -> &'static str {
self.mime
}
#[inline]
pub fn extensions(self) -> &'static [&'static str] {
self.exts
}
#[inline]
pub fn extension(self) -> &'static str {
self.exts[0]
}
pub fn from_extension(ext: &str) -> Option<Self> {
ensure_built();
let e = ext.trim().trim_start_matches('.').to_ascii_lowercase();
EXT_TO_MIME.get().unwrap().get(e.as_str()).copied().copied()
}
}
impl Display for MimeType {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(self.mime)
}
}
impl PartialEq for MimeType {
fn eq(&self, other: &Self) -> bool {
self.mime == other.mime
}
}
impl Eq for MimeType {}
impl Hash for MimeType {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.mime.hash(state)
}
}
#[derive(Debug)]
pub enum ParseError {
MissingSlash,
EmptyType,
EmptySubtype,
NotRegistered,
}
impl Display for ParseError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ParseError::MissingSlash => f.write_str("missing '/' separator"),
ParseError::EmptyType => f.write_str("empty type"),
ParseError::EmptySubtype => f.write_str("empty subtype"),
ParseError::NotRegistered => f.write_str("MIME type not registered"),
}
}
}
impl Error for ParseError {}
impl FromStr for MimeType {
type Err = ParseError;
fn from_str(src: &str) -> Result<Self, Self::Err> {
let head = src.split(';').next().unwrap_or("").trim();
let (ty, rest) = head.split_once('/').ok_or(ParseError::MissingSlash)?;
let ty = ty.trim();
if ty.is_empty() {
return Err(ParseError::EmptyType);
}
let rest = rest.trim();
if rest.is_empty() {
return Err(ParseError::EmptySubtype);
}
let norm = format!("{}/{}", ty.to_ascii_lowercase(), rest.to_ascii_lowercase());
lookup_mime_str(&norm).ok_or(ParseError::NotRegistered)
}
}
#[distributed_slice]
#[doc(hidden)]
pub static MIMETYPE_BLOCKS: [&'static [MimeType]] = [..];
static EXT_TO_MIME: OnceLock<HashMap<&'static str, &'static MimeType>> = OnceLock::new();
static MIME_TO_MIME: OnceLock<HashMap<&'static str, &'static MimeType>> = OnceLock::new();
fn ensure_built() {
if EXT_TO_MIME.get().is_some() {
return;
}
MIME_TO_MIME.get_or_init(|| {
let mut m = HashMap::new();
for block in MIMETYPE_BLOCKS {
for e in *block {
let prev = m.insert(e.mime, e);
assert!(prev.is_none(), "duplicate MIME '{}'", e.mime);
}
}
m
});
EXT_TO_MIME.get_or_init(|| {
let mut m = HashMap::new();
for block in MIMETYPE_BLOCKS {
for e in *block {
assert!(
!e.exts.is_empty(),
"MIME '{}' must have at least one extension",
e.mime
);
for &ext in e.exts {
m.entry(ext).or_insert(e);
}
}
}
m
});
}
#[inline]
fn lookup_mime_str(norm: &str) -> Option<MimeType> {
ensure_built();
MIME_TO_MIME.get().unwrap().get(norm).copied().copied()
}
#[doc(hidden)]
#[macro_export]
macro_rules! __mrmime_ext_to_str {
($e:ident) => {
stringify!($e)
};
($e:literal) => {
$e
};
}
#[macro_export]
macro_rules! register_mime {
(mod $modname:ident { $( $mime:literal => [ $( $ext:tt ),+ $(,)? ] ; )+ } ) => {
#[allow(non_snake_case)]
mod $modname {
#[linkme::distributed_slice($crate::MIMETYPE_BLOCKS)]
pub static __BLOCK: &'static [$crate::MimeType] = &[
$(
$crate::MimeType { mime: $mime, exts: &[ $( $crate::__mrmime_ext_to_str!($ext) ),+ ] },
)+
];
}
};
}
#[cfg(feature = "builtin")]
pub mod builtin;
#[cfg(feature = "serde")]
mod serde_impl;
#[cfg(feature = "diesel")]
mod diesel_impl;