use std::{
collections::HashMap,
path::Path,
sync::{Mutex, OnceLock},
};
#[derive(Debug, Clone, PartialEq)]
pub struct Info {
pub extension: String,
pub content_type: String,
pub encoding: String,
}
impl Info {
const BINARY_ENCODINGS: &'static [&'static str] = &["base64", "8bit"];
pub fn new(line: &str) -> Option<Self> {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 3 {
Some(Info {
extension: parts[0].to_string(),
content_type: parts[1].to_string(),
encoding: parts[2].to_string(),
})
} else {
None
}
}
pub fn is_binary(&self) -> bool {
Self::BINARY_ENCODINGS.contains(&self.encoding.as_str())
}
}
pub struct Db {
ext_db: HashMap<String, Info>,
content_type_db: HashMap<String, Info>,
}
impl Db {
pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
let mut db = Db {
ext_db: HashMap::new(),
content_type_db: HashMap::new(),
};
db.load_ext_db()?;
db.load_content_type_db()?;
Ok(db)
}
fn load_ext_db(&mut self) -> Result<(), Box<dyn std::error::Error>> {
let db_content = include_str!("db/ext_mime.db");
for line in db_content.lines() {
if let Some(info) = Info::new(line) {
self.ext_db.insert(info.extension.clone(), info);
}
}
Ok(())
}
fn load_content_type_db(&mut self) -> Result<(), Box<dyn std::error::Error>> {
let db_content = include_str!("db/content_type_mime.db");
for line in db_content.lines() {
if let Some(info) = Info::new(line) {
self.content_type_db.insert(info.content_type.clone(), info);
}
}
Ok(())
}
pub fn lookup_by_extension(&self, extension: &str) -> Option<&Info> {
self.ext_db
.get(extension)
.or_else(|| self.ext_db.get(&extension.to_lowercase()))
}
pub fn lookup_by_content_type(&self, content_type: &str) -> Option<&Info> {
self.content_type_db.get(content_type)
}
pub fn lookup_by_filename(&self, filename: &str) -> Option<&Info> {
let path = Path::new(filename);
if let Some(ext) = path.extension() {
if let Some(ext_str) = ext.to_str() {
return self.lookup_by_extension(ext_str);
}
}
None
}
}
static DB: OnceLock<Mutex<Db>> = OnceLock::new();
fn get_db() -> &'static Mutex<Db> {
DB.get_or_init(|| Mutex::new(Db::new().expect("Failed to initialize MIME database")))
}
pub fn lookup_by_filename(filename: &str) -> Option<Info> {
let db = get_db().lock().unwrap();
db.lookup_by_filename(filename).cloned()
}
pub fn lookup_by_extension(extension: &str) -> Option<Info> {
let db = get_db().lock().unwrap();
db.lookup_by_extension(extension).cloned()
}
pub fn lookup_by_content_type(content_type: &str) -> Option<Info> {
let db = get_db().lock().unwrap();
db.lookup_by_content_type(content_type).cloned()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_info_creation() {
let info = Info::new("pdf application/pdf base64").unwrap();
assert_eq!(info.extension, "pdf");
assert_eq!(info.content_type, "application/pdf");
assert_eq!(info.encoding, "base64");
assert!(info.is_binary());
}
#[test]
fn test_extension() {
if let Some(info) = lookup_by_extension("zip") {
assert_eq!(info.content_type, "application/zip");
}
}
#[test]
fn test_mixed_case() {
if let Some(info) = lookup_by_filename("a.GTM") {
assert_eq!(info.content_type, "application/vnd.groove-tool-message");
}
if let Some(info) = lookup_by_extension("ZiP") {
assert_eq!(info.content_type, "application/zip");
}
}
#[test]
fn test_content_type_lookups() {
if let Some(info) = lookup_by_filename("a.123") {
assert_eq!(info.content_type, "application/vnd.lotus-1-2-3");
}
if let Some(info) = lookup_by_filename("a.Z") {
assert_eq!(info.content_type, "application/x-compressed");
}
if let Some(info) = lookup_by_filename("a.gtm") {
assert_eq!(info.content_type, "application/vnd.groove-tool-message");
}
if let Some(info) = lookup_by_filename("a.zmm") {
assert_eq!(
info.content_type,
"application/vnd.HandHeld-Entertainment+xml"
);
}
if let Some(info) = lookup_by_filename("x.csv") {
assert_eq!(info.content_type, "text/csv");
}
if let Some(info) = lookup_by_filename("x.mda") {
assert_eq!(info.content_type, "application/x-msaccess");
}
assert!(lookup_by_filename("a.frog").is_none());
}
#[test]
fn test_binary() {
if let Some(info) = lookup_by_filename("a.z") {
assert!(info.is_binary());
}
if let Some(info) = lookup_by_filename("a.Z") {
assert!(info.is_binary());
}
if let Some(info) = lookup_by_filename("a.txt") {
assert!(!info.is_binary());
}
assert!(lookup_by_filename("a.frog").is_none());
}
#[test]
fn test_binary_content_type() {
if let Some(info) = lookup_by_content_type("application/x-compressed") {
assert!(info.is_binary());
}
assert!(lookup_by_content_type("something-fake").is_none());
if let Some(info) = lookup_by_content_type("text/plain") {
assert!(!info.is_binary());
}
}
#[test]
fn test_prioritize_extensions_correctly() {
if let Some(info) = lookup_by_content_type("text/plain") {
assert_eq!(info.extension, "txt");
}
}
#[test]
fn test_lookup_by_filename() {
if let Some(info) = lookup_by_filename("document.pdf") {
assert_eq!(info.content_type, "application/pdf");
}
}
#[test]
fn test_lookup_by_extension() {
if let Some(info) = lookup_by_extension("pdf") {
assert_eq!(info.content_type, "application/pdf");
}
}
}