Skip to main content

use_media_type/
lib.rs

1#![forbid(unsafe_code)]
2//! Primitive media type helpers.
3//!
4//! These helpers keep MIME-like parsing intentionally small and deterministic.
5//!
6//! # Examples
7//!
8//! ```rust
9//! use use_media_type::{MediaKind, is_video_mime, parse_media_type};
10//!
11//! let media_type = parse_media_type("video/mp4").unwrap();
12//!
13//! assert_eq!(media_type.kind(), MediaKind::Video);
14//! assert_eq!(media_type.subtype(), "mp4");
15//! assert_eq!(media_type.as_mime(), "video/mp4");
16//! assert!(is_video_mime("video/mp4"));
17//! ```
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum MediaKind {
21    Image,
22    Audio,
23    Video,
24    Text,
25    Application,
26    Unknown,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct MediaType {
31    kind: MediaKind,
32    subtype: String,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub enum MediaTypeError {
37    InvalidSubtype,
38    InvalidMime,
39}
40
41fn kind_label(kind: MediaKind) -> &'static str {
42    match kind {
43        MediaKind::Image => "image",
44        MediaKind::Audio => "audio",
45        MediaKind::Video => "video",
46        MediaKind::Text => "text",
47        MediaKind::Application => "application",
48        MediaKind::Unknown => "unknown",
49    }
50}
51
52impl MediaType {
53    pub fn new(kind: MediaKind, subtype: impl Into<String>) -> Result<Self, MediaTypeError> {
54        let subtype = subtype.into().trim().to_ascii_lowercase();
55
56        if subtype.is_empty() || subtype.contains('/') {
57            return Err(MediaTypeError::InvalidSubtype);
58        }
59
60        Ok(Self { kind, subtype })
61    }
62
63    #[must_use]
64    pub fn kind(&self) -> MediaKind {
65        self.kind
66    }
67
68    #[must_use]
69    pub fn subtype(&self) -> &str {
70        &self.subtype
71    }
72
73    #[must_use]
74    pub fn as_mime(&self) -> String {
75        format!("{}/{}", kind_label(self.kind), self.subtype)
76    }
77}
78
79#[must_use]
80pub fn media_kind_from_mime(mime: &str) -> MediaKind {
81    let base = mime.trim().split(';').next().unwrap_or("").trim();
82    let kind = base
83        .split('/')
84        .next()
85        .unwrap_or("")
86        .trim()
87        .to_ascii_lowercase();
88
89    match kind.as_str() {
90        "image" => MediaKind::Image,
91        "audio" => MediaKind::Audio,
92        "video" => MediaKind::Video,
93        "text" => MediaKind::Text,
94        "application" => MediaKind::Application,
95        _ => MediaKind::Unknown,
96    }
97}
98
99pub fn parse_media_type(mime: &str) -> Result<MediaType, MediaTypeError> {
100    let base = mime.trim().split(';').next().unwrap_or("").trim();
101    let mut parts = base.splitn(2, '/');
102    let kind_part = parts.next().unwrap_or("").trim();
103    let subtype = parts.next().unwrap_or("").trim();
104
105    if kind_part.is_empty() || subtype.is_empty() {
106        return Err(MediaTypeError::InvalidMime);
107    }
108
109    MediaType::new(media_kind_from_mime(base), subtype)
110}
111
112#[must_use]
113pub fn is_image_mime(mime: &str) -> bool {
114    media_kind_from_mime(mime) == MediaKind::Image
115}
116
117#[must_use]
118pub fn is_audio_mime(mime: &str) -> bool {
119    media_kind_from_mime(mime) == MediaKind::Audio
120}
121
122#[must_use]
123pub fn is_video_mime(mime: &str) -> bool {
124    media_kind_from_mime(mime) == MediaKind::Video
125}
126
127#[cfg(test)]
128mod tests {
129    use super::{
130        MediaKind, MediaType, MediaTypeError, is_audio_mime, is_image_mime, is_video_mime,
131        media_kind_from_mime, parse_media_type,
132    };
133
134    #[test]
135    fn parses_simple_media_types() {
136        let image = parse_media_type("image/png").unwrap();
137        let audio = parse_media_type("audio/mpeg").unwrap();
138        let video = parse_media_type("video/mp4; codecs=avc1").unwrap();
139
140        assert_eq!(image.kind(), MediaKind::Image);
141        assert_eq!(audio.kind(), MediaKind::Audio);
142        assert_eq!(video.kind(), MediaKind::Video);
143        assert_eq!(video.subtype(), "mp4");
144        assert_eq!(video.as_mime(), "video/mp4");
145        assert_eq!(
146            MediaType::new(MediaKind::Text, "plain").unwrap().as_mime(),
147            "text/plain"
148        );
149    }
150
151    #[test]
152    fn checks_media_kind_predicates() {
153        assert_eq!(media_kind_from_mime("image/jpeg"), MediaKind::Image);
154        assert_eq!(
155            media_kind_from_mime("application/json"),
156            MediaKind::Application
157        );
158        assert_eq!(media_kind_from_mime("font/woff2"), MediaKind::Unknown);
159        assert!(is_image_mime("image/png"));
160        assert!(is_audio_mime("audio/ogg"));
161        assert!(is_video_mime("video/mp4"));
162        assert!(!is_video_mime("image/png"));
163    }
164
165    #[test]
166    fn rejects_invalid_media_type_inputs() {
167        assert_eq!(parse_media_type("image"), Err(MediaTypeError::InvalidMime));
168        assert_eq!(parse_media_type("/png"), Err(MediaTypeError::InvalidMime));
169        assert_eq!(
170            MediaType::new(MediaKind::Image, ""),
171            Err(MediaTypeError::InvalidSubtype)
172        );
173        assert_eq!(
174            MediaType::new(MediaKind::Image, "image/png"),
175            Err(MediaTypeError::InvalidSubtype)
176        );
177    }
178}