use std::collections::BTreeMap;
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use super::MimeType;
use super::check::MimeCheck;
const DEFAULTS: &str = include_str!("../../mime/defaults.toml");
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(try_from = "TokenRuleWire", into = "TokenRuleWire")]
pub enum TokenRule {
PerByte(f64),
PerPixel {
divisor: u32,
max: usize,
},
PerSecond(u32),
PerPage(usize),
Fixed(usize),
}
pub const PAGE_BYTES_GUESS: u64 = 64 * 1024;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct TokenRuleWire {
#[serde(skip_serializing_if = "Option::is_none")]
pub per_byte: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub per_pixel: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub per_second: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub per_page: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fixed: Option<usize>,
}
impl TryFrom<TokenRuleWire> for TokenRule {
type Error = String;
fn try_from(w: TokenRuleWire) -> Result<Self, Self::Error> {
if w.max.is_some() && w.per_pixel.is_none() {
return Err("tokens.max only goes with per_pixel".to_string());
}
match (w.per_byte, w.per_pixel, w.per_second, w.per_page, w.fixed) {
(Some(rate), None, None, None, None) => Ok(TokenRule::PerByte(rate)),
(None, Some(divisor), None, None, None) => Ok(TokenRule::PerPixel {
divisor,
max: w.max.unwrap_or(1600),
}),
(None, None, Some(rate), None, None) => Ok(TokenRule::PerSecond(rate)),
(None, None, None, Some(rate), None) => Ok(TokenRule::PerPage(rate)),
(None, None, None, None, Some(n)) => Ok(TokenRule::Fixed(n)),
_ => Err(
"tokens needs exactly one of per_byte, per_pixel, per_second, per_page, fixed"
.to_string(),
),
}
}
}
impl From<TokenRule> for TokenRuleWire {
fn from(rule: TokenRule) -> Self {
let mut w = TokenRuleWire::default();
match rule {
TokenRule::PerByte(rate) => w.per_byte = Some(rate),
TokenRule::PerPixel { divisor, max } => {
w.per_pixel = Some(divisor);
w.max = Some(max);
}
TokenRule::PerSecond(rate) => w.per_second = Some(rate),
TokenRule::PerPage(rate) => w.per_page = Some(rate),
TokenRule::Fixed(n) => w.fixed = Some(n),
}
w
}
}
impl TokenRule {
pub fn estimate(
self,
size: u64,
dims: Option<(u32, u32)>,
duration_ms: Option<u64>,
pages: Option<u32>,
) -> usize {
let per_byte = |rate: f64| ((size as f64) * rate).ceil() as usize;
match self {
TokenRule::PerByte(rate) => per_byte(rate).max(1),
TokenRule::PerPixel { divisor, max } => match dims {
Some((w, h)) => {
let pixels = u64::from(w) * u64::from(h);
(pixels.div_ceil(u64::from(divisor.max(1))) as usize).clamp(1, max.max(1))
}
None => max.max(1),
},
TokenRule::PerSecond(rate) => match duration_ms {
Some(ms) => (ms.div_ceil(1000) as usize * rate as usize).max(1),
None => per_byte(1.0 / 32.0).max(1),
},
TokenRule::PerPage(rate) => {
let pages =
pages.map_or_else(|| size.div_ceil(PAGE_BYTES_GUESS) as usize, |n| n as usize);
pages.saturating_mul(rate).max(1)
}
TokenRule::Fixed(n) => n,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct MimeRow {
pub family: Option<String>,
pub text: Option<bool>,
pub tokens: Option<TokenRule>,
pub extensions: Option<Vec<String>>,
pub magic: Option<String>,
pub stand_in: Option<String>,
pub check: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MimeInfo {
pub mime_type: MimeType,
pub family: String,
pub text: bool,
pub tokens: TokenRule,
pub extensions: Vec<String>,
pub stand_in: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub check: Option<String>,
pub source: String,
}
impl MimeInfo {
pub fn render_stand_in(
&self,
name: Option<&str>,
size: u64,
dims: Option<(u32, u32)>,
duration_ms: Option<u64>,
pages: Option<u32>,
) -> String {
let dims_s = dims.map(|(w, h)| format!("{w}x{h}")).unwrap_or_default();
let dur_s = duration_ms
.map(|ms| format!("{:.1}s", ms as f64 / 1000.0))
.unwrap_or_default();
let pages_s = pages
.map(|n| match n {
1 => "1 page".to_string(),
n => format!("{n} pages"),
})
.unwrap_or_default();
let size_s = super::human_size(size);
let name_s = name.unwrap_or("");
match &self.stand_in {
Some(t) => t
.replace("{type}", self.mime_type.as_str())
.replace("{name}", name_s)
.replace("{size}", &size_s)
.replace("{dims}", &dims_s)
.replace("{duration}", &dur_s)
.replace("{pages}", &pages_s),
None => {
let mut s = format!("[{}", self.mime_type);
if !dims_s.is_empty() {
s.push(' ');
s.push_str(&dims_s);
} else if !dur_s.is_empty() {
s.push(' ');
s.push_str(&dur_s);
} else if !pages_s.is_empty() {
s.push(' ');
s.push_str(&pages_s);
}
s.push_str(", ");
s.push_str(&size_s);
s.push(']');
if !name_s.is_empty() {
s.push(' ');
s.push_str(name_s);
}
s
}
}
}
}
#[derive(Debug, Clone)]
struct Layered {
row: MimeRow,
source: String,
}
#[derive(Debug, Clone)]
pub struct MimeRegistry {
rows: BTreeMap<String, Layered>,
checks: BTreeMap<String, Arc<dyn MimeCheck>>,
}
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
pub enum RegistryError {
#[error("mime_types key {0}")]
Key(String),
#[error("mime_types.\"{key}\": {message}")]
Row {
key: String,
message: String,
},
#[error("mime_types.\"{0}\": magic must be hex digits")]
Magic(String),
}
impl Default for MimeRegistry {
fn default() -> Self {
Self::builtin()
}
}
impl MimeRegistry {
pub fn empty() -> Self {
Self {
rows: BTreeMap::new(),
checks: BTreeMap::new(),
}
}
pub fn builtin() -> Self {
Self::from_defaults(DEFAULTS)
}
pub fn builtin_checked() -> Result<Self, RegistryError> {
Self::from_toml_str(DEFAULTS, "builtin")
}
fn from_defaults(text: &str) -> Self {
Self::from_toml_str(text, "builtin").unwrap_or_else(|_| Self::empty())
}
fn from_toml_str(text: &str, source: &str) -> Result<Self, RegistryError> {
let table: toml::Table = toml::from_str(text).map_err(|e| RegistryError::Row {
key: source.to_string(),
message: e.message().to_string(),
})?;
let mut reg = Self::empty();
reg.layer(&table, source)?;
Ok(reg)
}
pub fn layer(&mut self, table: &toml::Table, source: &str) -> Result<(), RegistryError> {
for (key, value) in table {
let key_norm = normalise_key(key).ok_or_else(|| RegistryError::Key(key.clone()))?;
let row: MimeRow =
value
.clone()
.try_into()
.map_err(|e: toml::de::Error| RegistryError::Row {
key: key.clone(),
message: e.message().to_string(),
})?;
if row
.magic
.as_deref()
.is_some_and(|m| decode_magic(m).is_none())
{
return Err(RegistryError::Magic(key.clone()));
}
if row.check.is_some() {
self.checks.remove(&key_norm);
}
let merged = match self.rows.remove(&key_norm) {
Some(existing) => merge_rows(existing.row, row),
None => row,
};
self.rows.insert(
key_norm,
Layered {
row: merged,
source: source.to_string(),
},
);
}
Ok(())
}
pub fn layered(&self, table: &toml::Table, source: &str) -> Result<Self, RegistryError> {
let mut next = self.clone();
next.layer(table, source)?;
Ok(next)
}
pub fn attach_check(
&mut self,
key: &str,
check: Arc<dyn MimeCheck>,
) -> Result<(), RegistryError> {
let key_norm = normalise_key(key).ok_or_else(|| RegistryError::Key(key.to_string()))?;
self.checks.insert(key_norm, check);
Ok(())
}
pub fn declared_checks(&self) -> Vec<(String, String, String)> {
self.rows
.iter()
.filter_map(|(key, l)| {
l.row
.check
.as_deref()
.filter(|c| !c.trim().is_empty())
.map(|c| (key.clone(), c.to_string(), l.source.clone()))
})
.collect()
}
pub fn check_for(&self, mime_type: &MimeType) -> Option<&Arc<dyn MimeCheck>> {
let chain = [
mime_type.as_str().to_string(),
mime_type.family_pattern(),
"*/*".to_string(),
];
for key in chain {
let Some(l) = self.rows.get(&key) else {
continue;
};
match l.row.check.as_deref() {
Some(c) if c.trim().is_empty() => return None,
Some(_) => return self.checks.get(&key),
None => {}
}
}
None
}
pub fn verify(&self, mime_type: &MimeType, bytes: &[u8]) -> Result<(), String> {
match self.check_for(mime_type) {
Some(check) => check.check(mime_type, bytes),
None => Ok(()),
}
}
pub fn keys(&self) -> Vec<(String, String)> {
self.rows
.iter()
.map(|(k, v)| (k.clone(), v.source.clone()))
.collect()
}
pub fn row(&self, key: &str) -> Option<&MimeRow> {
normalise_key(key)
.and_then(|k| self.rows.get(&k))
.map(|l| &l.row)
}
pub fn info(&self, mime_type: &MimeType) -> MimeInfo {
let mut family = "binary".to_string();
let mut text = false;
let mut tokens = TokenRule::PerByte(0.34);
let mut extensions = Vec::new();
let mut stand_in = None;
let mut check = None;
let mut source = "fallback".to_string();
let chain = [
"*/*".to_string(),
mime_type.family_pattern(),
mime_type.as_str().to_string(),
];
for key in chain {
if let Some(l) = self.rows.get(&key) {
if let Some(f) = &l.row.family {
family = f.clone();
}
if let Some(t) = l.row.text {
text = t;
}
if let Some(r) = l.row.tokens {
tokens = r;
}
if let Some(e) = &l.row.extensions {
extensions = e.iter().map(|s| s.to_ascii_lowercase()).collect();
}
if let Some(s) = &l.row.stand_in {
stand_in = Some(s.clone());
}
if let Some(c) = &l.row.check {
check = Some(c.clone()).filter(|c| !c.trim().is_empty());
}
source = l.source.clone();
}
}
MimeInfo {
mime_type: mime_type.clone(),
family,
text,
tokens,
extensions,
stand_in,
check,
source,
}
}
pub fn from_extension(&self, ext: &str) -> Option<MimeType> {
let ext = ext.trim().trim_start_matches('.').to_ascii_lowercase();
if ext.is_empty() {
return None;
}
self.rows.iter().find_map(|(key, l)| {
let claims = l
.row
.extensions
.as_ref()
.is_some_and(|exts| exts.iter().any(|e| e.eq_ignore_ascii_case(&ext)));
(claims && !key.ends_with("/*"))
.then(|| MimeType::parse(key).ok())
.flatten()
})
}
pub fn from_name(&self, name: &str) -> Option<MimeType> {
let ext = std::path::Path::new(name)
.extension()
.and_then(|e| e.to_str())?;
self.from_extension(ext)
}
pub fn sniff(&self, bytes: &[u8]) -> Option<MimeType> {
let mut best: Option<(usize, MimeType)> = None;
for (key, l) in &self.rows {
let Some(prefix) = l.row.magic.as_deref().and_then(decode_magic) else {
continue;
};
let matches = bytes.len() >= prefix.len()
&& prefix
.iter()
.zip(bytes)
.all(|(want, have)| want.is_none_or(|w| w == *have));
if matches
&& best.as_ref().is_none_or(|(len, _)| prefix.len() > *len)
&& let Ok(t) = MimeType::parse(key)
{
best = Some((prefix.len(), t));
}
}
best.map(|(_, t)| t)
}
pub fn resolve(
&self,
declared: Option<&MimeType>,
name: Option<&str>,
bytes: &[u8],
) -> MimeType {
if let Some(t) = declared {
return t.clone();
}
if let Some(t) = self.sniff(bytes) {
return t;
}
if let Some(t) = name.and_then(|n| self.from_name(n)) {
return t;
}
if std::str::from_utf8(bytes).is_ok() {
return super::text_plain();
}
super::octet_stream()
}
}
fn normalise_key(key: &str) -> Option<String> {
let k = key.trim().to_ascii_lowercase();
let (kind, sub) = k.split_once('/')?;
if kind.is_empty() || sub.is_empty() || sub.contains('/') {
return None;
}
if sub == "*" {
return (kind == "*" || MimeType::parse(&format!("{kind}/x")).is_ok()).then_some(k);
}
MimeType::parse(&k).ok().map(|t| t.as_str().to_string())
}
fn merge_rows(under: MimeRow, over: MimeRow) -> MimeRow {
MimeRow {
family: over.family.or(under.family),
text: over.text.or(under.text),
tokens: over.tokens.or(under.tokens),
extensions: over.extensions.or(under.extensions),
magic: over.magic.or(under.magic),
stand_in: over.stand_in.or(under.stand_in),
check: over.check.or(under.check),
}
}
fn decode_magic(s: &str) -> Option<Vec<Option<u8>>> {
if s.is_empty() || !s.len().is_multiple_of(2) {
return None;
}
s.as_bytes()
.chunks(2)
.map(|pair| match pair {
b"??" => Some(None),
_ => {
let hi = (pair[0] as char).to_digit(16)?;
let lo = (pair[1] as char).to_digit(16)?;
Some(Some((hi * 16 + lo) as u8))
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn mt(s: &str) -> MimeType {
MimeType::parse(s).unwrap()
}
#[test]
fn defaults_parse_and_resolve() {
MimeRegistry::builtin_checked().expect("the compiled defaults load");
let reg = MimeRegistry::builtin();
let png = reg.info(&mt("image/png"));
assert_eq!(png.family, "image");
assert!(!png.text);
assert_eq!(png.extensions, vec!["png"]);
assert_eq!(png.source, "builtin");
assert_eq!(
png.tokens,
TokenRule::PerPixel {
divisor: 750,
max: 1600
}
);
let obj = reg.info(&mt("model/obj"));
assert_eq!(obj.family, "model");
assert!(obj.text);
let md = reg.info(&mt("text/markdown"));
assert!(md.text);
assert_eq!(md.family, "text");
let unknown = reg.info(&mt("application/x-made-up"));
assert_eq!(unknown.family, "binary");
assert!(!unknown.text);
assert_eq!(unknown.source, "builtin");
let unknown_kind = reg.info(&mt("chemical/x-pdb"));
assert_eq!(unknown_kind.family, "binary");
assert!(!reg.keys().is_empty());
assert!(reg.row("image/png").is_some());
assert!(reg.row("nope").is_none());
}
#[test]
fn empty_registry_has_a_floor() {
let reg = MimeRegistry::empty();
let info = reg.info(&mt("image/png"));
assert_eq!(info.family, "binary");
assert_eq!(info.source, "fallback");
assert!(reg.from_extension("png").is_none());
assert!(reg.sniff(b"\x89PNG").is_none());
assert_eq!(reg.resolve(None, None, b"hello").as_str(), "text/plain");
assert_eq!(
reg.resolve(None, None, &[0xff, 0xfe, 0x00]).as_str(),
"application/octet-stream"
);
let d: MimeRegistry = MimeRegistry::default();
assert!(d.row("image/png").is_some());
}
#[test]
fn layering_merges_field_by_field() {
let mut reg = MimeRegistry::builtin();
let table: toml::Table = toml::from_str(
r#"
["image/png"]
extensions = ["png", "apng"]
["model/obj"]
text = false
stand_in = "<{type} {name} {size} {dims} {duration}>"
["chemical/*"]
family = "chem"
"#,
)
.unwrap();
reg.layer(&table, "config").unwrap();
let png = reg.info(&mt("image/png"));
assert_eq!(png.extensions, vec!["png", "apng"]);
assert_eq!(png.family, "image");
assert_eq!(png.source, "config");
assert!(reg.row("image/png").unwrap().magic.is_some());
let obj = reg.info(&mt("model/obj"));
assert!(!obj.text);
assert_eq!(obj.family, "model");
assert_eq!(
obj.render_stand_in(Some("a.obj"), 10, Some((1, 2)), Some(1500), None),
"<model/obj a.obj 10 B 1x2 1.5s>"
);
assert_eq!(reg.info(&mt("chemical/x-pdb")).family, "chem");
assert_eq!(reg.from_extension("APNG").unwrap().as_str(), "image/png");
}
#[test]
fn layering_refuses_bad_rows() {
let mut reg = MimeRegistry::empty();
let bad_key: toml::Table = toml::from_str("[png]\nfamily = \"image\"").unwrap();
assert_eq!(
reg.layer(&bad_key, "t").unwrap_err(),
RegistryError::Key("png".into())
);
let bad_field: toml::Table = toml::from_str("[\"image/png\"]\nfamilies = 1").unwrap();
let err = reg.layer(&bad_field, "t").unwrap_err();
assert!(
err.to_string()
.starts_with("mime_types.\"image/png\": unknown field `families`"),
"{err}"
);
let not_toml = MimeRegistry::from_toml_str("not [[ toml", "t").unwrap_err();
assert!(not_toml.to_string().starts_with("mime_types.\"t\""));
assert_eq!(
MimeRegistry::from_toml_str("[png]\nfamily = \"x\"", "t").unwrap_err(),
RegistryError::Key("png".into())
);
assert!(MimeRegistry::from_defaults("not [[ toml").keys().is_empty());
let bad_magic: toml::Table = toml::from_str("[\"image/png\"]\nmagic = \"zz\"").unwrap();
assert_eq!(
reg.layer(&bad_magic, "t").unwrap_err(),
RegistryError::Magic("image/png".into())
);
let empty_magic: toml::Table = toml::from_str("[\"image/png\"]\nmagic = \"\"").unwrap();
assert!(reg.layer(&empty_magic, "t").is_err());
let bad_wild: toml::Table = toml::from_str("[\"a/b/*\"]\nfamily = \"x\"").unwrap();
assert!(reg.layer(&bad_wild, "t").is_err());
let bad_wild2: toml::Table = toml::from_str("[\"a b/*\"]\nfamily = \"x\"").unwrap();
assert!(reg.layer(&bad_wild2, "t").is_err());
let err = RegistryError::Row {
key: "k".into(),
message: "m".into(),
};
assert_eq!(err.to_string(), "mime_types.\"k\": m");
assert!(RegistryError::Magic("k".into()).to_string().contains("hex"));
assert!(RegistryError::Key("k".into()).to_string().contains("k"));
}
#[test]
fn extension_and_name_lookup() {
let reg = MimeRegistry::builtin();
assert_eq!(reg.from_extension(".JPG").unwrap().as_str(), "image/jpeg");
assert_eq!(reg.from_name("clip.MP4").unwrap().as_str(), "video/mp4");
assert!(reg.from_name("Makefile").is_none());
assert!(reg.from_extension("").is_none());
assert!(reg.from_extension("zzz").is_none());
assert_eq!(reg.from_extension("obj").unwrap().as_str(), "model/obj");
}
#[test]
fn wildcard_magic_tells_riff_containers_apart_and_a_bad_magic_is_refused() {
let reg = MimeRegistry::builtin();
assert_eq!(reg.sniff(b"GIF89a\x01\x00").unwrap().as_str(), "image/gif");
assert_eq!(
reg.sniff(b"RIFF\x24\x08\x00\x00WAVEfmt ").unwrap().as_str(),
"audio/wav"
);
assert_eq!(
reg.sniff(b"RIFF\x00\x00\x00\x00WEBPVP8 ").unwrap().as_str(),
"image/webp"
);
assert!(reg.sniff(b"RIFF\x00\x00\x00\x00AVI LIST").is_none());
assert!(reg.sniff(b"RIFF\x00\x00").is_none());
for bad in ["474946383", "?A", "", "GG"] {
let mut reg = MimeRegistry::empty();
let table: toml::Table =
toml::from_str(&format!("[\"x/y\"]\nmagic = \"{bad}\"\n")).unwrap();
assert!(
matches!(reg.layer(&table, "t"), Err(RegistryError::Magic(k)) if k == "x/y"),
"{bad:?} should be refused"
);
}
}
#[test]
fn sniffing_prefers_the_longest_magic() {
let reg = MimeRegistry::builtin();
assert_eq!(
reg.sniff(b"\x89PNG\r\n\x1a\nrest").unwrap().as_str(),
"image/png"
);
assert_eq!(
reg.sniff(b"\xff\xd8\xff\xe0").unwrap().as_str(),
"image/jpeg"
);
assert_eq!(reg.sniff(b"%PDF-1.7").unwrap().as_str(), "application/pdf");
assert!(reg.sniff(b"\x89PN").is_none());
assert!(reg.sniff(b"plain").is_none());
let mut reg = MimeRegistry::empty();
let table: toml::Table = toml::from_str(
r#"
["x/short"]
magic = "AB"
["x/long"]
magic = "ABCD"
"#,
)
.unwrap();
reg.layer(&table, "t").unwrap();
assert_eq!(reg.sniff(&[0xab, 0xcd, 0x01]).unwrap().as_str(), "x/long");
assert_eq!(reg.sniff(&[0xab, 0x00]).unwrap().as_str(), "x/short");
assert_eq!(decode_magic("ABC"), None);
assert_eq!(decode_magic("GG"), None);
assert_eq!(decode_magic("AG"), None);
assert_eq!(decode_magic("?A"), None);
assert_eq!(decode_magic("AB??"), Some(vec![Some(0xAB), None]));
}
#[test]
fn resolve_order() {
let reg = MimeRegistry::builtin();
let declared = mt("model/stl");
assert_eq!(
reg.resolve(Some(&declared), Some("a.png"), b"\x89PNG\r\n\x1a\n"),
declared
);
assert_eq!(
reg.resolve(None, Some("a.txt"), b"\x89PNG\r\n\x1a\n")
.as_str(),
"image/png"
);
assert_eq!(
reg.resolve(None, Some("a.md"), b"# hi").as_str(),
"text/markdown"
);
assert_eq!(
reg.resolve(None, Some("a.zzz"), b"hi").as_str(),
"text/plain"
);
assert_eq!(
reg.resolve(None, None, &[0, 159, 146, 150]).as_str(),
"application/octet-stream"
);
}
#[test]
fn token_rules() {
assert_eq!(TokenRule::PerByte(0.25).estimate(8, None, None, None), 2);
assert_eq!(TokenRule::PerByte(0.25).estimate(0, None, None, None), 1);
let px = TokenRule::PerPixel {
divisor: 750,
max: 1600,
};
assert_eq!(px.estimate(0, Some((750, 2)), None, None), 2);
assert_eq!(px.estimate(0, Some((4000, 4000)), None, None), 1600);
assert_eq!(px.estimate(0, None, None, None), 1600);
assert_eq!(px.estimate(0, Some((0, 0)), None, None), 1);
let zero_div = TokenRule::PerPixel { divisor: 0, max: 0 };
assert_eq!(zero_div.estimate(0, Some((3, 3)), None, None), 1);
assert_eq!(
TokenRule::PerSecond(32).estimate(0, None, Some(1500), None),
64
);
assert_eq!(TokenRule::PerSecond(32).estimate(0, None, Some(0), None), 1);
assert_eq!(
TokenRule::PerSecond(32).estimate(3200, None, None, None),
100
);
assert_eq!(TokenRule::Fixed(7).estimate(99, None, None, None), 7);
assert_eq!(
TokenRule::PerPage(2000).estimate(0, None, None, Some(3)),
6000
);
assert_eq!(TokenRule::PerPage(2000).estimate(0, None, None, Some(0)), 1);
assert_eq!(
TokenRule::PerPage(2000).estimate(65_537, None, None, None),
4000
);
assert_eq!(TokenRule::PerPage(2000).estimate(0, None, None, None), 1);
let per_page: TokenRule = serde_json::from_str("{\"per_page\":2000}").unwrap();
assert_eq!(per_page, TokenRule::PerPage(2000));
assert_eq!(
serde_json::to_string(&per_page).unwrap(),
"{\"per_page\":2000}"
);
let two: Result<TokenRule, _> = serde_json::from_str("{\"per_page\":1,\"fixed\":2}");
assert!(two.unwrap_err().to_string().contains("per_page"));
let json = serde_json::to_string(&TokenRule::PerSecond(32)).unwrap();
assert_eq!(json, "{\"per_second\":32}");
let px: TokenRule = serde_json::from_str("{\"per_pixel\":750,\"max\":1600}").unwrap();
assert_eq!(
px,
TokenRule::PerPixel {
divisor: 750,
max: 1600
}
);
let px_default: TokenRule = serde_json::from_str("{\"per_pixel\":750}").unwrap();
assert_eq!(
px_default,
TokenRule::PerPixel {
divisor: 750,
max: 1600
}
);
assert_eq!(
serde_json::to_string(&px).unwrap(),
"{\"per_pixel\":750,\"max\":1600}"
);
assert_eq!(
serde_json::to_string(&TokenRule::Fixed(3)).unwrap(),
"{\"fixed\":3}"
);
assert_eq!(
serde_json::to_string(&TokenRule::PerByte(0.5)).unwrap(),
"{\"per_byte\":0.5}"
);
let fixed: TokenRule = serde_json::from_str("{\"fixed\":3}").unwrap();
assert_eq!(fixed, TokenRule::Fixed(3));
let per_byte: TokenRule = serde_json::from_str("{\"per_byte\":0.5}").unwrap();
assert_eq!(per_byte, TokenRule::PerByte(0.5));
let none = serde_json::from_str::<TokenRule>("{}").unwrap_err();
assert!(none.to_string().contains("exactly one"));
let two = serde_json::from_str::<TokenRule>("{\"fixed\":1,\"per_byte\":1.0}").unwrap_err();
assert!(two.to_string().contains("exactly one"));
let stray_max = serde_json::from_str::<TokenRule>("{\"fixed\":1,\"max\":2}").unwrap_err();
assert!(stray_max.to_string().contains("max"));
assert!(serde_json::from_str::<TokenRule>("{\"per_byte\":\"x\"}").is_err());
}
#[test]
fn a_check_resolves_by_row_and_runs_when_attached() {
use crate::mime::FnCheck;
let mut reg = MimeRegistry::builtin();
let table: toml::Table = toml::from_str(
"[\"image/*\"]\ncheck = \"checks/image.rhai\"\n[\"image/gif\"]\ncheck = \"\"\n",
)
.unwrap();
reg.layer(&table, "config").unwrap();
assert_eq!(
reg.info(&mt("image/png")).check.as_deref(),
Some("checks/image.rhai"),
"a subtype inherits the family's check"
);
assert_eq!(
reg.info(&mt("image/gif")).check,
None,
"an empty name lifts it"
);
assert_eq!(reg.info(&mt("audio/wav")).check, None);
assert_eq!(
reg.declared_checks(),
vec![(
"image/*".to_string(),
"checks/image.rhai".to_string(),
"config".to_string()
)]
);
assert!(reg.check_for(&mt("image/png")).is_none());
assert_eq!(reg.verify(&mt("image/png"), b"anything"), Ok(()));
let check = Arc::new(FnCheck::new(
"png-magic",
|t: &MimeType, bytes: &[u8]| match bytes.starts_with(b"\x89PNG") {
true => Ok(()),
false => Err(format!("not a {t} header")),
},
));
reg.attach_check("Image/*", check.clone()).unwrap();
assert!(reg.check_for(&mt("image/png")).is_some());
assert_eq!(reg.verify(&mt("image/png"), b"\x89PNG\r\n"), Ok(()));
assert_eq!(
reg.verify(&mt("image/png"), b"GIF89a"),
Err("not a image/png header".to_string())
);
assert_eq!(
reg.verify(&mt("image/gif"), b"GIF89a"),
Ok(()),
"the lifted subtype is not checked"
);
assert_eq!(reg.verify(&mt("audio/wav"), b"RIFF"), Ok(()));
assert_eq!(
reg.verify(&mt("application/x-made-up"), b"??"),
Ok(()),
"a type with no row of its own walks up to the floor"
);
assert_eq!(
reg.attach_check("png", check).unwrap_err(),
RegistryError::Key("png".into())
);
let snapshot = reg.clone();
let renamed: toml::Table =
toml::from_str("[\"image/*\"]\ncheck = \"checks/image2.rhai\"\n").unwrap();
let next = reg.layered(&renamed, "file").unwrap();
assert!(next.check_for(&mt("image/png")).is_none());
assert_eq!(
next.info(&mt("image/png")).check.as_deref(),
Some("checks/image2.rhai")
);
assert!(snapshot.check_for(&mt("image/png")).is_some());
assert!(format!("{next:?}").contains("checks"));
let unrelated: toml::Table =
toml::from_str("[\"image/*\"]\nfamily = \"picture\"\n").unwrap();
let kept = reg.layered(&unrelated, "file").unwrap();
assert!(kept.check_for(&mt("image/png")).is_some());
assert!(reg.layered(&bad_key_table(), "file").is_err());
}
fn bad_key_table() -> toml::Table {
toml::from_str("[png]\nfamily = \"image\"").unwrap()
}
#[test]
fn stand_in_defaults() {
let reg = MimeRegistry::builtin();
let png = reg.info(&mt("image/png"));
assert_eq!(
png.render_stand_in(Some("hero.png"), 240 * 1024, Some((1024, 768)), None, None),
"[image/png 1024x768, 240 KB] hero.png"
);
let wav = reg.info(&mt("audio/wav"));
assert_eq!(
wav.render_stand_in(None, 2048, None, Some(2500), None),
"[audio/wav 2.5s, 2 KB]"
);
let pdf = reg.info(&MimeType::parse("application/pdf").unwrap());
assert_eq!(
pdf.render_stand_in(Some("brief.pdf"), 2048, None, None, Some(1)),
"[application/pdf 1 page, 2 KB] brief.pdf"
);
assert_eq!(
pdf.render_stand_in(None, 2048, None, None, Some(12)),
"[application/pdf 12 pages, 2 KB]"
);
assert_eq!(
reg.info(&mt("model/stl"))
.render_stand_in(Some("a"), 1, None, None, None),
"[model/stl, 1 B] a"
);
}
}