1#![forbid(unsafe_code)]
2#[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}