use bytes::Bytes;
use crate::error::HeaderError;
use crate::headers::grammar::{self, HeaderParam, is_token_char, parse_u64, skip_ws, trim};
use crate::message::{Method, TypedHeader};
use crate::name::HeaderName;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CSeq {
pub sequence: u32,
pub method: Method,
}
impl TypedHeader for CSeq {
const NAME: HeaderName = HeaderName::CSeq;
fn decode(value: &[u8]) -> Result<Self, HeaderError> {
let value = trim(value);
let space = value
.iter()
.position(|&b| matches!(b, b' ' | b'\t'))
.ok_or(HeaderError::Syntax { header: "CSeq" })?;
let digits = value.get(..space).unwrap_or(&[]);
let method_raw = trim(value.get(skip_ws(value, space)..).unwrap_or(&[]));
if method_raw.is_empty() || !method_raw.iter().all(|&b| is_token_char(b)) {
return Err(HeaderError::Syntax { header: "CSeq" });
}
let sequence = parse_u64(digits, "CSeq")?;
if sequence > u64::from(i32::MAX as u32) {
return Err(HeaderError::OutOfRange { header: "CSeq" });
}
Ok(Self {
sequence: u32::try_from(sequence)
.map_err(|_| HeaderError::OutOfRange { header: "CSeq" })?,
method: Method::parse(&Bytes::copy_from_slice(method_raw)),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CallId(pub Vec<u8>);
impl TypedHeader for CallId {
const NAME: HeaderName = HeaderName::CallId;
fn decode(value: &[u8]) -> Result<Self, HeaderError> {
let value = trim(value);
if value.is_empty() {
return Err(HeaderError::Syntax { header: "Call-ID" });
}
Ok(Self(value.to_vec()))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MaxForwards(pub u8);
impl TypedHeader for MaxForwards {
const NAME: HeaderName = HeaderName::MaxForwards;
fn decode(value: &[u8]) -> Result<Self, HeaderError> {
let n = parse_u64(trim(value), "Max-Forwards")?;
u8::try_from(n)
.map(Self)
.map_err(|_| HeaderError::OutOfRange {
header: "Max-Forwards",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Expires(pub u32);
impl TypedHeader for Expires {
const NAME: HeaderName = HeaderName::Expires;
fn decode(value: &[u8]) -> Result<Self, HeaderError> {
let n = parse_u64(trim(value), "Expires")?;
u32::try_from(n)
.map(Self)
.map_err(|_| HeaderError::OutOfRange { header: "Expires" })
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ContentLength(pub u64);
impl TypedHeader for ContentLength {
const NAME: HeaderName = HeaderName::ContentLength;
fn decode(value: &[u8]) -> Result<Self, HeaderError> {
parse_u64(trim(value), "Content-Length").map(Self)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContentType {
pub media_type: Vec<u8>,
pub subtype: Vec<u8>,
pub params: Vec<HeaderParam>,
}
impl ContentType {
#[must_use]
pub fn is(&self, media_type: &str, subtype: &str) -> bool {
self.media_type == media_type.as_bytes() && self.subtype == subtype.as_bytes()
}
#[must_use]
pub fn param(&self, name: &str) -> Option<&[u8]> {
grammar::param(&self.params, name).and_then(|p| p.value.as_deref())
}
}
impl TypedHeader for ContentType {
const NAME: HeaderName = HeaderName::ContentType;
fn decode(value: &[u8]) -> Result<Self, HeaderError> {
const LABEL: &str = "Content-Type";
let value = trim(value);
let (before_params, tail) = match grammar::find_param_start(value) {
Some(semi) => (
value.get(..semi).unwrap_or(&[]),
value.get(semi..).unwrap_or(&[]),
),
None => (value, &[][..]),
};
let slash = before_params
.iter()
.position(|&b| b == b'/')
.ok_or(HeaderError::Syntax { header: LABEL })?;
let media_type = trim(before_params.get(..slash).unwrap_or(&[]));
let subtype = trim(before_params.get(slash + 1..).unwrap_or(&[]));
if media_type.is_empty()
|| subtype.is_empty()
|| !media_type.iter().all(|&b| is_token_char(b))
|| !subtype.iter().all(|&b| is_token_char(b))
{
return Err(HeaderError::Syntax { header: LABEL });
}
Ok(Self {
media_type: media_type.to_ascii_lowercase(),
subtype: subtype.to_ascii_lowercase(),
params: grammar::parse_params(trim(tail), LABEL)?,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Date(pub Vec<u8>);
impl TypedHeader for Date {
const NAME: HeaderName = HeaderName::Date;
fn decode(value: &[u8]) -> Result<Self, HeaderError> {
const LABEL: &str = "Date";
const WKDAYS: [&[u8]; 7] = [b"Mon", b"Tue", b"Wed", b"Thu", b"Fri", b"Sat", b"Sun"];
const MONTHS: [&[u8]; 12] = [
b"Jan", b"Feb", b"Mar", b"Apr", b"May", b"Jun", b"Jul", b"Aug", b"Sep", b"Oct", b"Nov",
b"Dec",
];
let value = trim(value);
if value.len() != 29 {
return Err(HeaderError::Syntax { header: LABEL });
}
let field = |from: usize, to: usize| value.get(from..to).unwrap_or(&[]);
if field(3, 5) != b", "
|| field(7, 8) != b" "
|| field(11, 12) != b" "
|| field(16, 17) != b" "
|| field(19, 20) != b":"
|| field(22, 23) != b":"
|| field(25, 26) != b" "
|| field(26, 29) != b"GMT"
{
return Err(HeaderError::Syntax { header: LABEL });
}
if !WKDAYS.contains(&field(0, 3))
|| !MONTHS.contains(&field(8, 11))
|| !field(12, 16).iter().all(u8::is_ascii_digit)
{
return Err(HeaderError::Syntax { header: LABEL });
}
let day = two_digits(value, 5).ok_or(HeaderError::Syntax { header: LABEL })?;
let hour = two_digits(value, 17).ok_or(HeaderError::Syntax { header: LABEL })?;
let minute = two_digits(value, 20).ok_or(HeaderError::Syntax { header: LABEL })?;
let second = two_digits(value, 23).ok_or(HeaderError::Syntax { header: LABEL })?;
if !(1..=31).contains(&day) || hour > 23 || minute > 59 || second > 59 {
return Err(HeaderError::OutOfRange { header: LABEL });
}
Ok(Self(value.to_vec()))
}
}
#[must_use]
fn two_digits(value: &[u8], at: usize) -> Option<u8> {
let hi = value.get(at)?.checked_sub(b'0')?;
let lo = value.get(at + 1)?.checked_sub(b'0')?;
(hi <= 9 && lo <= 9).then_some(hi * 10 + lo)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TokenList(pub Vec<Vec<u8>>);
impl TokenList {
fn decode_named(
value: &[u8],
header: &'static str,
may_be_empty: bool,
) -> Result<Self, HeaderError> {
let mut tokens = Vec::new();
for part in grammar::split_list(value, header)? {
let token = trim(part);
if token.is_empty() {
if may_be_empty && grammar::split_list(value, header)?.len() == 1 {
return Ok(Self(Vec::new()));
}
return Err(HeaderError::Syntax { header });
}
if !token.iter().all(|&b| is_token_char(b)) {
return Err(HeaderError::Syntax { header });
}
tokens.push(token.to_vec());
}
Ok(Self(tokens))
}
#[must_use]
pub fn contains(&self, token: &str) -> bool {
self.0
.iter()
.any(|t| t.eq_ignore_ascii_case(token.as_bytes()))
}
}
macro_rules! token_list_header {
($(#[$meta:meta])* $type:ident => $variant:ident, $label:literal, $may_be_empty:literal) => {
$(#[$meta])*
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct $type(pub TokenList);
impl std::ops::Deref for $type {
type Target = TokenList;
fn deref(&self) -> &TokenList {
&self.0
}
}
impl TypedHeader for $type {
const NAME: HeaderName = HeaderName::$variant;
fn decode(value: &[u8]) -> Result<Self, HeaderError> {
TokenList::decode_named(value, $label, $may_be_empty).map(Self)
}
}
};
}
token_list_header!(
Allow => Allow, "Allow", true
);
token_list_header!(
Supported => Supported, "Supported", true
);
token_list_header!(
Require => Require, "Require", false
);
token_list_header!(
ProxyRequire => ProxyRequire, "Proxy-Require", false
);
token_list_header!(
Unsupported => Unsupported, "Unsupported", false
);
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing
)]
mod tests {
use super::*;
#[test]
fn cseq_parses_number_and_method() {
let c = CSeq::decode(b"8 INVITE").unwrap();
assert_eq!(c.sequence, 8);
assert_eq!(c.method, Method::Invite);
}
#[test]
fn cseq_accepts_leading_zeros_and_extra_whitespace() {
let c = CSeq::decode(b"0009 INVITE").unwrap();
assert_eq!(c.sequence, 9);
}
#[test]
fn cseq_rejects_overlarge_sequence_numbers() {
assert!(matches!(
CSeq::decode(b"2147483648 INVITE"),
Err(HeaderError::OutOfRange { header: "CSeq" })
));
assert!(matches!(
CSeq::decode(b"9292394834772304023312 OPTIONS"),
Err(HeaderError::OutOfRange { header: "CSeq" })
));
assert_eq!(
CSeq::decode(b"2147483647 INVITE").unwrap().sequence,
i32::MAX as u32
);
}
#[test]
fn cseq_rejects_a_missing_or_non_token_method() {
assert!(CSeq::decode(b"8").is_err());
assert!(CSeq::decode(b"8 IN VITE").is_err());
assert!(CSeq::decode(b"x INVITE").is_err());
}
#[test]
fn max_forwards_is_bounded_at_255() {
assert_eq!(MaxForwards::decode(b"0068").unwrap().0, 68);
assert_eq!(MaxForwards::decode(b"0").unwrap().0, 0);
assert!(matches!(
MaxForwards::decode(b"256"),
Err(HeaderError::OutOfRange { .. })
));
}
#[test]
fn content_type_lowercases_and_keeps_parameters() {
let ct = ContentType::decode(b"Application/SDP").unwrap();
assert!(ct.is("application", "sdp"));
let ct = ContentType::decode(b"multipart/mixed;boundary=unique-boundary-1").unwrap();
assert_eq!(ct.param("boundary"), Some(&b"unique-boundary-1"[..]));
}
#[test]
fn content_type_rejects_a_missing_subtype() {
assert!(ContentType::decode(b"application").is_err());
assert!(ContentType::decode(b"application/").is_err());
assert!(ContentType::decode(b"/sdp").is_err());
}
#[test]
fn date_requires_gmt() {
assert!(Date::decode(b"Fri, 01 Jan 2010 16:00:00 GMT").is_ok());
assert!(Date::decode(b"Fri, 01 Jan 2010 16:00:00 EST").is_err());
assert!(Date::decode(b"Fri, 01 Jan 2010 16:00:00").is_err());
assert!(Date::decode(b"nonsense GMT").is_err());
}
#[test]
fn date_validates_every_field_not_just_the_shape() {
for bad in [
&b"aaa, aaaaaaaaaaaaaaaaaaaa GMT"[..],
b"Fri, 32 Jan 2010 25:99:99 GMT",
b"Xyz, 01 Jan 2010 16:00:00 GMT",
b"Fri, 00 Jan 2010 16:00:00 GMT",
b"Fri, 01 Foo 2010 16:00:00 GMT",
b"Fri, 01 Jan 2010 24:00:00 GMT",
b"Fri, 01 Jan 2010 16:60:00 GMT",
b"Fri, 01 Jan 2010 16:00:60 GMT",
b"Fri, 01 Jan x010 16:00:00 GMT",
b"Fri, 01 Jan 2010 16.00.00 GMT",
] {
assert!(
Date::decode(bad).is_err(),
"{:?} should be rejected",
String::from_utf8_lossy(bad)
);
}
for good in [
&b"Mon, 01 Jan 2010 00:00:00 GMT"[..],
b"Sat, 13 Nov 2010 23:29:00 GMT",
b"Sun, 31 Dec 2699 23:59:59 GMT",
] {
assert!(
Date::decode(good).is_ok(),
"{:?} should parse",
String::from_utf8_lossy(good)
);
}
}
#[test]
fn token_lists_split_and_compare_case_insensitively() {
let allow = Allow::decode(b"INVITE, ACK, OPTIONS, CANCEL, BYE").unwrap();
assert_eq!(allow.0.0.len(), 5);
assert!(allow.contains("invite"));
assert!(!allow.contains("REFER"));
assert_eq!(Supported::decode(b"").unwrap().0.0.len(), 0);
assert!(Supported::decode(b"100rel,,timer").is_err());
}
#[test]
fn only_allow_and_supported_may_be_empty() {
assert!(Allow::decode(b"").is_ok());
assert!(Supported::decode(b"").is_ok());
assert!(Require::decode(b"").is_err());
assert!(ProxyRequire::decode(b"").is_err());
assert!(Unsupported::decode(b"").is_err());
assert!(Require::decode(b"100rel").is_ok());
assert!(ProxyRequire::decode(b"sec-agree").is_ok());
assert!(Unsupported::decode(b"foo, bar").is_ok());
}
#[test]
fn call_id_is_opaque_but_not_empty() {
assert_eq!(
CallId::decode(b"wsinv.ndaksdj@192.0.2.1").unwrap().0,
b"wsinv.ndaksdj@192.0.2.1"
);
assert!(CallId::decode(b" ").is_err());
}
}