#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MimeType {
pub mime_type: String,
pub subtype: String,
pub suffix: Option<String>,
}
const VALID_TYPES: &[&str] = &[
"application",
"audio",
"font",
"image",
"message",
"model",
"multipart",
"text",
"video",
"example",
];
fn is_valid_subtype_char(c: char) -> bool {
c.is_ascii_alphanumeric() || "!#$%&'*+.^_`|~-".contains(c)
}
fn is_valid_type_char(c: char) -> bool {
c.is_ascii_lowercase() || c == 'x' || c == '-'
}
pub fn parse_mime_type(mime_type_str: &str) -> Option<MimeType> {
let s = mime_type_str.trim();
let slash_pos = s.find('/')?;
let (mime_type_part, subtype_part) = s.split_at(slash_pos);
let subtype_part = &subtype_part[1..];
let mime_type = mime_type_part.to_lowercase();
if mime_type.is_empty() || !mime_type.chars().all(is_valid_type_char) {
return None;
}
let valid = VALID_TYPES.contains(&mime_type.as_str()) || mime_type.starts_with("x-");
if !valid {
return None;
}
let semicolon_pos = subtype_part.find(';');
let subtype_raw = if let Some(pos) = semicolon_pos {
&subtype_part[..pos]
} else {
subtype_part
};
if subtype_raw.is_empty() || !subtype_raw.chars().all(is_valid_subtype_char) {
return None;
}
let (subtype, suffix) = parse_sub_type(subtype_raw);
Some(MimeType {
mime_type,
subtype,
suffix,
})
}
fn parse_sub_type(value: &str) -> (String, Option<String>) {
let mut parts = value.splitn(2, '+');
let subtype = parts.next().unwrap_or(value).to_string();
let suffix = parts.next().map(|s| s.to_string());
(subtype, suffix)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_mime_type() {
let result = parse_mime_type("application/json").unwrap();
assert_eq!(result.mime_type, "application");
assert_eq!(result.subtype, "json");
assert_eq!(result.suffix, None);
}
#[test]
fn test_parse_text_plain() {
let result = parse_mime_type("text/plain").unwrap();
assert_eq!(result.mime_type, "text");
assert_eq!(result.subtype, "plain");
assert_eq!(result.suffix, None);
}
#[test]
fn test_parse_image_png() {
let result = parse_mime_type("image/png").unwrap();
assert_eq!(result.mime_type, "image");
assert_eq!(result.subtype, "png");
assert_eq!(result.suffix, None);
}
#[test]
fn test_parse_mime_type_with_suffix() {
let result = parse_mime_type("application/vnd.api+json").unwrap();
assert_eq!(result.mime_type, "application");
assert_eq!(result.subtype, "vnd.api");
assert_eq!(result.suffix, Some("json".to_string()));
}
#[test]
fn test_parse_mime_type_with_charset() {
let result = parse_mime_type("text/plain; charset=utf-8").unwrap();
assert_eq!(result.mime_type, "text");
assert_eq!(result.subtype, "plain");
assert_eq!(result.suffix, None);
}
#[test]
fn test_parse_invalid_mime_type() {
assert!(parse_mime_type("invalid").is_none());
assert!(parse_mime_type("").is_none());
assert!(parse_mime_type("text/").is_none());
assert!(parse_mime_type("/json").is_none());
}
#[test]
fn test_parse_custom_mime_type() {
let result = parse_mime_type("application/x-custom-type").unwrap();
assert_eq!(result.mime_type, "application");
assert_eq!(result.subtype, "x-custom-type");
assert_eq!(result.suffix, None);
}
#[test]
fn test_parse_mime_type_with_plus_suffix() {
let result = parse_mime_type("application/soap+xml").unwrap();
assert_eq!(result.mime_type, "application");
assert_eq!(result.subtype, "soap");
assert_eq!(result.suffix, Some("xml".to_string()));
}
#[test]
fn test_parse_case_insensitive_type() {
let result = parse_mime_type("APPLICATION/JSON").unwrap();
assert_eq!(result.mime_type, "application");
assert_eq!(result.subtype, "JSON");
}
}