Skip to main content

use_mime/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4/// A lightweight MIME type split into type, subtype, and optional suffix.
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct MimeType {
7    pub type_: String,
8    pub subtype: String,
9    pub suffix: Option<String>,
10}
11
12/// Parses a MIME type essence into simple parts.
13#[must_use]
14pub fn parse_mime(input: &str) -> Option<MimeType> {
15    let essence = input.trim().split(';').next()?.trim().to_ascii_lowercase();
16    let (type_, subtype_with_suffix) = essence.split_once('/')?;
17    if type_.is_empty() || subtype_with_suffix.is_empty() {
18        return None;
19    }
20    if !is_token(type_) || !is_token(subtype_with_suffix) {
21        return None;
22    }
23
24    let (subtype, suffix) = match subtype_with_suffix.rsplit_once('+') {
25        Some((subtype, suffix)) if !subtype.is_empty() && !suffix.is_empty() => {
26            (subtype.to_string(), Some(suffix.to_string()))
27        }
28        _ => (subtype_with_suffix.to_string(), None),
29    };
30
31    Some(MimeType {
32        type_: type_.to_string(),
33        subtype,
34        suffix,
35    })
36}
37
38/// Returns `true` when the input parses as a MIME type.
39#[must_use]
40pub fn looks_like_mime(input: &str) -> bool {
41    parse_mime(input).is_some()
42}
43
44/// Returns a common MIME type for the requested extension.
45#[must_use]
46pub fn mime_from_extension(extension: &str) -> Option<&'static str> {
47    match extension
48        .trim()
49        .trim_start_matches('.')
50        .to_ascii_lowercase()
51        .as_str()
52    {
53        "html" | "htm" => Some("text/html"),
54        "css" => Some("text/css"),
55        "js" | "mjs" => Some("application/javascript"),
56        "json" => Some("application/json"),
57        "xml" => Some("application/xml"),
58        "txt" => Some("text/plain"),
59        "md" => Some("text/markdown"),
60        "csv" => Some("text/csv"),
61        "png" => Some("image/png"),
62        "jpg" | "jpeg" => Some("image/jpeg"),
63        "gif" => Some("image/gif"),
64        "svg" => Some("image/svg+xml"),
65        "webp" => Some("image/webp"),
66        "ico" => Some("image/x-icon"),
67        "pdf" => Some("application/pdf"),
68        "wasm" => Some("application/wasm"),
69        "zip" => Some("application/zip"),
70        _ => None,
71    }
72}
73
74/// Returns a common extension for the requested MIME type.
75#[must_use]
76pub fn extension_from_mime(mime: &str) -> Option<&'static str> {
77    match mime
78        .trim()
79        .split(';')
80        .next()?
81        .trim()
82        .to_ascii_lowercase()
83        .as_str()
84    {
85        "text/html" => Some("html"),
86        "text/css" => Some("css"),
87        "application/javascript" | "text/javascript" => Some("js"),
88        "application/json" => Some("json"),
89        "application/xml" | "text/xml" => Some("xml"),
90        "text/plain" => Some("txt"),
91        "text/markdown" => Some("md"),
92        "text/csv" => Some("csv"),
93        "image/png" => Some("png"),
94        "image/jpeg" => Some("jpg"),
95        "image/gif" => Some("gif"),
96        "image/svg+xml" => Some("svg"),
97        "image/webp" => Some("webp"),
98        "image/x-icon" => Some("ico"),
99        "application/pdf" => Some("pdf"),
100        "application/wasm" => Some("wasm"),
101        "application/zip" => Some("zip"),
102        _ => None,
103    }
104}
105
106/// Returns `true` when the MIME type is text-based.
107#[must_use]
108pub fn is_text_mime(mime: &str) -> bool {
109    matches!(parse_mime(mime), Some(MimeType { type_, .. }) if type_ == "text")
110}
111
112/// Returns `true` when the MIME type is image-based.
113#[must_use]
114pub fn is_image_mime(mime: &str) -> bool {
115    matches!(parse_mime(mime), Some(MimeType { type_, .. }) if type_ == "image")
116}
117
118/// Returns `true` when the MIME type is JSON or a `+json` subtype.
119#[must_use]
120pub fn is_json_mime(mime: &str) -> bool {
121    matches!(parse_mime(mime), Some(MimeType { subtype, suffix, .. }) if subtype == "json" || suffix.as_deref() == Some("json"))
122}
123
124/// Returns `true` when the MIME type is HTML.
125#[must_use]
126pub fn is_html_mime(mime: &str) -> bool {
127    matches!(parse_mime(mime), Some(MimeType { type_, subtype, .. }) if type_ == "text" && subtype == "html")
128}
129
130/// Returns `true` when the MIME type is XML or a `+xml` subtype.
131#[must_use]
132pub fn is_xml_mime(mime: &str) -> bool {
133    matches!(parse_mime(mime), Some(MimeType { subtype, suffix, .. }) if subtype == "xml" || suffix.as_deref() == Some("xml"))
134}
135
136/// Returns `true` when the MIME type is CSS.
137#[must_use]
138pub fn is_css_mime(mime: &str) -> bool {
139    matches!(parse_mime(mime), Some(MimeType { type_, subtype, .. }) if type_ == "text" && subtype == "css")
140}
141
142fn is_token(input: &str) -> bool {
143    input.bytes().all(|byte| {
144        byte.is_ascii_alphanumeric()
145            || matches!(
146                byte,
147                b'!' | b'#' | b'$' | b'&' | b'^' | b'_' | b'.' | b'+' | b'-'
148            )
149    })
150}