cookie-monster 0.2.2

A Cookie library for managing HTTP Cookies, with Axum integration.
Documentation
use std::borrow::Cow;

use crate::{Cookie, error::Error, util::TinyStr};

impl Cookie {
    /// Parses the given cookie header value. Errors when:
    /// * No '=' is found.
    /// * The name is empty.
    /// * The name contains an invalid character.
    /// * The cookie value contains an invalid character.
    ///
    /// Since this only parses a cookie header value, it does not parse any cookie attributes.
    pub fn parse_cookie(string: impl Into<Box<str>>) -> crate::Result<Cookie> {
        Self::parse_inner(string.into(), |name, value| {
            Ok((Cow::Borrowed(name), Cow::Borrowed(value)))
        })
    }

    /// Parses a percent encoded cookie value. Errors when:
    /// * No '=' is found.
    /// * The name is empty.
    /// * The name contains an invalid character.
    /// * The cookie value contains an invalid character.
    ///
    /// Since this only parses a cookie header value, it does not parse any cookie attributes.
    #[cfg(feature = "percent-encode")]
    pub fn parse_cookie_encoded(string: impl Into<Box<str>>) -> crate::Result<Cookie> {
        use crate::cookie::encoding;

        Self::parse_inner(string.into(), encoding::decode_name_value)
    }

    fn parse_inner(
        mut string: Box<str>,
        callback: impl for<'a> Fn(&'a str, &'a str) -> crate::Result<(Cow<'a, str>, Cow<'a, str>)>,
    ) -> Result<Cookie, Error> {
        let mut parts = SplitMut::new(&mut string);

        let name_value = parts.next().expect("First split always returns something");

        // 2.  If the name-value-pair string lacks a %x3D ("=") character,
        //     ignore the set-cookie-string entirely.
        let Some(index) = name_value.find('=') else {
            return Err(Error::EqualsNotFound);
        };

        // 4.  Remove any leading or trailing WSP characters from the name
        //     string and the value string.
        let name = name_value[..index].trim();
        let mut value = name_value[(index + 1)..].trim();

        // 5.  If the name string is empty, ignore the set-cookie-string entirely.
        if name.is_empty() {
            return Err(Error::NameEmpty);
        } else if let Some(token) = invalid_token(name) {
            return Err(Error::InvalidName(token));
        }

        // Remove optional brackets.
        value = trim_quotes(value);

        if let Some(invalid_char) = find_invalid_cookie_value(value) {
            return Err(Error::InvalidValue(invalid_char));
        }

        // Optionally decode the name and value.
        let (name, value) = callback(name, value)?;

        // Strip a recognized `__Host-` / `__Secure-` prefix off the name and remember it.
        let (prefix, name) = crate::cookie::prefix::split_prefix(name);

        let name = TinyStr::from_cow_ref(name, parts.ptr);
        let value = TinyStr::from_cow_ref(value, parts.ptr);

        let mut cookie = Cookie::new_inner(name, value);
        cookie.prefix = prefix;
        cookie.raw_value = Some(string);
        Ok(cookie)
    }
}

struct SplitMut<'s> {
    haystack: Option<&'s mut str>,
    ptr: *const u8,
}

impl<'s> SplitMut<'s> {
    pub fn new(haystack: &'s mut str) -> SplitMut<'s> {
        SplitMut {
            ptr: haystack.as_ptr(),
            haystack: Some(haystack),
        }
    }
}

impl<'s> Iterator for SplitMut<'s> {
    type Item = &'s mut str;

    fn next(&mut self) -> Option<Self::Item> {
        let remainder = self.haystack.take()?;

        match remainder.find(';') {
            Some(index) => {
                let (current, rest) = remainder.split_at_mut(index);

                if !rest.is_empty() {
                    self.haystack = Some(&mut rest[1..]);
                } else {
                    self.haystack = Some(&mut rest[..]);
                }

                Some(current)
            }
            None => Some(remainder),
        }
    }
}

#[inline]
fn invalid_cookie_value_char(val: &char) -> bool {
    match val {
        ' ' | '"' | ',' | ';' | '\\' => true,
        control if control.is_ascii_control() => true,
        _ => false,
    }
}

pub fn trim_quotes(value: &str) -> &str {
    if value.len() > 1 && value.starts_with('"') && value.ends_with('"') {
        &value[1..(value.len() - 1)]
    } else {
        value
    }
}

#[inline]
pub fn invalid_token(val: &str) -> Option<char> {
    val.chars().find(|c| match c {
        '!' | '#' | '$' | '%' | '&' | '\'' | '*' | '+' | '-' | '.' | '^' | '_' | '`' | '|'
        | '~' => false,
        c if c.is_alphanumeric() => false,
        _ => true,
    })
}

#[inline]
pub fn find_invalid_cookie_value(val: &str) -> Option<char> {
    val.chars().find(invalid_cookie_value_char)
}