use crate::error::MediaErrorKind;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct MediaType<'a> {
type_: &'a [u8],
subtype: &'a [u8],
params: &'a [u8],
}
impl<'a> MediaType<'a> {
pub fn parse(value: &'a [u8]) -> Result<Self, MediaErrorKind> {
let trimmed = trim_ows(value);
if trimmed.is_empty() {
return Err(MediaErrorKind::Empty);
}
let (mt, params) = trimmed
.iter()
.position(|&b| b == b';')
.map_or_else(|| (trimmed, &b""[..]), |i| (&trimmed[..i], &trimmed[i..]));
let mt = trim_ows(mt);
let slash = mt
.iter()
.position(|&b| b == b'/')
.ok_or(MediaErrorKind::MissingSlash)?;
let type_ = trim_ows(&mt[..slash]);
let subtype = trim_ows(&mt[slash + 1..]);
if type_.is_empty() || subtype.is_empty() {
return Err(MediaErrorKind::InvalidToken);
}
Ok(Self {
type_,
subtype,
params,
})
}
#[inline]
#[must_use]
pub const fn type_(&self) -> &'a [u8] {
self.type_
}
#[inline]
#[must_use]
pub const fn subtype(&self) -> &'a [u8] {
self.subtype
}
#[must_use]
pub fn param(&self, name: &str) -> Option<&'a [u8]> {
let name = name.as_bytes();
for (n, v) in self.params() {
if n.eq_ignore_ascii_case(name) {
return Some(unquote(v));
}
}
None
}
#[inline]
#[must_use]
pub const fn params(&self) -> ParamsIter<'a> {
ParamsIter { rest: self.params }
}
}
#[derive(Clone, Debug)]
pub struct ParamsIter<'a> {
rest: &'a [u8],
}
impl<'a> Iterator for ParamsIter<'a> {
type Item = (&'a [u8], &'a [u8]);
fn next(&mut self) -> Option<Self::Item> {
loop {
self.rest = trim_ows(self.rest);
if self.rest.is_empty() {
return None;
}
if self.rest[0] != b';' {
return None;
}
self.rest = &self.rest[1..];
self.rest = trim_ows(self.rest);
if self.rest.is_empty() {
return None;
}
if self.rest[0] == b';' {
continue;
}
break;
}
let end = scan_param_end(self.rest);
let segment = &self.rest[..end];
self.rest = &self.rest[end..];
let (name, value) = segment.iter().position(|&b| b == b'=').map_or_else(
|| (segment, &b""[..]),
|eq| (&segment[..eq], &segment[eq + 1..]),
);
Some((trim_ows(name), trim_ows(value)))
}
}
fn scan_param_end(input: &[u8]) -> usize {
let mut i = 0;
let mut in_quote = false;
while i < input.len() {
let b = input[i];
if in_quote {
if b == b'\\' && i + 1 < input.len() {
i += 2;
continue;
}
if b == b'"' {
in_quote = false;
}
} else {
match b {
b'"' => in_quote = true,
b';' => return i,
_ => {}
}
}
i += 1;
}
input.len()
}
#[inline]
fn unquote(v: &[u8]) -> &[u8] {
if v.len() >= 2 && v[0] == b'"' && v[v.len() - 1] == b'"' {
&v[1..v.len() - 1]
} else {
v
}
}
#[inline]
const fn trim_ows(mut s: &[u8]) -> &[u8] {
while let [b' ' | b'\t', rest @ ..] = s {
s = rest;
}
while let [rest @ .., b' ' | b'\t'] = s {
s = rest;
}
s
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_simple() {
let mt = MediaType::parse(b"application/json").unwrap();
assert_eq!(mt.type_(), b"application");
assert_eq!(mt.subtype(), b"json");
assert!(mt.params().next().is_none());
}
#[test]
fn parse_with_charset() {
let mt = MediaType::parse(b"text/html; charset=utf-8").unwrap();
assert_eq!(mt.type_(), b"text");
assert_eq!(mt.subtype(), b"html");
assert_eq!(mt.param("charset"), Some(&b"utf-8"[..]));
}
#[test]
fn parse_multipart_with_boundary() {
let mt = MediaType::parse(b"multipart/form-data; boundary=----abc123").unwrap();
assert_eq!(mt.param("boundary"), Some(&b"----abc123"[..]));
}
#[test]
fn parse_quoted_boundary() {
let mt = MediaType::parse(b"multipart/form-data; boundary=\"abc; xyz\"").unwrap();
assert_eq!(mt.param("boundary"), Some(&b"abc; xyz"[..]));
}
#[test]
fn param_lookup_is_case_insensitive() {
let mt = MediaType::parse(b"text/html; CharSet=utf-8").unwrap();
assert_eq!(mt.param("charset"), Some(&b"utf-8"[..]));
assert_eq!(mt.param("CHARSET"), Some(&b"utf-8"[..]));
}
#[test]
fn parse_multiple_params() {
let mt = MediaType::parse(b"text/html; charset=utf-8; foo=bar").unwrap();
assert_eq!(mt.params().count(), 2);
assert_eq!(mt.param("foo"), Some(&b"bar"[..]));
}
#[test]
fn parse_trims_outer_whitespace() {
let mt = MediaType::parse(b" application/json ").unwrap();
assert_eq!(mt.type_(), b"application");
assert_eq!(mt.subtype(), b"json");
}
#[test]
fn parse_trims_around_slash() {
let mt = MediaType::parse(b"text / html").unwrap();
assert_eq!(mt.type_(), b"text");
assert_eq!(mt.subtype(), b"html");
}
#[test]
fn missing_param_returns_none() {
let mt = MediaType::parse(b"text/plain").unwrap();
assert_eq!(mt.param("charset"), None);
}
#[test]
fn empty_input_errors() {
assert_eq!(MediaType::parse(b""), Err(MediaErrorKind::Empty));
assert_eq!(MediaType::parse(b" "), Err(MediaErrorKind::Empty));
}
#[test]
fn missing_slash_errors() {
assert_eq!(
MediaType::parse(b"applicationjson"),
Err(MediaErrorKind::MissingSlash)
);
}
#[test]
fn empty_type_or_subtype_errors() {
assert_eq!(
MediaType::parse(b"/json"),
Err(MediaErrorKind::InvalidToken)
);
assert_eq!(
MediaType::parse(b"application/"),
Err(MediaErrorKind::InvalidToken)
);
assert_eq!(MediaType::parse(b"/"), Err(MediaErrorKind::InvalidToken));
}
#[test]
fn empty_param_segment_is_skipped() {
let mt = MediaType::parse(b"text/html;; charset=utf-8").unwrap();
assert_eq!(mt.param("charset"), Some(&b"utf-8"[..]));
}
#[test]
fn param_without_value() {
let mt = MediaType::parse(b"text/html; flag").unwrap();
let collected: Vec<_> = mt.params().collect();
assert_eq!(collected, &[(&b"flag"[..], &b""[..])]);
}
#[test]
fn unquote_helper() {
assert_eq!(unquote(b"\"x\""), b"x");
assert_eq!(unquote(b"x"), b"x");
assert_eq!(unquote(b"\""), b"\""); assert_eq!(unquote(b""), b"");
}
}