use alloc::string::{String, ToString};
use alloc::vec::Vec;
pub(crate) fn is_token_char(b: u8) -> bool {
matches!(
b,
b'!' | b'#' | b'$' | b'%' | b'&' | b'\'' | b'*' | b'+' | b'-' | b'.' |
b'0'..=b'9' | b'A'..=b'Z' | b'^' | b'_' | b'`' | b'a'..=b'z' | b'|' | b'~'
)
}
pub(crate) fn is_valid_header_name(name: &str) -> bool {
!name.is_empty() && name.bytes().all(is_token_char)
}
pub(crate) fn is_valid_token(s: &str) -> bool {
!s.is_empty() && s.bytes().all(is_token_char)
}
pub(crate) fn is_valid_field_vchar(b: u8) -> bool {
matches!(b, 0x09 | 0x20..=0x7E | 0x80..=0xFF)
}
pub(crate) fn is_valid_field_value(value: &str) -> bool {
value.bytes().all(is_valid_field_vchar)
}
pub(crate) fn is_valid_method(method: &str) -> bool {
!method.is_empty() && method.bytes().all(is_token_char)
}
pub(crate) fn is_valid_protocol_version(version: &str) -> bool {
let bytes = version.as_bytes();
let slash_pos = match bytes.iter().position(|&b| b == b'/') {
Some(pos) => pos,
None => return false,
};
if slash_pos == 0 {
return false;
}
if !bytes[..slash_pos].iter().all(|&b| is_token_char(b)) {
return false;
}
let after_slash = &bytes[slash_pos + 1..];
let dot_pos = match after_slash.iter().position(|&b| b == b'.') {
Some(pos) => pos,
None => return false,
};
if dot_pos == 0 {
return false;
}
if !after_slash[..dot_pos].iter().all(|b| b.is_ascii_digit()) {
return false;
}
let after_dot = &after_slash[dot_pos + 1..];
if after_dot.is_empty() {
return false;
}
after_dot.iter().all(|b| b.is_ascii_digit())
}
pub(crate) fn is_valid_status_code(code: u16) -> bool {
(100..=599).contains(&code)
}
pub(crate) fn is_valid_reason_phrase(phrase: &str) -> bool {
!phrase.is_empty()
&& phrase
.bytes()
.all(|b| matches!(b, 0x09 | 0x20..=0x7E | 0x80..=0xFF))
}
pub(crate) const RFC3986_EXCLUDED: &[u8] = b"\"#<>\\^`{|}";
pub(crate) fn is_valid_request_target(target: &str) -> bool {
if target.is_empty() {
return false;
}
let bytes = target.as_bytes();
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
if b <= 0x20 || b == 0x7F {
return false;
}
if RFC3986_EXCLUDED.contains(&b) {
return false;
}
if b == b'%' {
if i + 2 >= bytes.len() {
return false; }
let high = bytes[i + 1];
let low = bytes[i + 2];
if !high.is_ascii_hexdigit() || !low.is_ascii_hexdigit() {
return false; }
if high == b'0' && low == b'0' {
return false;
}
i += 3;
continue;
}
i += 1;
}
true
}
pub(crate) fn is_pchar_or_slash(b: u8) -> bool {
is_pchar_byte(b) || b == b'/'
}
pub(crate) fn is_pchar_byte(b: u8) -> bool {
is_unreserved_byte(b) || is_sub_delim_byte(b) || b == b':' || b == b'@'
}
pub(crate) fn is_query_char(b: u8) -> bool {
is_pchar_byte(b) || b == b'/' || b == b'?'
}
pub(crate) fn is_unreserved_byte(b: u8) -> bool {
b.is_ascii_alphanumeric() || b == b'-' || b == b'.' || b == b'_' || b == b'~'
}
pub(crate) fn is_sub_delim_byte(b: u8) -> bool {
matches!(
b,
b'!' | b'$' | b'&' | b'\'' | b'(' | b')' | b'*' | b'+' | b',' | b';' | b'='
)
}
pub(crate) fn is_qdtext_char(c: char) -> bool {
matches!(c, '\t' | ' ' | '!' | '#'..='[' | ']'..='~') || c as u32 >= 0x80
}
pub(crate) fn is_quoted_pair_char(c: char) -> bool {
matches!(c, '\t' | ' '..='~') || c as u32 >= 0x80
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum QuotedStringError {
InvalidQdtext,
InvalidQuotedPair,
Unterminated,
}
pub(crate) fn parse_quoted_string(input: &str) -> Result<(String, &str), QuotedStringError> {
let mut result = String::new();
let mut escaped = false;
for (i, c) in input.char_indices() {
if escaped {
if !is_quoted_pair_char(c) {
return Err(QuotedStringError::InvalidQuotedPair);
}
result.push(c);
escaped = false;
} else if c == '\\' {
escaped = true;
} else if c == '"' {
return Ok((result, &input[i + 1..]));
} else {
if !is_qdtext_char(c) {
return Err(QuotedStringError::InvalidQdtext);
}
result.push(c);
}
}
Err(QuotedStringError::Unterminated)
}
pub(crate) fn escape_quotes(s: &str) -> String {
let mut result = String::with_capacity(s.len());
for c in s.chars() {
if !is_quoted_pair_char(c) {
result.push(' '); continue;
}
if c == '"' || c == '\\' {
result.push('\\');
}
result.push(c);
}
result
}
pub(crate) fn trim_ows(s: &str) -> &str {
let bytes = s.as_bytes();
let start = bytes
.iter()
.position(|&b| b != b' ' && b != b'\t')
.unwrap_or(bytes.len());
let end = bytes
.iter()
.rposition(|&b| b != b' ' && b != b'\t')
.map(|p| p + 1)
.unwrap_or(start);
&s[start..end]
}
pub(crate) fn split_with_quotes(input: &str, delimiter: char) -> Vec<String> {
let mut parts = Vec::new();
let mut start = 0;
let mut in_quote = false;
let mut escaped = false;
for (i, c) in input.char_indices() {
if escaped {
escaped = false;
continue;
}
if c == '\\' && in_quote {
escaped = true;
continue;
}
if c == '"' {
in_quote = !in_quote;
continue;
}
if c == delimiter && !in_quote {
parts.push(input[start..i].to_string());
start = i + c.len_utf8();
}
}
parts.push(input[start..].to_string());
parts
}
pub(crate) fn is_valid_language_tag(tag: &str) -> bool {
if tag.is_empty() {
return false;
}
let mut parts = tag.split('-');
let Some(primary) = parts.next() else {
return false;
};
if primary.is_empty() || primary.len() > 8 || !primary.chars().all(|c| c.is_ascii_alphabetic())
{
return false;
}
for part in parts {
if part.is_empty() || part.len() > 8 || !part.chars().all(|c| c.is_ascii_alphanumeric()) {
return false;
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn escape_quotes_passes_through_safe_chars() {
assert_eq!(escape_quotes(""), "");
assert_eq!(escape_quotes("hello"), "hello");
assert_eq!(escape_quotes("日本語"), "日本語");
assert_eq!(escape_quotes("\u{0080}\u{00FF}"), "\u{0080}\u{00FF}");
assert_eq!(escape_quotes("\u{0100}\u{10FFFF}"), "\u{0100}\u{10FFFF}");
}
#[test]
fn escape_quotes_escapes_dquote_and_backslash() {
assert_eq!(escape_quotes("a\"b"), "a\\\"b");
assert_eq!(escape_quotes("a\\b"), "a\\\\b");
}
#[test]
fn escape_quotes_replaces_ctl_with_space() {
assert_eq!(escape_quotes("\r"), " ");
assert_eq!(escape_quotes("\n"), " ");
assert_eq!(escape_quotes("\0"), " ");
assert_eq!(escape_quotes("\x01"), " ");
assert_eq!(escape_quotes("\x1F"), " ");
assert_eq!(escape_quotes("\x7F"), " ");
assert_eq!(escape_quotes("\r\n\0"), " ");
assert_eq!(escape_quotes("\0\""), " \\\"");
assert_eq!(escape_quotes("\0\\"), " \\\\");
assert_eq!(escape_quotes("a\rb\nc"), "a b c");
}
}