use std::collections::{HashMap, HashSet};
use std::sync::{OnceLock, RwLock};
use qubit_config::Config;
use crate::{
CONFIG_MEDIA_STREAM_CLASSIFIER_DEFAULT, CONFIG_MIME_AMBIGUOUS_MIME_MAPPING,
CONFIG_MIME_DETECTOR_DEFAULT, CONFIG_MIME_ENABLE_PRECISE_DETECTION,
CONFIG_MIME_PRECISE_DETECTION_PATTERNS, DEFAULT_AMBIGUOUS_MIME_MAPPING,
DEFAULT_ENABLE_PRECISE_DETECTION, DEFAULT_MEDIA_STREAM_CLASSIFIER, DEFAULT_MIME_DETECTOR,
DEFAULT_PRECISE_DETECTION_PATTERNS, ENV_MEDIA_STREAM_CLASSIFIER_DEFAULT,
ENV_MIME_DETECTOR_AMBIGUOUS_MIME_MAPPING, ENV_MIME_DETECTOR_DEFAULT,
ENV_MIME_DETECTOR_ENABLE_PRECISE_DETECTION, ENV_MIME_DETECTOR_PRECISE_DETECTION_PATTERNS,
MimeResult,
};
static DEFAULT_MIME_CONFIG: OnceLock<RwLock<MimeConfig>> = OnceLock::new();
#[derive(Debug, Clone)]
pub struct MimeConfig {
mime_detector_default: String,
media_stream_classifier_default: String,
enable_precise_detection: bool,
precise_detection_patterns: HashSet<String>,
ambiguous_mime_mapping: HashMap<String, [String; 2]>,
}
impl MimeConfig {
pub fn load() -> Self {
match Self::from_env() {
Ok(config) => config,
Err(_) => Self::builtin_default(),
}
}
pub fn new(
mime_detector_default: &str,
media_stream_classifier_default: &str,
enable_precise_detection: bool,
precise_detection_patterns: &str,
ambiguous_mime_mapping: &str,
) -> Self {
Self {
mime_detector_default: normalize_selector(mime_detector_default, DEFAULT_MIME_DETECTOR),
media_stream_classifier_default: normalize_selector(
media_stream_classifier_default,
DEFAULT_MEDIA_STREAM_CLASSIFIER,
),
enable_precise_detection,
precise_detection_patterns: parse_patterns(precise_detection_patterns),
ambiguous_mime_mapping: parse_mapping(ambiguous_mime_mapping),
}
}
pub fn from_config(config: &Config) -> MimeResult<Self> {
let mime_detector_default = read_string(
config,
&[CONFIG_MIME_DETECTOR_DEFAULT, ENV_MIME_DETECTOR_DEFAULT],
DEFAULT_MIME_DETECTOR,
)?;
let media_stream_classifier_default = read_string(
config,
&[
CONFIG_MEDIA_STREAM_CLASSIFIER_DEFAULT,
ENV_MEDIA_STREAM_CLASSIFIER_DEFAULT,
],
DEFAULT_MEDIA_STREAM_CLASSIFIER,
)?;
let enable_precise_detection = read_bool(
config,
&[
CONFIG_MIME_ENABLE_PRECISE_DETECTION,
ENV_MIME_DETECTOR_ENABLE_PRECISE_DETECTION,
],
DEFAULT_ENABLE_PRECISE_DETECTION,
)?;
let precise_detection_patterns = read_string(
config,
&[
CONFIG_MIME_PRECISE_DETECTION_PATTERNS,
ENV_MIME_DETECTOR_PRECISE_DETECTION_PATTERNS,
],
DEFAULT_PRECISE_DETECTION_PATTERNS,
)?;
let ambiguous_mime_mapping = read_string(
config,
&[
CONFIG_MIME_AMBIGUOUS_MIME_MAPPING,
ENV_MIME_DETECTOR_AMBIGUOUS_MIME_MAPPING,
],
DEFAULT_AMBIGUOUS_MIME_MAPPING,
)?;
Ok(Self::new(
&mime_detector_default,
&media_stream_classifier_default,
enable_precise_detection,
&precise_detection_patterns,
&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 cell = default_config_cell();
match cell.write() {
Ok(mut guard) => *guard = config,
Err(poisoned) => *poisoned.into_inner() = 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 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::new(
DEFAULT_MIME_DETECTOR,
DEFAULT_MEDIA_STREAM_CLASSIFIER,
DEFAULT_ENABLE_PRECISE_DETECTION,
DEFAULT_PRECISE_DETECTION_PATTERNS,
DEFAULT_AMBIGUOUS_MIME_MAPPING,
)
}
#[cfg(coverage)]
fn from_raw_values(
mime_detector_default: Option<String>,
media_stream_classifier_default: Option<String>,
enable_precise_detection: Option<String>,
precise_detection_patterns: Option<String>,
ambiguous_mime_mapping: Option<String>,
) -> Self {
let mime_detector_default =
mime_detector_default.unwrap_or_else(|| DEFAULT_MIME_DETECTOR.to_owned());
let media_stream_classifier_default = media_stream_classifier_default
.unwrap_or_else(|| DEFAULT_MEDIA_STREAM_CLASSIFIER.to_owned());
let enable_precise_detection = enable_precise_detection
.as_deref()
.and_then(parse_bool)
.unwrap_or(DEFAULT_ENABLE_PRECISE_DETECTION);
let precise_detection_patterns = precise_detection_patterns
.unwrap_or_else(|| DEFAULT_PRECISE_DETECTION_PATTERNS.to_owned());
let ambiguous_mime_mapping =
ambiguous_mime_mapping.unwrap_or_else(|| DEFAULT_AMBIGUOUS_MIME_MAPPING.to_owned());
Self::new(
&mime_detector_default,
&media_stream_classifier_default,
enable_precise_detection,
&precise_detection_patterns,
&ambiguous_mime_mapping,
)
}
}
impl Default for MimeConfig {
fn default() -> Self {
let cell = default_config_cell();
match cell.read() {
Ok(guard) => guard.clone(),
Err(poisoned) => poisoned.into_inner().clone(),
}
}
}
fn default_config_cell() -> &'static RwLock<MimeConfig> {
DEFAULT_MIME_CONFIG.get_or_init(|| RwLock::new(MimeConfig::load()))
}
fn read_string(config: &Config, keys: &[&str], default: &str) -> MimeResult<String> {
for key in keys {
if let Some(value) = config.get_optional_string(key)? {
let value = value.trim();
if !value.is_empty() {
return Ok(value.to_owned());
}
}
}
Ok(default.to_owned())
}
fn read_bool(config: &Config, keys: &[&str], default: bool) -> MimeResult<bool> {
for key in keys {
if let Some(value) = config.get_optional_string(key)?
&& let Some(parsed) = parse_bool(&value)
{
return Ok(parsed);
}
}
Ok(default)
}
fn normalize_selector(selector: &str, default: &str) -> String {
let selector = selector.trim();
if selector.is_empty() {
default.to_owned()
} else {
selector.to_owned()
}
}
fn parse_bool(value: &str) -> Option<bool> {
match value.trim().to_ascii_lowercase().as_str() {
"1" | "true" | "yes" | "on" => Some(true),
"0" | "false" | "no" | "off" => Some(false),
_ => None,
}
}
fn parse_patterns(patterns: &str) -> HashSet<String> {
patterns
.split(',')
.map(str::trim)
.filter(|pattern| !pattern.is_empty())
.map(|pattern| pattern.trim_start_matches('.').to_ascii_lowercase())
.collect()
}
fn parse_mapping(mapping: &str) -> HashMap<String, [String; 2]> {
mapping
.split(';')
.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()
}
#[cfg(coverage)]
pub(crate) mod coverage_support {
use qubit_config::Config;
use super::{MimeConfig, parse_bool, parse_mapping, parse_patterns};
use crate::{
CONFIG_MEDIA_STREAM_CLASSIFIER_DEFAULT, CONFIG_MIME_AMBIGUOUS_MIME_MAPPING,
CONFIG_MIME_DETECTOR_DEFAULT, CONFIG_MIME_ENABLE_PRECISE_DETECTION,
CONFIG_MIME_PRECISE_DETECTION_PATTERNS,
};
pub(crate) fn exercise_config_edges() -> Vec<String> {
let config = MimeConfig::new(
"repository",
"ffprobe",
false,
"webm,.ogg,, ",
"webm:video/webm,audio/webm;bad;ogg:video/ogg,audio/ogg;bad:one;empty:,audio/x;extra:video/x,audio/x,other",
);
let blank_selectors = MimeConfig::new(" ", "\t", false, "", "");
let mut explicit_config = Config::new();
explicit_config
.set(CONFIG_MIME_DETECTOR_DEFAULT, "repository")
.expect("detector default should be configurable");
explicit_config
.set(CONFIG_MEDIA_STREAM_CLASSIFIER_DEFAULT, "ffprobe")
.expect("classifier default should be configurable");
explicit_config
.set(CONFIG_MIME_ENABLE_PRECISE_DETECTION, "yes")
.expect("precise detection should be configurable");
explicit_config
.set(CONFIG_MIME_PRECISE_DETECTION_PATTERNS, "webm")
.expect("precise detection patterns should be configurable");
explicit_config
.set(
CONFIG_MIME_AMBIGUOUS_MIME_MAPPING,
"webm:video/webm,audio/webm",
)
.expect("ambiguous MIME mapping should be configurable");
let explicit = MimeConfig::from_config(&explicit_config)
.expect("explicit coverage config should parse");
let builtin = MimeConfig::builtin_default();
let loaded = MimeConfig::load();
let defaulted = <MimeConfig as Default>::default();
let raw_values = MimeConfig::from_raw_values(
Some("repository".to_owned()),
Some("ffprobe".to_owned()),
Some("true".to_owned()),
Some("webm".to_owned()),
Some("webm:video/webm,audio/webm".to_owned()),
);
let raw_invalid =
MimeConfig::from_raw_values(None, None, Some("maybe".to_owned()), None, None);
vec![
config.mime_detector_default().to_owned(),
config.media_stream_classifier_default().to_owned(),
config.enable_precise_detection().to_string(),
blank_selectors.mime_detector_default().to_owned(),
blank_selectors.media_stream_classifier_default().to_owned(),
explicit.enable_precise_detection().to_string(),
explicit
.ambiguous_mime_mapping()
.contains_key("webm")
.to_string(),
builtin.mime_detector_default().to_owned(),
config
.precise_detection_patterns()
.contains("ogg")
.to_string(),
config.ambiguous_mime_mapping().len().to_string(),
loaded.enable_precise_detection().to_string(),
defaulted.enable_precise_detection().to_string(),
raw_values.enable_precise_detection().to_string(),
raw_invalid.enable_precise_detection().to_string(),
format!("{:?}", parse_bool("yes")),
format!("{:?}", parse_bool("true")),
format!("{:?}", parse_bool("1")),
format!("{:?}", parse_bool("on")),
format!("{:?}", parse_bool("off")),
format!("{:?}", parse_bool("false")),
format!("{:?}", parse_bool("0")),
format!("{:?}", parse_bool("no")),
format!("{:?}", parse_bool("maybe")),
parse_patterns("a,.b").len().to_string(),
parse_mapping("x:video/x,audio/x;y:video/y,audio/y")
.len()
.to_string(),
]
}
}