sip-header 0.3.13

SIP header field parsers: Via, Warning, Auth, Accept, Contact, Call-Info, History-Info, Geolocation, Security, and full IANA header catalog
Documentation
//! SIP Accept header parser (RFC 3261 §20.1).

use std::fmt;

/// A single Accept entry: `type/subtype *(SEMI accept-param)`.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct SipAcceptEntry {
    media_range: String,
    slash_pos: usize,
    params: Vec<(String, String)>,
}

impl SipAcceptEntry {
    /// The media type (e.g. `"application"`).
    pub fn media_type(&self) -> &str {
        &self.media_range[..self.slash_pos]
    }

    /// The media subtype (e.g. `"sdp"`).
    pub fn subtype(&self) -> &str {
        &self.media_range[self.slash_pos + 1..]
    }

    /// The full media range as `type/subtype`.
    pub fn media_range(&self) -> &str {
        &self.media_range
    }

    /// All parameters as `(key, value)` pairs.
    pub fn params(&self) -> &[(String, String)] {
        &self.params
    }

    /// Look up a parameter by key (case-insensitive).
    pub fn param(&self, key: &str) -> Option<&str> {
        self.params
            .iter()
            .find(|(k, _)| k.eq_ignore_ascii_case(key))
            .map(|(_, v)| v.as_str())
    }

    /// The `q` quality value, if present.
    pub fn q(&self) -> Option<&str> {
        self.param("q")
    }
}

impl fmt::Display for SipAcceptEntry {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.media_range)?;
        write_accept_params(f, &self.params)
    }
}

/// Errors from parsing an Accept header value.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum SipAcceptError {
    /// An empty value. Not returned by parsing, which reads an empty value as
    /// the empty list RFC 3261 §25.1 allows.
    Empty,
    /// An entry could not be parsed.
    InvalidFormat(String),
}

impl fmt::Display for SipAcceptError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Empty => write!(f, "empty Accept header value"),
            Self::InvalidFormat(raw) => write!(f, "invalid Accept entry ({} bytes)", raw.len()),
        }
    }
}

impl std::error::Error for SipAcceptError {}

fn parse_accept_entry(raw: &str) -> Result<SipAcceptEntry, SipAcceptError> {
    let raw = raw.trim();
    if raw.is_empty() {
        return Err(SipAcceptError::InvalidFormat(raw.to_string()));
    }

    let (media_part, params_part) = match raw.split_once(';') {
        Some((m, p)) => (m.trim(), Some(p)),
        None => (raw, None),
    };

    let (type_str, subtype_str) = media_part
        .split_once('/')
        .ok_or_else(|| SipAcceptError::InvalidFormat(raw.to_string()))?;

    let type_str = type_str.trim();
    let subtype_str = subtype_str.trim();

    if type_str.is_empty() || subtype_str.is_empty() {
        return Err(SipAcceptError::InvalidFormat(raw.to_string()));
    }

    let mut media_range = type_str.to_ascii_lowercase();
    let slash_pos = media_range.len();
    media_range.push('/');
    media_range.push_str(&subtype_str.to_ascii_lowercase());

    Ok(SipAcceptEntry {
        media_range,
        slash_pos,
        params: read_accept_params(params_part.unwrap_or("")),
    })
}

/// Read `*(SEMI accept-param)`: keys lowercased, values raw, `""` for a flag.
pub(crate) fn read_accept_params(s: &str) -> Vec<(String, String)> {
    crate::parse_params(s)
        .into_iter()
        .map(|p| {
            (
                p.key
                    .to_ascii_lowercase(),
                p.value
                    .unwrap_or("")
                    .to_string(),
            )
        })
        .collect()
}

/// Write `*(SEMI accept-param)`, an empty value as a flag.
pub(crate) fn write_accept_params(
    f: &mut fmt::Formatter<'_>,
    params: &[(String, String)],
) -> fmt::Result {
    for (key, value) in params {
        crate::write_param(
            f,
            key,
            Some(value.as_str()).filter(|v| !v.is_empty()),
            false,
        )?;
    }
    Ok(())
}

/// True when every entry is blank: the empty list `[ accept-range *(COMMA
/// accept-range) ]` allows (RFC 3261 §25.1), shared by the Accept-* headers.
pub(crate) fn all_blank(entries: &[&str]) -> bool {
    entries
        .iter()
        .all(|e| {
            e.trim()
                .is_empty()
        })
}

/// Parsed SIP Accept header value.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct SipAccept(Vec<SipAcceptEntry>);

impl SipAccept {
    /// Parse a comma-separated Accept header value.
    ///
    /// An empty or whitespace-only value is the empty list (RFC 3261 §25.1).
    pub fn parse(raw: &str) -> Result<Self, SipAcceptError> {
        Self::from_entries(crate::split_comma_entries(raw))
    }

    /// Build from entries a transport already split; each is one `accept-range`.
    ///
    /// No entries, or only blank ones, is the empty list; a blank entry beside
    /// a real one is an error.
    pub fn from_entries<'a>(
        entries: impl IntoIterator<Item = &'a str>,
    ) -> Result<Self, SipAcceptError> {
        let entries: Vec<&str> = entries
            .into_iter()
            .collect();
        if all_blank(&entries) {
            return Ok(Self(Vec::new()));
        }
        entries
            .into_iter()
            .map(parse_accept_entry)
            .collect::<Result<_, _>>()
            .map(Self)
    }

    /// The parsed entries as a slice.
    pub fn entries(&self) -> &[SipAcceptEntry] {
        &self.0
    }

    /// Consume self and return entries as a `Vec`.
    pub fn into_entries(self) -> Vec<SipAcceptEntry> {
        self.0
    }

    /// Number of entries.
    pub fn len(&self) -> usize {
        self.0
            .len()
    }

    /// Returns `true` if there are no entries.
    pub fn is_empty(&self) -> bool {
        self.0
            .is_empty()
    }
}

impl fmt::Display for SipAccept {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        crate::fmt_joined(f, &self.0, ", ")
    }
}

impl_from_str_via_parse!(SipAccept, SipAcceptError);

impl<'a> IntoIterator for &'a SipAccept {
    type Item = &'a SipAcceptEntry;
    type IntoIter = std::slice::Iter<'a, SipAcceptEntry>;

    fn into_iter(self) -> Self::IntoIter {
        self.0
            .iter()
    }
}

impl IntoIterator for SipAccept {
    type Item = SipAcceptEntry;
    type IntoIter = std::vec::IntoIter<SipAcceptEntry>;

    fn into_iter(self) -> Self::IntoIter {
        self.0
            .into_iter()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn single_media_type() {
        let accept = SipAccept::parse("application/sdp").unwrap();
        assert_eq!(accept.len(), 1);
        assert_eq!(accept.entries()[0].media_type(), "application");
        assert_eq!(accept.entries()[0].subtype(), "sdp");
    }

    #[test]
    fn multiple_types() {
        let accept = SipAccept::parse("application/sdp, application/pidf+xml;q=0.5").unwrap();
        assert_eq!(accept.len(), 2);
        assert_eq!(accept.entries()[0].media_range(), "application/sdp");
        assert_eq!(accept.entries()[1].q(), Some("0.5"));
    }

    #[test]
    fn wildcard_type() {
        let accept = SipAccept::parse("*/*").unwrap();
        assert_eq!(accept.entries()[0].media_type(), "*");
        assert_eq!(accept.entries()[0].subtype(), "*");
    }

    #[test]
    fn wildcard_subtype() {
        let accept = SipAccept::parse("application/*").unwrap();
        assert_eq!(accept.entries()[0].media_type(), "application");
        assert_eq!(accept.entries()[0].subtype(), "*");
    }

    #[test]
    fn empty_input_is_empty_list() {
        assert!(SipAccept::parse("")
            .unwrap()
            .is_empty());
        assert!(SipAccept::parse("  ")
            .unwrap()
            .is_empty());
    }

    #[test]
    fn flag_param_roundtrip() {
        let raw = "application/sdp;foo";
        let accept = SipAccept::parse(raw).unwrap();
        assert_eq!(accept.entries()[0].param("foo"), Some(""));
        assert_eq!(accept.to_string(), raw);
    }

    #[test]
    fn quoted_param_keeps_semicolon() {
        let raw = r#"application/sdp;x="a;b";q=0.5"#;
        let accept = SipAccept::parse(raw).unwrap();
        assert_eq!(accept.entries()[0].param("x"), Some(r#""a;b""#));
        assert_eq!(accept.entries()[0].q(), Some("0.5"));
        assert_eq!(accept.to_string(), raw);
    }

    #[test]
    fn error_display_omits_input() {
        let err = SipAccept::parse("secretvalue").unwrap_err();
        assert!(!err
            .to_string()
            .contains("secretvalue"));
    }

    #[test]
    fn missing_slash() {
        assert!(matches!(
            SipAccept::parse("application"),
            Err(SipAcceptError::InvalidFormat(_))
        ));
    }

    #[test]
    fn from_str() {
        let accept: SipAccept = "application/sdp"
            .parse()
            .unwrap();
        assert_eq!(accept.len(), 1);
    }

    #[test]
    fn display_roundtrip() {
        let raw = "application/sdp;q=0.8";
        let accept = SipAccept::parse(raw).unwrap();
        assert_eq!(accept.to_string(), raw);
    }

    #[test]
    fn from_entries_matches_parse() {
        let a = "application/sdp";
        let b = "application/pidf+xml;q=0.5";
        let split = SipAccept::from_entries([a, b]).unwrap();
        let joined = SipAccept::parse(&format!("{a}, {b}")).unwrap();
        assert_eq!(split, joined);
    }

    #[test]
    fn from_entries_bad_entry_is_error() {
        assert!(matches!(
            SipAccept::from_entries(["application/sdp", "noslash"]),
            Err(SipAcceptError::InvalidFormat(_))
        ));
    }

    #[test]
    fn from_entries_empty_is_empty_list() {
        assert!(SipAccept::from_entries(std::iter::empty::<&str>())
            .unwrap()
            .is_empty());
        assert!(SipAccept::from_entries(["", "  "])
            .unwrap()
            .is_empty());
    }
}