pub(crate) const LIST_SEP: char = ',';
pub(crate) const PARAM_SEP: char = ';';
pub(crate) const VALUE_SEP: char = '=';
pub(crate) const SLASH: char = '/';
pub(crate) const RANGE_SEP: char = '-';
const DQUOTE: char = '"';
const BACKSLASH: char = '\\';
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct LexError(pub(crate) String);
fn err<T>(msg: impl Into<String>) -> Result<T, LexError> {
Err(LexError(msg.into()))
}
fn is_lws(c: char) -> bool {
matches!(c, ' ' | '\t' | '\r' | '\n')
}
pub(crate) fn trim_lws(s: &str) -> &str {
s.trim_matches(is_lws)
}
pub(crate) fn is_token(s: &str) -> bool {
!s.is_empty()
&& s.chars().all(|c| {
c.is_ascii()
&& !c.is_ascii_control()
&& !matches!(
c,
'(' | ')'
| '<'
| '>'
| '@'
| ','
| ';'
| ':'
| '\\'
| '"'
| '/'
| '['
| ']'
| '?'
| '='
| '{'
| '}'
| ' '
| '\t'
)
})
}
fn has_balanced_quotes(s: &str) -> bool {
let (mut in_quote, mut escaped) = (false, false);
for c in s.chars() {
if escaped {
escaped = false;
} else if in_quote && c == BACKSLASH {
escaped = true;
} else if c == DQUOTE {
in_quote = !in_quote;
}
}
!in_quote
}
pub(crate) fn split_session_value(s: &str) -> (&str, Vec<&str>) {
let mut segs = if has_balanced_quotes(s) {
split_outside_quotes(s, PARAM_SEP)
} else {
s.split(PARAM_SEP).collect()
};
if segs.is_empty() {
return ("", Vec::new());
}
let id = segs.remove(0);
(trim_lws(id), segs)
}
pub(crate) fn split_outside_quotes(s: &str, sep: char) -> Vec<&str> {
let mut out = Vec::new();
let (mut start, mut in_quote, mut escaped) = (0, false, false);
for (i, c) in s.char_indices() {
if escaped {
escaped = false;
} else if in_quote && c == BACKSLASH {
escaped = true;
} else if c == DQUOTE {
in_quote = !in_quote;
} else if c == sep && !in_quote {
out.push(&s[start..i]);
start = i + c.len_utf8();
}
}
out.push(&s[start..]);
out
}
pub(crate) fn split_param(seg: &str) -> Result<(&str, Option<&str>), LexError> {
let seg = trim_lws(seg);
let (name, value) = match seg.split_once(VALUE_SEP) {
Some((n, v)) => (trim_lws(n), Some(trim_lws(v))),
None => (seg, None),
};
if !is_token(name) {
return err(format!("invalid parameter name {name:?}"));
}
Ok((name, value))
}
pub(crate) fn unquote(v: &str) -> Result<(String, bool), LexError> {
let v = trim_lws(v);
if v.chars().any(|c| c.is_ascii_control()) {
return err("control character in a header value");
}
let Some(rest) = v.strip_prefix(DQUOTE) else {
return Ok((v.to_string(), false));
};
let mut out = String::new();
let mut chars = rest.chars();
loop {
match chars.next() {
None => return err("unterminated quoted-string"),
Some(BACKSLASH) => match chars.next() {
Some(c) => out.push(c),
None => return err("dangling quoted-pair"),
},
Some(DQUOTE) => {
return if trim_lws(chars.as_str()).is_empty() {
Ok((out, true))
} else {
err("text after a quoted-string")
};
}
Some(c) => out.push(c),
}
}
}
pub(crate) fn emit_value(v: &str) -> Result<String, LexError> {
if v.chars().any(|c| c.is_ascii_control()) {
return err("control character in a header value");
}
let bare = !v.is_empty()
&& v.chars()
.all(|c| !is_lws(c) && !matches!(c, ',' | ';' | '"' | '\\'))
&& v.trim_matches(is_lws) == v;
if bare {
return Ok(v.to_string());
}
emit_value_quoted(v)
}
pub(crate) fn emit_value_quoted(v: &str) -> Result<String, LexError> {
if v.chars().any(|c| c.is_ascii_control()) {
return err("control character in a header value");
}
let mut out = String::from(DQUOTE);
for c in v.chars() {
if c == DQUOTE || c == BACKSLASH {
out.push(BACKSLASH);
}
out.push(c);
}
out.push(DQUOTE);
Ok(out)
}
pub(crate) fn has_ctl(s: &str) -> bool {
s.chars().any(|c| c.is_ascii_control())
}
pub(crate) fn digits(s: &str, max_digits: usize, what: &str) -> Result<u32, LexError> {
let s = trim_lws(s);
if s.is_empty() || s.len() > max_digits || !s.bytes().all(|b| b.is_ascii_digit()) {
return err(format!(
"{what}: expected 1 to {max_digits} digits, got {s:?}"
));
}
s.parse::<u32>()
.or_else(|_| err(format!("{what}: {s:?} out of range")))
}
pub(crate) fn hex_exact(s: &str, digits: usize, what: &str) -> Result<u32, LexError> {
let s = trim_lws(s);
if s.len() != digits || !s.bytes().all(|b| b.is_ascii_hexdigit()) {
return err(format!("{what} must be {digits} hex digits, got {s:?}"));
}
u32::from_str_radix(s, 16).or_else(|_| err(format!("{what}: bad hex {s:?}")))
}
pub(crate) fn range(
v: &str,
max_digits: usize,
max: u32,
what: &str,
) -> Result<(u32, u32), LexError> {
let mut parts = v.split(RANGE_SEP);
let lo = digits(parts.next().unwrap_or(""), max_digits, what)?;
let hi = match parts.next() {
Some(h) => digits(h, max_digits, what)?,
None => lo,
};
if parts.next().is_some() {
return err(format!("{what}: more than two range ends in {v:?}"));
}
if lo > max || hi > max {
return err(format!("{what}: {v:?} exceeds {max}"));
}
Ok((lo, hi))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn splitting_respects_quoted_strings_and_quoted_pairs() {
assert_eq!(
split_outside_quotes(r#"a;b="x;y";c"#, ';'),
vec!["a", r#"b="x;y""#, "c"]
);
assert_eq!(
split_outside_quotes(r#"a="q\";x";b"#, ';'),
vec![r#"a="q\";x""#, "b"]
);
assert_eq!(split_outside_quotes("", ','), vec![""]);
}
#[test]
fn unquote_and_emit_round_trip() {
for v in ["PLAY", "a b", "a,b", "q\"x", "back\\slash", "", " lead"] {
assert_eq!(unquote(&emit_value(v).unwrap()).unwrap().0, v, "{v:?}");
}
assert!(matches!(
unquote("\"open"),
Err(crate::rfc2326_lex::LexError(..))
));
assert!(matches!(
emit_value("a\r\nb"),
Err(crate::rfc2326_lex::LexError(..))
));
assert!(matches!(
emit_value_quoted("a\nb"),
Err(crate::rfc2326_lex::LexError(..))
));
assert!(matches!(
unquote("\"a\r\nb\""),
Err(crate::rfc2326_lex::LexError(..))
));
assert!(matches!(
unquote("\"a\"junk"),
Err(crate::rfc2326_lex::LexError(..))
));
assert_eq!(unquote(" tok ").unwrap(), ("tok".into(), false));
}
#[test]
fn tokens_follow_the_tspecials_list() {
assert!(is_token("client_port") && is_token("3056-3057"));
assert!(!is_token("") && !is_token("a;b") && !is_token("a b") && !is_token("a=b"));
}
#[test]
fn ranges_check_digits_and_bounds() {
assert_eq!(range("1-2", 5, 65535, "port").unwrap(), (1, 2));
assert_eq!(range(" 7 ", 3, 255, "ch").unwrap(), (7, 7));
assert!(matches!(
range("65536", 5, 65535, "port"),
Err(crate::rfc2326_lex::LexError(..))
));
assert!(matches!(
range("123456", 5, 65535, "port"),
Err(crate::rfc2326_lex::LexError(..))
));
assert!(matches!(
range("1-2-3", 5, 65535, "port"),
Err(crate::rfc2326_lex::LexError(..))
));
assert!(matches!(
range("a", 5, 65535, "port"),
Err(crate::rfc2326_lex::LexError(..))
));
assert!(matches!(
range("-3", 5, 65535, "port"),
Err(crate::rfc2326_lex::LexError(..))
));
}
}