use crate::{
class,
error::{ErrorKind, ParseError, Result, WriteError, err},
limits::Limits,
param::{self, Params},
scan::control_kind,
text::{Word, is_token, skip_ows, trim_ows},
write::{Sink, reserve, word_len, write_word},
};
pub fn parse_media_type(value: &[u8]) -> Result<MediaType<'_>> {
parse(value, &Limits::DEFAULT)
}
fn bad_byte(value: &[u8], at: usize) -> ParseError {
match value.get(at) {
Some(&b) if b < 0x20 || b == 0x7f => ParseError::new(control_kind(b), at),
_ => ParseError::new(ErrorKind::InvalidMediaType, at),
}
}
fn essence(value: &[u8], base: usize) -> Result<(&[u8], &[u8], usize)> {
let type_len = value.iter().take_while(|&&b| class::is_tchar(b)).count();
if type_len == 0 {
return Err(bad_byte(value, 0).shifted(base));
}
if value.get(type_len) != Some(&b'/') {
return Err(bad_byte(value, type_len).shifted(base));
}
let sub_start = type_len + 1;
let sub_len = value
.get(sub_start..)
.unwrap_or(&[])
.iter()
.take_while(|&&b| class::is_tchar(b))
.count();
if sub_len == 0 {
return Err(bad_byte(value, sub_start).shifted(base));
}
let type_ = value.get(..type_len).unwrap_or(&[]);
let subtype = value.get(sub_start..sub_start + sub_len).unwrap_or(&[]);
Ok((type_, subtype, sub_start + sub_len))
}
pub fn parse<'a>(value: &'a [u8], limits: &Limits) -> Result<MediaType<'a>> {
let lead = skip_ows(value);
let trimmed = trim_ows(value);
let (type_, subtype, used) = essence(trimmed, lead)?;
if type_ == b"*" {
return err(ErrorKind::Wildcard, lead);
}
if subtype == b"*" {
return err(ErrorKind::Wildcard, lead + type_.len() + 1);
}
let rest = trimmed.get(used..).unwrap_or(&[]);
let (params, n) = param::parse(rest, limits).map_err(|e| e.shifted(lead + used))?;
let end = used + n;
if end < trimmed.len() {
let at = end + skip_ows(trimmed.get(end..).unwrap_or(&[]));
return Err(bad_byte(trimmed, at).shifted(lead));
}
Ok(MediaType {
type_,
subtype,
params,
})
}
pub fn write(
type_: &str,
subtype: &str,
params: &[(&str, &str)],
out: &mut impl Sink,
) -> core::result::Result<(), WriteError> {
let (t, s) = (type_.as_bytes(), subtype.as_bytes());
if !is_token(t) || !is_token(s) || t == b"*" || s == b"*" {
return Err(WriteError::NotToken);
}
let mut len = t.len().saturating_add(1).saturating_add(s.len());
for (name, value) in params {
if !is_token(name.as_bytes()) {
return Err(WriteError::NotToken);
}
let v = word_len(value.as_bytes())?;
len = len
.saturating_add(3)
.saturating_add(name.len())
.saturating_add(v);
}
reserve(out, len)?;
out.put(t)?;
out.put(b"/")?;
out.put(s)?;
for (name, value) in params {
out.put(b"; ")?;
out.put(name.as_bytes())?;
out.put(b"=")?;
write_word(value.as_bytes(), out)?;
}
Ok(())
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct MediaType<'a> {
type_: &'a [u8],
subtype: &'a [u8],
params: Params<'a>,
}
impl<'a> MediaType<'a> {
#[must_use]
pub const fn type_(&self) -> &'a [u8] {
self.type_
}
#[must_use]
pub const fn subtype(&self) -> &'a [u8] {
self.subtype
}
#[must_use]
pub fn suffix(&self) -> Option<&'a [u8]> {
let plus = self.subtype.iter().rposition(|&b| b == b'+')?;
self.subtype.get(plus + 1..).filter(|s| !s.is_empty())
}
#[must_use]
pub fn essence_is(&self, essence: &str) -> bool {
match essence.split_once('/') {
Some((t, s)) => self.is(t, s),
None => false,
}
}
#[must_use]
pub fn is(&self, type_: &str, subtype: &str) -> bool {
self.type_.eq_ignore_ascii_case(type_.as_bytes())
&& self.subtype.eq_ignore_ascii_case(subtype.as_bytes())
}
#[must_use]
pub const fn params(&self) -> Params<'a> {
self.params
}
#[must_use]
pub fn param(&self, name: &str) -> Option<Word<'a>> {
self.params.get(name)
}
#[must_use]
pub fn charset(&self) -> Option<Word<'a>> {
self.params.get("charset")
}
pub fn boundary(&self) -> Result<Option<Boundary<'a>>> {
let Some(word) = self.params.get("boundary") else {
return Ok(None);
};
let bytes = match word {
Word::Token(t) => t,
Word::Quoted(q) if q.has_escapes() => {
let at = q.raw().iter().position(|&b| b == b'\\').unwrap_or(0);
return err(ErrorKind::InvalidBoundary, at);
}
Word::Quoted(q) => q.raw(),
};
if bytes.is_empty() {
return err(ErrorKind::InvalidBoundary, 0);
}
if let Some(at) = bytes
.iter()
.position(|&b| !(class::is_bchar_nospace(b) || b == b' '))
{
return err(ErrorKind::InvalidBoundary, at);
}
if bytes.len() > 70 {
return err(ErrorKind::InvalidBoundary, 70);
}
if bytes.last() == Some(&b' ') {
return err(ErrorKind::InvalidBoundary, bytes.len() - 1);
}
Ok(Some(Boundary(bytes)))
}
#[must_use]
pub fn is_json(&self) -> bool {
self.is("application", "json")
|| self
.suffix()
.is_some_and(|s| s.eq_ignore_ascii_case(b"json"))
}
#[must_use]
pub fn is_form_urlencoded(&self) -> bool {
self.is("application", "x-www-form-urlencoded")
}
#[must_use]
pub fn is_multipart_form_data(&self) -> bool {
self.is("multipart", "form-data")
}
pub fn write_canonical(&self, out: &mut impl Sink) -> core::result::Result<(), WriteError> {
let mut len = self.type_.len() + 1 + self.subtype.len();
for p in self.params.iter() {
len = len.saturating_add(3 + p.name.len() + raw_word_len(&p.value));
}
reserve(out, len)?;
put_lower(self.type_, out)?;
out.put(b"/")?;
put_lower(self.subtype, out)?;
for p in self.params.iter() {
out.put(b"; ")?;
put_lower(p.name, out)?;
out.put(b"=")?;
match p.value {
Word::Token(t) => out.put(t)?,
Word::Quoted(q) => {
out.put(b"\"")?;
out.put(q.raw())?;
out.put(b"\"")?;
}
}
}
Ok(())
}
}
fn raw_word_len(w: &Word<'_>) -> usize {
match w {
Word::Token(t) => t.len(),
Word::Quoted(q) => q.raw().len() + 2,
}
}
fn put_lower(s: &[u8], out: &mut impl Sink) -> core::result::Result<(), WriteError> {
let mut buf = [0u8; 64];
for chunk in s.chunks(buf.len()) {
let dst = buf.get_mut(..chunk.len()).unwrap_or(&mut []);
for (d, &c) in dst.iter_mut().zip(chunk) {
*d = c.to_ascii_lowercase();
}
out.put(dst)?;
}
Ok(())
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Boundary<'a>(&'a [u8]);
impl<'a> Boundary<'a> {
#[must_use]
pub const fn as_bytes(&self) -> &'a [u8] {
self.0
}
}
#[cfg(test)]
mod tests {
use std::{vec, vec::Vec};
use super::*;
use crate::write::SliceSink;
fn mt(s: &[u8]) -> Result<MediaType<'_>> {
parse_media_type(s)
}
#[test]
fn test_media_type_edge_cases() {
assert!(mt(b"text/html").unwrap().is("text", "html"));
assert!(mt(b"TEXT/HTML").unwrap().essence_is("text/html"));
assert!(mt(b"text/html;").unwrap().params().is_empty());
assert!(mt(b"text/html ;charset=utf-8").unwrap().charset().is_some());
assert!(mt(b" text/html \t").is_ok());
let e = mt(b"text/ html").unwrap_err();
assert_eq!((e.kind(), e.offset()), (ErrorKind::InvalidMediaType, 5));
let e = mt(b"text /html").unwrap_err();
assert_eq!((e.kind(), e.offset()), (ErrorKind::InvalidMediaType, 4));
assert_eq!(
mt(b"text/").unwrap_err().kind(),
ErrorKind::InvalidMediaType
);
assert_eq!(
mt(b"/html").unwrap_err().kind(),
ErrorKind::InvalidMediaType
);
assert_eq!(mt(b"").unwrap_err().kind(), ErrorKind::InvalidMediaType);
assert_eq!(mt(b"text").unwrap_err().kind(), ErrorKind::InvalidMediaType);
assert_eq!(mt(b"*/*").unwrap_err().kind(), ErrorKind::Wildcard);
let e = mt(b"text/*").unwrap_err();
assert_eq!((e.kind(), e.offset()), (ErrorKind::Wildcard, 5));
assert_eq!(
mt(b"text/html x").unwrap_err().kind(),
ErrorKind::InvalidMediaType
);
assert_eq!(mt(b"text/html\r").unwrap_err().kind(), ErrorKind::BareCr);
assert_eq!(
mt(b"text/html;a=b\nc").unwrap_err().kind(),
ErrorKind::LineFeed
);
let e = mt(b"text/html; charset=a; Charset=b").unwrap_err();
assert_eq!((e.kind(), e.offset()), (ErrorKind::DuplicateParameter, 22));
}
#[test]
fn test_media_type_suffix_and_predicates() {
let v = mt(b"application/vnd.api+json").unwrap();
assert_eq!(v.suffix(), Some(&b"json"[..]));
assert!(v.is_json());
assert!(mt(b"application/json").unwrap().is_json());
assert!(!mt(b"application/jsonx").unwrap().is_json());
assert_eq!(mt(b"application/a+").unwrap().suffix(), None);
assert!(
mt(b"application/x-www-form-urlencoded")
.unwrap()
.is_form_urlencoded()
);
assert!(!mt(b"text/html").unwrap().essence_is("texthtml"));
}
fn boundary(s: &[u8]) -> Result<Option<Vec<u8>>> {
Ok(mt(s)?.boundary()?.map(|b| b.as_bytes().to_vec()))
}
#[test]
fn test_boundary_rfc2046_rules() {
assert_eq!(boundary(b"multipart/form-data").unwrap(), None);
assert_eq!(
boundary(b"multipart/form-data; boundary=")
.unwrap_err()
.kind(),
ErrorKind::MissingValue
);
assert_eq!(
boundary(b"multipart/form-data; boundary=\"\"")
.unwrap_err()
.kind(),
ErrorKind::InvalidBoundary
);
let seventy = vec![b'a'; 70];
let mut v = b"multipart/mixed; boundary=".to_vec();
v.extend_from_slice(&seventy);
assert_eq!(boundary(&v).unwrap(), Some(seventy));
v.push(b'a');
assert_eq!(boundary(&v).unwrap_err().kind(), ErrorKind::InvalidBoundary);
assert_eq!(
boundary(b"multipart/mixed; boundary=\"ab \"")
.unwrap_err()
.kind(),
ErrorKind::InvalidBoundary
);
assert_eq!(
boundary(b"multipart/mixed; boundary=\"a b\"").unwrap(),
Some(b"a b".to_vec())
);
assert_eq!(
boundary(b"multipart/mixed; boundary=\"a\\\\b\"")
.unwrap_err()
.kind(),
ErrorKind::InvalidBoundary
);
assert_eq!(
boundary(b"multipart/mixed; boundary=a*b")
.unwrap_err()
.kind(),
ErrorKind::InvalidBoundary
);
assert_eq!(
mt(b"multipart/mixed; boundary=a; boundary=b")
.unwrap_err()
.kind(),
ErrorKind::DuplicateParameter
);
}
#[test]
fn test_media_write_refuses_bad_names_and_round_trips() {
let mut buf = [0u8; 128];
let mut out = SliceSink::new(&mut buf);
assert_eq!(write("text", "*", &[], &mut out), Err(WriteError::NotToken));
assert_eq!(
write("te xt", "a", &[], &mut out),
Err(WriteError::NotToken)
);
assert_eq!(
write("a", "b", &[("x y", "1")], &mut out),
Err(WriteError::NotToken)
);
assert_eq!(
write("a", "b", &[("x", "1\r\n")], &mut out),
Err(WriteError::Forbidden { at: 1 })
);
assert!(out.is_empty());
write("a", "b", &[("x", "1 2"), ("y", "")], &mut out).unwrap();
let back = mt(out.written()).unwrap();
assert!(back.param("x").unwrap().eq_bytes(b"1 2"));
assert!(back.param("y").unwrap().is_empty());
}
#[test]
fn test_write_canonical_lowercases_names_only() {
let v = mt(b"TEXT/Plain; FORMAT=Flowed;x=\"A\\\"B\"").unwrap();
let mut buf = [0u8; 64];
let mut out = SliceSink::new(&mut buf);
v.write_canonical(&mut out).unwrap();
assert_eq!(out.written(), b"text/plain; format=Flowed; x=\"A\\\"B\"");
let mut small = [0u8; 4];
let mut out = SliceSink::new(&mut small);
assert_eq!(v.write_canonical(&mut out), Err(WriteError::Full));
assert!(out.is_empty());
}
}