use std::collections::{
HashMap,
HashSet,
};
use std::sync::{
LazyLock,
RwLock,
};
use qubit_config::{
Config,
options::{
CollectionReadOptions,
ConfigReadOptions,
EmptyItemPolicy,
},
};
use crate::{
CONFIG_MEDIA_STREAM_CLASSIFIER_DEFAULT,
CONFIG_MIME_AMBIGUOUS_MIME_MAPPING,
CONFIG_MIME_DETECTOR_DEFAULT,
CONFIG_MIME_DETECTOR_FALLBACKS,
CONFIG_MIME_ENABLE_PRECISE_DETECTION,
CONFIG_MIME_PRECISE_DETECTION_PATTERNS,
DEFAULT_ENABLE_PRECISE_DETECTION,
DEFAULT_MEDIA_STREAM_CLASSIFIER,
DEFAULT_MIME_DETECTOR,
DEFAULT_MIME_DETECTOR_FALLBACKS,
ENV_MEDIA_STREAM_CLASSIFIER_DEFAULT,
ENV_MIME_DETECTOR_AMBIGUOUS_MIME_MAPPING,
ENV_MIME_DETECTOR_DEFAULT,
ENV_MIME_DETECTOR_ENABLE_PRECISE_DETECTION,
ENV_MIME_DETECTOR_FALLBACKS,
ENV_MIME_DETECTOR_PRECISE_DETECTION_PATTERNS,
MimeResult,
};
#[derive(Debug, Clone)]
pub struct MimeConfig {
mime_detector_default: String,
mime_detector_fallbacks: Vec<String>,
media_stream_classifier_default: String,
enable_precise_detection: bool,
precise_detection_patterns: HashSet<String>,
ambiguous_mime_mapping: HashMap<String, [String; 2]>,
}
static DEFAULT_MIME_CONFIG: LazyLock<RwLock<MimeConfig>> =
LazyLock::new(|| RwLock::new(MimeConfig::load()));
static VALUE_READ_OPTIONS: LazyLock<ConfigReadOptions> =
LazyLock::new(ConfigReadOptions::env_friendly);
static LIST_READ_OPTIONS: LazyLock<ConfigReadOptions> = LazyLock::new(|| {
ConfigReadOptions::env_friendly().with_collection_options(
CollectionReadOptions::default()
.with_split_scalar_strings(true)
.with_delimiters([',', ';'])
.with_trim_items(true)
.with_empty_item_policy(EmptyItemPolicy::Skip),
)
});
static MAPPING_READ_OPTIONS: LazyLock<ConfigReadOptions> = LazyLock::new(|| {
ConfigReadOptions::env_friendly().with_collection_options(
CollectionReadOptions::default()
.with_split_scalar_strings(true)
.with_delimiters([';'])
.with_trim_items(true)
.with_empty_item_policy(EmptyItemPolicy::Skip),
)
});
static DEFAULT_PRECISE_DETECTION_PATTERNS: &[&str] = &["webm", "ogg"];
static DEFAULT_AMBIGUOUS_MIME_MAPPING_ENTRIES: &[&str] =
&["webm:video/webm,audio/webm", "ogg:video/ogg,audio/ogg"];
impl MimeConfig {
pub fn load() -> Self {
match Self::from_env() {
Ok(config) => config,
Err(_) => Self::builtin_default(),
}
}
pub fn from_config(config: &Config) -> MimeResult<Self> {
let mime_detector_default = config.get_any_or_with(
[CONFIG_MIME_DETECTOR_DEFAULT, ENV_MIME_DETECTOR_DEFAULT],
DEFAULT_MIME_DETECTOR.to_owned(),
&VALUE_READ_OPTIONS,
)?;
let mime_detector_fallbacks = config.get_any_or_with(
[CONFIG_MIME_DETECTOR_FALLBACKS, ENV_MIME_DETECTOR_FALLBACKS],
fallback_defaults(),
&LIST_READ_OPTIONS,
)?;
let media_stream_classifier_default = config.get_any_or_with(
[
CONFIG_MEDIA_STREAM_CLASSIFIER_DEFAULT,
ENV_MEDIA_STREAM_CLASSIFIER_DEFAULT,
],
DEFAULT_MEDIA_STREAM_CLASSIFIER.to_owned(),
&VALUE_READ_OPTIONS,
)?;
let enable_precise_detection = config.get_any_or_with(
[
CONFIG_MIME_ENABLE_PRECISE_DETECTION,
ENV_MIME_DETECTOR_ENABLE_PRECISE_DETECTION,
],
DEFAULT_ENABLE_PRECISE_DETECTION,
&VALUE_READ_OPTIONS,
)?;
let precise_detection_patterns = config.get_any_or_with(
[
CONFIG_MIME_PRECISE_DETECTION_PATTERNS,
ENV_MIME_DETECTOR_PRECISE_DETECTION_PATTERNS,
],
DEFAULT_PRECISE_DETECTION_PATTERNS,
&VALUE_READ_OPTIONS,
)?;
let ambiguous_mime_mapping = config.get_any_or_with(
[
CONFIG_MIME_AMBIGUOUS_MIME_MAPPING,
ENV_MIME_DETECTOR_AMBIGUOUS_MIME_MAPPING,
],
DEFAULT_AMBIGUOUS_MIME_MAPPING_ENTRIES,
&MAPPING_READ_OPTIONS,
)?;
Ok(Self {
mime_detector_default,
mime_detector_fallbacks: normalize_detector_names(mime_detector_fallbacks),
media_stream_classifier_default,
enable_precise_detection,
precise_detection_patterns: normalize_patterns(precise_detection_patterns),
ambiguous_mime_mapping: build_ambiguous_mime_mapping(ambiguous_mime_mapping),
})
}
pub fn from_env() -> MimeResult<Self> {
let config = Config::from_env()?;
Self::from_config(&config)
}
pub fn set_default(config: Self) {
let mut guard = DEFAULT_MIME_CONFIG
.write()
.expect("default MIME configuration lock should not be poisoned");
*guard = config;
}
pub fn reload_default(config: &Config) -> MimeResult<()> {
Self::set_default(Self::from_config(config)?);
Ok(())
}
pub fn reload_default_from_env() -> MimeResult<()> {
Self::set_default(Self::from_env()?);
Ok(())
}
pub fn mime_detector_default(&self) -> &str {
&self.mime_detector_default
}
pub fn mime_detector_fallbacks(&self) -> &[String] {
&self.mime_detector_fallbacks
}
pub fn media_stream_classifier_default(&self) -> &str {
&self.media_stream_classifier_default
}
pub fn enable_precise_detection(&self) -> bool {
self.enable_precise_detection
}
pub fn precise_detection_patterns(&self) -> &HashSet<String> {
&self.precise_detection_patterns
}
pub fn ambiguous_mime_mapping(&self) -> &HashMap<String, [String; 2]> {
&self.ambiguous_mime_mapping
}
fn builtin_default() -> Self {
Self {
mime_detector_default: DEFAULT_MIME_DETECTOR.to_owned(),
mime_detector_fallbacks: fallback_defaults(),
media_stream_classifier_default: DEFAULT_MEDIA_STREAM_CLASSIFIER.to_owned(),
enable_precise_detection: DEFAULT_ENABLE_PRECISE_DETECTION,
precise_detection_patterns: normalize_patterns(
DEFAULT_PRECISE_DETECTION_PATTERNS
.iter()
.map(|pattern| pattern.to_string())
.collect(),
),
ambiguous_mime_mapping: build_ambiguous_mime_mapping(
DEFAULT_AMBIGUOUS_MIME_MAPPING_ENTRIES
.iter()
.map(|entry| entry.to_string())
.collect(),
),
}
}
}
impl Default for MimeConfig {
fn default() -> Self {
DEFAULT_MIME_CONFIG
.read()
.expect("default MIME configuration lock should not be poisoned")
.clone()
}
}
fn fallback_defaults() -> Vec<String> {
DEFAULT_MIME_DETECTOR_FALLBACKS
.split(',')
.map(str::trim)
.filter(|name| !name.is_empty())
.map(str::to_owned)
.collect()
}
fn normalize_detector_names(names: Vec<String>) -> Vec<String> {
names
.into_iter()
.map(|name| name.trim().to_owned())
.filter(|name| !name.is_empty())
.collect()
}
fn normalize_patterns(patterns: Vec<String>) -> HashSet<String> {
patterns
.into_iter()
.map(|pattern| pattern.trim().to_owned())
.filter(|pattern| !pattern.is_empty())
.map(|pattern| pattern.trim_start_matches('.').to_ascii_lowercase())
.collect()
}
fn build_ambiguous_mime_mapping(entries: Vec<String>) -> HashMap<String, [String; 2]> {
entries
.into_iter()
.filter_map(|entry| {
let (extension, mime_types) = entry.split_once(':')?;
let mut mime_types = mime_types.split(',').map(str::trim);
let video_type = mime_types.next()?.to_owned();
let audio_type = mime_types.next()?.to_owned();
if extension.trim().is_empty()
|| video_type.is_empty()
|| audio_type.is_empty()
|| mime_types.next().is_some()
{
None
} else {
Some((
extension
.trim()
.trim_start_matches('.')
.to_ascii_lowercase(),
[video_type, audio_type],
))
}
})
.collect()
}