This repository has no description
1const SNIFF_LIMIT: usize = 512;
2
3fn is_ws(byte: u8) -> bool {
4 matches!(byte, b'\t' | b'\n' | 0x0c | b'\r' | b' ')
5}
6
7fn is_tt(byte: u8) -> bool {
8 matches!(byte, b' ' | b'>')
9}
10
11enum Sig {
12 Exact(&'static [u8], &'static str),
13 Masked {
14 mask: &'static [u8],
15 pat: &'static [u8],
16 skip_ws: bool,
17 ct: &'static str,
18 },
19 Html(&'static [u8]),
20 Mp4,
21 Text,
22}
23
24impl Sig {
25 fn detect(&self, data: &[u8], first_non_ws: usize) -> Option<&'static str> {
26 match self {
27 Sig::Exact(sig, ct) => data.starts_with(sig).then_some(*ct),
28 Sig::Masked {
29 mask,
30 pat,
31 skip_ws,
32 ct,
33 } => {
34 let data = if *skip_ws {
35 &data[first_non_ws..]
36 } else {
37 data
38 };
39 (mask.len() == pat.len()
40 && data.len() >= pat.len()
41 && pat
42 .iter()
43 .zip(mask.iter())
44 .enumerate()
45 .all(|(index, (byte, mask))| data[index] & mask == *byte))
46 .then_some(*ct)
47 }
48 Sig::Html(tag) => {
49 let data = &data[first_non_ws..];
50 (data.len() > tag.len()
51 && tag.iter().enumerate().all(|(index, byte)| {
52 let candidate = data[index];
53 let candidate = match byte.is_ascii_uppercase() {
54 true => candidate & 0xDF,
55 false => candidate,
56 };
57 *byte == candidate
58 })
59 && is_tt(data[tag.len()]))
60 .then_some("text/html; charset=utf-8")
61 }
62 Sig::Mp4 => mp4(data),
63 Sig::Text => text(data, first_non_ws),
64 }
65 }
66}
67
68fn mp4(data: &[u8]) -> Option<&'static str> {
69 if data.len() < 12 {
70 return None;
71 }
72 let box_size = u32::from_be_bytes([data[0], data[1], data[2], data[3]]) as usize;
73 if data.len() < box_size || !box_size.is_multiple_of(4) || &data[4..8] != b"ftyp" {
74 return None;
75 }
76 (8..box_size)
77 .step_by(4)
78 .filter(|start| *start != 12)
79 .any(|start| &data[start..start + 3] == b"mp4")
80 .then_some("video/mp4")
81}
82
83fn text(data: &[u8], first_non_ws: usize) -> Option<&'static str> {
84 data[first_non_ws..]
85 .iter()
86 .all(|byte| !matches!(byte, 0x00..=0x08 | 0x0b | 0x0e..=0x1a | 0x1c..=0x1f))
87 .then_some("text/plain; charset=utf-8")
88}
89
90const SIGNATURES: &[Sig] = &[
91 Sig::Html(b"<!DOCTYPE HTML"),
92 Sig::Html(b"<HTML"),
93 Sig::Html(b"<HEAD"),
94 Sig::Html(b"<SCRIPT"),
95 Sig::Html(b"<IFRAME"),
96 Sig::Html(b"<H1"),
97 Sig::Html(b"<DIV"),
98 Sig::Html(b"<FONT"),
99 Sig::Html(b"<TABLE"),
100 Sig::Html(b"<A"),
101 Sig::Html(b"<STYLE"),
102 Sig::Html(b"<TITLE"),
103 Sig::Html(b"<B"),
104 Sig::Html(b"<BODY"),
105 Sig::Html(b"<BR"),
106 Sig::Html(b"<P"),
107 Sig::Html(b"<!--"),
108 Sig::Masked {
109 mask: b"\xff\xff\xff\xff\xff",
110 pat: b"<?xml",
111 skip_ws: true,
112 ct: "text/xml; charset=utf-8",
113 },
114 Sig::Exact(b"%PDF-", "application/pdf"),
115 Sig::Exact(b"%!PS-Adobe-", "application/postscript"),
116 Sig::Masked {
117 mask: b"\xff\xff\x00\x00",
118 pat: b"\xfe\xff\x00\x00",
119 skip_ws: false,
120 ct: "text/plain; charset=utf-16be",
121 },
122 Sig::Masked {
123 mask: b"\xff\xff\x00\x00",
124 pat: b"\xff\xfe\x00\x00",
125 skip_ws: false,
126 ct: "text/plain; charset=utf-16le",
127 },
128 Sig::Masked {
129 mask: b"\xff\xff\xff\x00",
130 pat: b"\xef\xbb\xbf\x00",
131 skip_ws: false,
132 ct: "text/plain; charset=utf-8",
133 },
134 Sig::Exact(b"\x00\x00\x01\x00", "image/x-icon"),
135 Sig::Exact(b"\x00\x00\x02\x00", "image/x-icon"),
136 Sig::Exact(b"BM", "image/bmp"),
137 Sig::Exact(b"GIF87a", "image/gif"),
138 Sig::Exact(b"GIF89a", "image/gif"),
139 Sig::Masked {
140 mask: b"\xff\xff\xff\xff\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff",
141 pat: b"RIFF\x00\x00\x00\x00WEBPVP",
142 skip_ws: false,
143 ct: "image/webp",
144 },
145 Sig::Exact(b"\x89PNG\x0d\x0a\x1a\x0a", "image/png"),
146 Sig::Exact(b"\xff\xd8\xff", "image/jpeg"),
147 Sig::Masked {
148 mask: b"\xff\xff\xff\xff\x00\x00\x00\x00\xff\xff\xff\xff",
149 pat: b"FORM\x00\x00\x00\x00AIFF",
150 skip_ws: false,
151 ct: "audio/aiff",
152 },
153 Sig::Masked {
154 mask: b"\xff\xff\xff",
155 pat: b"ID3",
156 skip_ws: false,
157 ct: "audio/mpeg",
158 },
159 Sig::Masked {
160 mask: b"\xff\xff\xff\xff\xff",
161 pat: b"OggS\x00",
162 skip_ws: false,
163 ct: "application/ogg",
164 },
165 Sig::Masked {
166 mask: b"\xff\xff\xff\xff\xff\xff\xff\xff",
167 pat: b"MThd\x00\x00\x00\x06",
168 skip_ws: false,
169 ct: "audio/midi",
170 },
171 Sig::Masked {
172 mask: b"\xff\xff\xff\xff\x00\x00\x00\x00\xff\xff\xff\xff",
173 pat: b"RIFF\x00\x00\x00\x00AVI ",
174 skip_ws: false,
175 ct: "video/avi",
176 },
177 Sig::Masked {
178 mask: b"\xff\xff\xff\xff\x00\x00\x00\x00\xff\xff\xff\xff",
179 pat: b"RIFF\x00\x00\x00\x00WAVE",
180 skip_ws: false,
181 ct: "audio/wave",
182 },
183 Sig::Mp4,
184 Sig::Exact(b"\x1a\x45\xdf\xa3", "video/webm"),
185 Sig::Masked {
186 mask: b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff",
187 pat: b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00LP",
188 skip_ws: false,
189 ct: "application/vnd.ms-fontobject",
190 },
191 Sig::Exact(b"\x00\x01\x00\x00", "font/ttf"),
192 Sig::Exact(b"OTTO", "font/otf"),
193 Sig::Exact(b"ttcf", "font/collection"),
194 Sig::Exact(b"wOFF", "font/woff"),
195 Sig::Exact(b"wOF2", "font/woff2"),
196 Sig::Exact(b"\x1f\x8b\x08", "application/x-gzip"),
197 Sig::Exact(b"PK\x03\x04", "application/zip"),
198 Sig::Exact(b"Rar!\x1a\x07\x00", "application/x-rar-compressed"),
199 Sig::Exact(b"Rar!\x1a\x07\x01\x00", "application/x-rar-compressed"),
200 Sig::Exact(b"\x00\x61\x73\x6d", "application/wasm"),
201 Sig::Text,
202];
203
204pub(crate) fn detect_content_type(content: &[u8]) -> &'static str {
205 let data = &content[..content.len().min(SNIFF_LIMIT)];
206 let first_non_ws = data
207 .iter()
208 .position(|byte| !is_ws(*byte))
209 .unwrap_or(data.len());
210 SIGNATURES
211 .iter()
212 .find_map(|sig| sig.detect(data, first_non_ws))
213 .unwrap_or("application/octet-stream")
214}
215
216pub(crate) fn override_by_extension(path: &str, detected: &'static str) -> &'static str {
217 let extension = path.rsplit_once('.').map(|(_, ext)| ext).unwrap_or("");
218 match extension.to_ascii_lowercase().as_str() {
219 "svg" => "image/svg+xml",
220 "avif" => "image/avif",
221 "jxl" => "image/jxl",
222 "heic" | "heif" => "image/heif",
223 _ => detected,
224 }
225}
226
227pub(crate) fn is_textual_mime(mime: &str) -> bool {
228 mime.starts_with("text/")
229 || matches!(
230 mime,
231 "application/json"
232 | "application/xml"
233 | "application/yaml"
234 | "application/x-yaml"
235 | "application/toml"
236 | "application/javascript"
237 | "application/ecmascript"
238 )
239}
240
241#[cfg(test)]
242mod tests {
243 use super::detect_content_type;
244
245 #[test]
246 fn detects_common_content_signatures() {
247 let cases: &[(&[u8], &str)] = &[
248 (b" <!DOCTYPE html>\n", "text/html; charset=utf-8"),
249 (b"<html><body>", "text/html; charset=utf-8"),
250 (b"<a href>", "text/html; charset=utf-8"),
251 (b"\n\t<?xml version=\"1.0\"?>", "text/xml; charset=utf-8"),
252 (b"\xfe\xff\x00h", "text/plain; charset=utf-16be"),
253 (b"\xef\xbb\xbfhello", "text/plain; charset=utf-8"),
254 (b"\x89PNG\x0d\x0a\x1a\x0a", "image/png"),
255 (b"GIF89a", "image/gif"),
256 (b"fn main() {}\n", "text/plain; charset=utf-8"),
257 (b"\x00\x01\x02\x03", "application/octet-stream"),
258 (b"", "text/plain; charset=utf-8"),
259 ];
260 cases.iter().for_each(|(input, expected)| {
261 assert_eq!(detect_content_type(input), *expected);
262 });
263 }
264}