1#![doc = include_str!("../README.md")]
2#![warn(missing_docs)]
3#![cfg_attr(not(feature = "std"), no_std)]
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13#[non_exhaustive]
14pub enum MimeType {
15 Jpeg,
18 Png,
20 Gif,
22 WebP,
24 Bmp,
26 Ico,
28 Tiff,
30 Mp4,
33 QuickTime,
35 ThreeGp,
37 Mkv,
39 WebM,
41 Avi,
43 Heic,
46 Heif,
48 Avif,
50 Mp3,
53 Wav,
55 Flac,
57 Ogg,
59 Pdf,
62 Zip,
64 Gzip,
66 Tar,
68 Sqlite,
70 Wasm,
73 Elf,
75 PeExe,
77}
78
79impl MimeType {
80 pub const fn as_str(&self) -> &'static str {
82 match self {
83 Self::Jpeg => "image/jpeg",
84 Self::Png => "image/png",
85 Self::Gif => "image/gif",
86 Self::WebP => "image/webp",
87 Self::Bmp => "image/bmp",
88 Self::Ico => "image/x-icon",
89 Self::Tiff => "image/tiff",
90 Self::Mp4 => "video/mp4",
91 Self::QuickTime => "video/quicktime",
92 Self::ThreeGp => "video/3gpp",
93 Self::Mkv => "video/x-matroska",
94 Self::WebM => "video/webm",
95 Self::Avi => "video/x-msvideo",
96 Self::Heic => "image/heic",
97 Self::Heif => "image/heif",
98 Self::Avif => "image/avif",
99 Self::Mp3 => "audio/mpeg",
100 Self::Wav => "audio/wav",
101 Self::Flac => "audio/flac",
102 Self::Ogg => "audio/ogg",
103 Self::Pdf => "application/pdf",
104 Self::Zip => "application/zip",
105 Self::Gzip => "application/gzip",
106 Self::Tar => "application/x-tar",
107 Self::Sqlite => "application/x-sqlite3",
108 Self::Wasm => "application/wasm",
109 Self::Elf => "application/x-elf",
110 Self::PeExe => "application/x-msdownload",
111 }
112 }
113
114 pub const fn extension(&self) -> &'static str {
116 match self {
117 Self::Jpeg => "jpg",
118 Self::Png => "png",
119 Self::Gif => "gif",
120 Self::WebP => "webp",
121 Self::Bmp => "bmp",
122 Self::Ico => "ico",
123 Self::Tiff => "tiff",
124 Self::Mp4 => "mp4",
125 Self::QuickTime => "mov",
126 Self::ThreeGp => "3gp",
127 Self::Mkv => "mkv",
128 Self::WebM => "webm",
129 Self::Avi => "avi",
130 Self::Heic => "heic",
131 Self::Heif => "heif",
132 Self::Avif => "avif",
133 Self::Mp3 => "mp3",
134 Self::Wav => "wav",
135 Self::Flac => "flac",
136 Self::Ogg => "ogg",
137 Self::Pdf => "pdf",
138 Self::Zip => "zip",
139 Self::Gzip => "gz",
140 Self::Tar => "tar",
141 Self::Sqlite => "db",
142 Self::Wasm => "wasm",
143 Self::Elf => "elf",
144 Self::PeExe => "exe",
145 }
146 }
147}
148
149pub fn detect(bytes: &[u8]) -> Option<MimeType> {
163 let b = bytes;
164 let len = b.len();
165
166 macro_rules! starts_with {
167 ($sig:expr) => {
168 len >= $sig.len() && b[..$sig.len()] == $sig[..]
169 };
170 }
171
172 macro_rules! at_offset {
173 ($offset:expr, $sig:expr) => {
174 len >= $offset + $sig.len() && b[$offset..$offset + $sig.len()] == $sig[..]
175 };
176 }
177
178 if starts_with!([0xFF, 0xD8, 0xFF]) {
180 return Some(MimeType::Jpeg);
181 }
182 if starts_with!([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) {
184 return Some(MimeType::Png);
185 }
186 if starts_with!([0x47, 0x49, 0x46, 0x38]) {
188 return Some(MimeType::Gif);
189 }
190 if starts_with!([0x52, 0x49, 0x46, 0x46]) && at_offset!(8, [0x57, 0x45, 0x42, 0x50]) {
192 return Some(MimeType::WebP);
193 }
194 if starts_with!([0x25, 0x50, 0x44, 0x46]) {
196 return Some(MimeType::Pdf);
197 }
198 if starts_with!([0x00, 0x61, 0x73, 0x6D]) {
200 return Some(MimeType::Wasm);
201 }
202 if starts_with!([0x7F, 0x45, 0x4C, 0x46]) {
204 return Some(MimeType::Elf);
205 }
206 if starts_with!([0x4D, 0x5A]) {
208 return Some(MimeType::PeExe);
209 }
210 if starts_with!([0x1F, 0x8B]) {
212 return Some(MimeType::Gzip);
213 }
214 if starts_with!([0x50, 0x4B, 0x03, 0x04]) {
216 return Some(MimeType::Zip);
217 }
218 if starts_with!([
220 0x53, 0x51, 0x4C, 0x69, 0x74, 0x65, 0x20, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x20, 0x33,
221 0x00
222 ]) {
223 return Some(MimeType::Sqlite);
224 }
225 if starts_with!([0x66, 0x4C, 0x61, 0x43]) {
227 return Some(MimeType::Flac);
228 }
229 if starts_with!([0x4F, 0x67, 0x67, 0x53]) {
231 return Some(MimeType::Ogg);
232 }
233 if starts_with!([0x49, 0x44, 0x33]) {
235 return Some(MimeType::Mp3);
236 }
237 if len >= 2 && b[0] == 0xFF && (b[1] == 0xFB || b[1] == 0xFA || b[1] == 0xF3 || b[1] == 0xF2) {
238 return Some(MimeType::Mp3);
239 }
240 if starts_with!([0x1A, 0x45, 0xDF, 0xA3]) {
244 let limit = len.min(512);
245 let mut idx = 0;
246 let mut kind = MimeType::Mkv;
247 while idx + 3 <= limit {
248 if b[idx] == 0x42 && b[idx + 1] == 0x82 {
249 let data_len = (b[idx + 2] & 0x7F) as usize;
250 if idx + 3 + data_len <= limit {
251 let doc_type = &b[idx + 3..idx + 3 + data_len];
252 if doc_type == b"webm" {
253 kind = MimeType::WebM;
254 break;
255 } else if doc_type == b"matroska" {
256 kind = MimeType::Mkv;
257 break;
258 }
259 }
260 }
261 idx += 1;
262 }
263 return Some(kind);
264 }
265 if at_offset!(4, [0x66, 0x74, 0x79, 0x70]) {
269 if len >= 12 {
270 let brand = &b[8..12];
271 return Some(match brand {
272 b"heic" | b"heix" | b"hevc" | b"hevx" => MimeType::Heic,
273 b"mif1" => MimeType::Heif,
274 b"avif" | b"avis" => MimeType::Avif,
275 b"qt " => MimeType::QuickTime,
276 b"3gp4" | b"3gp5" | b"3gp6" | b"3gr6" | b"3gs6" | b"3gpp" => MimeType::ThreeGp,
277 _ => MimeType::Mp4,
278 });
279 }
280 return Some(MimeType::Mp4);
281 }
282 if starts_with!([0x52, 0x49, 0x46, 0x46]) && at_offset!(8, [0x57, 0x41, 0x56, 0x45]) {
284 return Some(MimeType::Wav);
285 }
286 if starts_with!([0x52, 0x49, 0x46, 0x46]) && at_offset!(8, [0x41, 0x56, 0x49, 0x20]) {
288 return Some(MimeType::Avi);
289 }
290 if at_offset!(257, [0x75, 0x73, 0x74, 0x61, 0x72]) {
292 return Some(MimeType::Tar);
293 }
294 if starts_with!([0x42, 0x4D]) {
296 return Some(MimeType::Bmp);
297 }
298 if starts_with!([0x00, 0x00, 0x01, 0x00]) {
300 return Some(MimeType::Ico);
301 }
302 if starts_with!([0x49, 0x49, 0x2A, 0x00]) || starts_with!([0x4D, 0x4D, 0x00, 0x2A]) {
304 return Some(MimeType::Tiff);
305 }
306
307 None
308}
309
310pub fn detect_by_extension(ext: &str) -> Option<MimeType> {
324 let eq = |a: &str| {
326 a.len() == ext.len()
327 && a.bytes()
328 .zip(ext.bytes())
329 .all(|(a, b)| a == b.to_ascii_lowercase())
330 };
331
332 if eq("jpg") || eq("jpeg") {
333 return Some(MimeType::Jpeg);
334 }
335 if eq("png") {
336 return Some(MimeType::Png);
337 }
338 if eq("gif") {
339 return Some(MimeType::Gif);
340 }
341 if eq("webp") {
342 return Some(MimeType::WebP);
343 }
344 if eq("bmp") {
345 return Some(MimeType::Bmp);
346 }
347 if eq("ico") {
348 return Some(MimeType::Ico);
349 }
350 if eq("tiff") || eq("tif") {
351 return Some(MimeType::Tiff);
352 }
353 if eq("mp4") || eq("m4v") {
354 return Some(MimeType::Mp4);
355 }
356 if eq("mov") || eq("qt") {
357 return Some(MimeType::QuickTime);
358 }
359 if eq("3gp") || eq("3gpp") {
360 return Some(MimeType::ThreeGp);
361 }
362 if eq("mkv") || eq("mk3d") {
363 return Some(MimeType::Mkv);
364 }
365 if eq("heic") {
366 return Some(MimeType::Heic);
367 }
368 if eq("heif") {
369 return Some(MimeType::Heif);
370 }
371 if eq("avif") {
372 return Some(MimeType::Avif);
373 }
374 if eq("webm") {
375 return Some(MimeType::WebM);
376 }
377 if eq("avi") {
378 return Some(MimeType::Avi);
379 }
380 if eq("mp3") {
381 return Some(MimeType::Mp3);
382 }
383 if eq("wav") {
384 return Some(MimeType::Wav);
385 }
386 if eq("flac") {
387 return Some(MimeType::Flac);
388 }
389 if eq("ogg") || eq("oga") {
390 return Some(MimeType::Ogg);
391 }
392 if eq("pdf") {
393 return Some(MimeType::Pdf);
394 }
395 if eq("zip") || eq("docx") || eq("xlsx") || eq("pptx") || eq("jar") {
396 return Some(MimeType::Zip);
397 }
398 if eq("gz") || eq("gzip") {
399 return Some(MimeType::Gzip);
400 }
401 if eq("tar") {
402 return Some(MimeType::Tar);
403 }
404 if eq("db") || eq("sqlite") || eq("sqlite3") {
405 return Some(MimeType::Sqlite);
406 }
407 if eq("wasm") {
408 return Some(MimeType::Wasm);
409 }
410 if eq("elf") {
411 return Some(MimeType::Elf);
412 }
413 if eq("exe") || eq("dll") {
414 return Some(MimeType::PeExe);
415 }
416
417 None
418}
419
420#[cfg(feature = "std")]
425pub fn detect_file(path: impl AsRef<std::path::Path>) -> std::io::Result<Option<MimeType>> {
426 use std::io::Read;
427 let mut buf = [0u8; 512];
428 let mut f = std::fs::File::open(path)?;
429 let n = f.read(&mut buf)?;
430 Ok(detect(&buf[..n]))
431}
432
433#[cfg(test)]
434mod tests {
435 use super::*;
436
437 #[test]
438 fn jpeg() {
439 assert_eq!(detect(&[0xFF, 0xD8, 0xFF, 0xE0]), Some(MimeType::Jpeg));
440 }
441
442 #[test]
443 fn png() {
444 assert_eq!(
445 detect(&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]),
446 Some(MimeType::Png)
447 );
448 }
449
450 #[test]
451 fn gif() {
452 assert_eq!(
453 detect(&[0x47, 0x49, 0x46, 0x38, 0x39, 0x61]),
454 Some(MimeType::Gif)
455 );
456 }
457
458 #[test]
459 fn pdf() {
460 assert_eq!(detect(b"%PDF-1.4"), Some(MimeType::Pdf));
461 }
462
463 #[test]
464 fn wasm() {
465 assert_eq!(
466 detect(&[0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00]),
467 Some(MimeType::Wasm)
468 );
469 }
470
471 #[test]
472 fn elf() {
473 assert_eq!(detect(&[0x7F, 0x45, 0x4C, 0x46, 0x02]), Some(MimeType::Elf));
474 }
475
476 #[test]
477 fn pe_exe() {
478 assert_eq!(detect(&[0x4D, 0x5A, 0x90, 0x00]), Some(MimeType::PeExe));
479 }
480
481 #[test]
482 fn gzip() {
483 assert_eq!(detect(&[0x1F, 0x8B, 0x08, 0x00]), Some(MimeType::Gzip));
484 }
485
486 #[test]
487 fn zip() {
488 assert_eq!(detect(&[0x50, 0x4B, 0x03, 0x04, 0x14]), Some(MimeType::Zip));
489 }
490
491 #[test]
492 fn flac() {
493 assert_eq!(detect(b"fLaC\x00\x00\x00\x22"), Some(MimeType::Flac));
494 }
495
496 #[test]
497 fn ogg() {
498 assert_eq!(detect(b"OggS\x00\x02"), Some(MimeType::Ogg));
499 }
500
501 #[test]
502 fn mp3_id3() {
503 assert_eq!(detect(b"ID3\x04\x00"), Some(MimeType::Mp3));
504 }
505
506 #[test]
507 fn mp3_sync() {
508 assert_eq!(detect(&[0xFF, 0xFB, 0x90, 0x00]), Some(MimeType::Mp3));
509 }
510
511 #[test]
512 fn unknown_returns_none() {
513 assert_eq!(detect(b"hello world"), None);
514 }
515
516 #[test]
517 fn empty_returns_none() {
518 assert_eq!(detect(&[]), None);
519 }
520
521 #[test]
522 fn ext_jpg() {
523 assert_eq!(detect_by_extension("jpg"), Some(MimeType::Jpeg));
524 assert_eq!(detect_by_extension("jpeg"), Some(MimeType::Jpeg));
525 assert_eq!(detect_by_extension("JPG"), Some(MimeType::Jpeg));
526 }
527
528 #[test]
529 fn ext_pdf() {
530 assert_eq!(detect_by_extension("pdf"), Some(MimeType::Pdf));
531 assert_eq!(detect_by_extension("PDF"), Some(MimeType::Pdf));
532 }
533
534 #[test]
535 fn ext_unknown() {
536 assert_eq!(detect_by_extension("xyz"), None);
537 assert_eq!(detect_by_extension(""), None);
538 }
539
540 #[test]
541 fn ext_docx_is_zip() {
542 assert_eq!(detect_by_extension("docx"), Some(MimeType::Zip));
543 }
544
545 #[test]
546 fn mime_str_and_ext() {
547 assert_eq!(MimeType::Jpeg.as_str(), "image/jpeg");
548 assert_eq!(MimeType::Jpeg.extension(), "jpg");
549 assert_eq!(MimeType::Wasm.as_str(), "application/wasm");
550 assert_eq!(MimeType::Wasm.extension(), "wasm");
551 }
552
553 #[test]
554 fn webp() {
555 let mut bytes = [0u8; 12];
556 bytes[0..4].copy_from_slice(&[0x52, 0x49, 0x46, 0x46]);
557 bytes[8..12].copy_from_slice(&[0x57, 0x45, 0x42, 0x50]);
558 assert_eq!(detect(&bytes), Some(MimeType::WebP));
559 }
560
561 #[test]
562 fn wav() {
563 let mut bytes = [0u8; 12];
564 bytes[0..4].copy_from_slice(&[0x52, 0x49, 0x46, 0x46]);
565 bytes[8..12].copy_from_slice(&[0x57, 0x41, 0x56, 0x45]);
566 assert_eq!(detect(&bytes), Some(MimeType::Wav));
567 }
568
569 #[test]
570 fn bmp() {
571 assert_eq!(detect(&[0x42, 0x4D, 0x00, 0x00]), Some(MimeType::Bmp));
572 }
573
574 #[test]
575 fn ico() {
576 assert_eq!(detect(&[0x00, 0x00, 0x01, 0x00, 0x01]), Some(MimeType::Ico));
577 }
578
579 #[test]
580 fn tiff_le() {
581 assert_eq!(detect(&[0x49, 0x49, 0x2A, 0x00]), Some(MimeType::Tiff));
582 }
583
584 #[test]
585 fn tiff_be() {
586 assert_eq!(detect(&[0x4D, 0x4D, 0x00, 0x2A]), Some(MimeType::Tiff));
587 }
588
589 #[test]
590 fn webm_via_ebml_doctype() {
591 let bytes = [
592 0x1A, 0x45, 0xDF, 0xA3, 0x8F, 0x42, 0x82, 0x84, b'w', b'e', b'b', b'm', 0, 0, 0, 0, 0, 0, 0, 0,
595 ];
596 assert_eq!(detect(&bytes), Some(MimeType::WebM));
597 }
598
599 #[test]
600 fn mkv_via_ebml_doctype() {
601 let bytes = [
602 0x1A, 0x45, 0xDF, 0xA3, 0x8F, 0x42, 0x82, 0x88, b'm', b'a', b't', b'r', b'o', b's', b'k', b'a', 0, 0, 0, 0,
605 ];
606 assert_eq!(detect(&bytes), Some(MimeType::Mkv));
607 }
608
609 #[test]
610 fn heic_via_ftyp_brand() {
611 let bytes = [
612 0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, b'h', b'e', b'i', b'c', 0x00, 0x00,
613 0x00, 0x00,
614 ];
615 assert_eq!(detect(&bytes), Some(MimeType::Heic));
616 }
617
618 #[test]
619 fn heif_via_ftyp_brand() {
620 let bytes = [
621 0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, b'm', b'i', b'f', b'1', 0x00, 0x00,
622 0x00, 0x00,
623 ];
624 assert_eq!(detect(&bytes), Some(MimeType::Heif));
625 }
626
627 #[test]
628 fn avif_via_ftyp_brand() {
629 let bytes = [
630 0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, b'a', b'v', b'i', b'f', 0x00, 0x00,
631 0x00, 0x00,
632 ];
633 assert_eq!(detect(&bytes), Some(MimeType::Avif));
634 }
635
636 #[test]
637 fn mov_via_ftyp_brand() {
638 let bytes = [
639 0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, b'q', b't', b' ', b' ', 0x00, 0x00,
640 0x00, 0x00,
641 ];
642 assert_eq!(detect(&bytes), Some(MimeType::QuickTime));
643 }
644
645 #[test]
646 fn threegp_via_ftyp_brand() {
647 let bytes = [
648 0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, b'3', b'g', b'p', b'4', 0x00, 0x00,
649 0x00, 0x00,
650 ];
651 assert_eq!(detect(&bytes), Some(MimeType::ThreeGp));
652 }
653
654 #[test]
655 fn mp4_fallback_for_unknown_brand() {
656 let bytes = [
657 0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, b'i', b's', b'o', b'm', 0x00, 0x00,
658 0x00, 0x00,
659 ];
660 assert_eq!(detect(&bytes), Some(MimeType::Mp4));
661 }
662
663 #[test]
664 fn ext_new_variants() {
665 assert_eq!(detect_by_extension("heic"), Some(MimeType::Heic));
666 assert_eq!(detect_by_extension("heif"), Some(MimeType::Heif));
667 assert_eq!(detect_by_extension("avif"), Some(MimeType::Avif));
668 assert_eq!(detect_by_extension("mov"), Some(MimeType::QuickTime));
669 assert_eq!(detect_by_extension("3gp"), Some(MimeType::ThreeGp));
670 }
671}