1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct MimeType {
7 pub type_: String,
8 pub subtype: String,
9 pub suffix: Option<String>,
10}
11
12#[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#[must_use]
40pub fn looks_like_mime(input: &str) -> bool {
41 parse_mime(input).is_some()
42}
43
44#[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#[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#[must_use]
108pub fn is_text_mime(mime: &str) -> bool {
109 matches!(parse_mime(mime), Some(MimeType { type_, .. }) if type_ == "text")
110}
111
112#[must_use]
114pub fn is_image_mime(mime: &str) -> bool {
115 matches!(parse_mime(mime), Some(MimeType { type_, .. }) if type_ == "image")
116}
117
118#[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#[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#[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#[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}