use super::{
CacheControl, ContentType, Header,
cache_control::{NO_CACHE, NO_STORE, PRIVATE, PUBLIC},
};
use mime::{
APPLICATION_JSON, APPLICATION_OCTET_STREAM, APPLICATION_WWW_FORM_URLENCODED, TEXT_EVENT_STREAM,
TEXT_HTML, TEXT_HTML_UTF_8, TEXT_PLAIN, TEXT_PLAIN_UTF_8,
};
impl ContentType {
#[inline]
pub fn text() -> Header<Self> {
Self::from_static(TEXT_PLAIN.as_ref())
}
#[inline]
pub fn text_utf_8() -> Header<Self> {
Self::from_static(TEXT_PLAIN_UTF_8.as_ref())
}
#[inline]
pub fn html() -> Header<Self> {
Self::from_static(TEXT_HTML.as_ref())
}
#[inline]
pub fn html_utf_8() -> Header<Self> {
Self::from_static(TEXT_HTML_UTF_8.as_ref())
}
#[inline]
pub fn json() -> Header<Self> {
Self::from_static(APPLICATION_JSON.as_ref())
}
#[inline]
pub fn form() -> Header<Self> {
Self::from_static(APPLICATION_WWW_FORM_URLENCODED.as_ref())
}
#[inline]
pub fn events() -> Header<Self> {
Self::from_static(TEXT_EVENT_STREAM.as_ref())
}
#[inline]
pub fn stream() -> Header<Self> {
Self::from_static(APPLICATION_OCTET_STREAM.as_ref())
}
#[inline]
pub fn multipart_form_data(boundary: &str) -> Header<Self> {
Self::multipart_custom("form-data", boundary)
.expect("`form-data` is a static, valid subtype")
}
#[inline]
pub fn multipart_mixed(boundary: &str) -> Header<Self> {
Self::multipart_custom("mixed", boundary).expect("`mixed` is a static, valid subtype")
}
#[inline]
pub fn multipart_byte_ranges(boundary: &str) -> Header<Self> {
Self::multipart_custom("byteranges", boundary)
.expect("`byteranges` is a static, valid subtype")
}
pub fn multipart_custom(
subtype: &str,
boundary: &str,
) -> Result<Header<Self>, crate::error::Error> {
let value = if boundary_needs_quoting(boundary) {
format!("multipart/{subtype}; boundary=\"{boundary}\"")
} else {
format!("multipart/{subtype}; boundary={boundary}")
};
Self::from_bytes(value.as_bytes())
}
}
#[inline]
fn boundary_needs_quoting(value: &str) -> bool {
value.bytes().any(|b| {
matches!(
b,
b' ' | b'\t'
| b'('
| b')'
| b'<'
| b'>'
| b'@'
| b','
| b';'
| b':'
| b'\\'
| b'"'
| b'/'
| b'['
| b']'
| b'?'
| b'='
)
})
}
impl CacheControl {
#[inline]
pub fn no_cache() -> Header<Self> {
Self::from_static(NO_CACHE)
}
#[inline]
pub fn no_store() -> Header<Self> {
Self::from_static(NO_STORE)
}
#[inline]
pub fn max_age_0() -> Header<Self> {
Self::from_static("max-age=0")
}
#[inline]
pub fn public() -> Header<Self> {
Self::from_static(PUBLIC)
}
#[inline]
pub fn private() -> Header<Self> {
Self::from_static(PRIVATE)
}
}
#[cfg(test)]
mod tests {
use crate::headers::{CacheControl, ContentType, FromHeaders, Header};
fn assert_header_value<T>(h: Header<T>, expected: &str)
where
T: FromHeaders,
{
let v = h.as_str().expect("header value must be valid ASCII");
assert_eq!(v, expected);
}
#[test]
fn it_creates_content_type_text() {
assert_header_value(ContentType::text(), "text/plain");
}
#[test]
fn it_creates_content_type_text_utf_8() {
assert_header_value(ContentType::text_utf_8(), "text/plain; charset=utf-8");
}
#[test]
fn it_creates_content_type_html() {
assert_header_value(ContentType::html(), "text/html");
}
#[test]
fn it_creates_content_type_html_utf_8() {
assert_header_value(ContentType::html_utf_8(), "text/html; charset=utf-8");
}
#[test]
fn it_creates_content_type_json() {
assert_header_value(ContentType::json(), "application/json");
}
#[test]
fn it_creates_content_type_form() {
assert_header_value(ContentType::form(), "application/x-www-form-urlencoded");
}
#[test]
fn it_creates_content_type_events() {
assert_header_value(ContentType::events(), "text/event-stream");
}
#[test]
fn it_creates_content_type_stream() {
assert_header_value(ContentType::stream(), "application/octet-stream");
}
#[test]
fn it_creates_cache_control_no_cache() {
assert_header_value(CacheControl::no_cache(), "no-cache");
}
#[test]
fn it_creates_cache_control_no_store() {
assert_header_value(CacheControl::no_store(), "no-store");
}
#[test]
fn it_creates_cache_control_max_age_0() {
assert_header_value(CacheControl::max_age_0(), "max-age=0");
}
#[test]
fn it_creates_cache_control_public() {
assert_header_value(CacheControl::public(), "public");
}
#[test]
fn it_creates_cache_control_private() {
assert_header_value(CacheControl::private(), "private");
}
}
#[cfg(test)]
mod multipart_content_type_tests {
use super::ContentType;
#[test]
fn form_data_with_boundary() {
let h = ContentType::multipart_form_data("X-BOUNDARY");
assert_eq!(h.as_ref(), "multipart/form-data; boundary=X-BOUNDARY");
}
#[test]
fn mixed_with_boundary() {
let h = ContentType::multipart_mixed("XYZ");
assert_eq!(h.as_ref(), "multipart/mixed; boundary=XYZ");
}
#[test]
fn byteranges_with_boundary() {
let h = ContentType::multipart_byte_ranges("abc");
assert_eq!(h.as_ref(), "multipart/byteranges; boundary=abc");
}
#[test]
fn custom_subtype() {
let h = ContentType::multipart_custom("alternative", "abc").unwrap();
assert_eq!(h.as_ref(), "multipart/alternative; boundary=abc");
}
#[test]
fn custom_subtype_rejects_invalid_header_bytes() {
let err = ContentType::multipart_custom("evil\r\ninjected", "abc").unwrap_err();
assert!(!format!("{err}").is_empty());
}
#[test]
fn boundary_with_tspecials_is_quoted() {
let h = ContentType::multipart_form_data("a:b");
assert_eq!(h.as_ref(), "multipart/form-data; boundary=\"a:b\"");
}
#[test]
fn boundary_with_internal_space_is_quoted() {
let h = ContentType::multipart_form_data("with space");
assert_eq!(h.as_ref(), "multipart/form-data; boundary=\"with space\"");
}
#[test]
fn plain_token_boundary_remains_unquoted() {
let h = ContentType::multipart_form_data("plain-token_42");
assert_eq!(h.as_ref(), "multipart/form-data; boundary=plain-token_42");
}
}