use memchr::memchr;
use std::borrow::Cow;
use std::cmp::Ordering;
use std::collections::HashMap;
static COMMON_HEADERS: &[(&str, &str)] = &[
("accept", "Accept"),
("accept-charset", "Accept-Charset"),
("accept-encoding", "Accept-Encoding"),
("accept-language", "Accept-Language"),
("authorization", "Authorization"),
("cache-control", "Cache-Control"),
("connection", "Connection"),
("content-encoding", "Content-Encoding"),
("content-length", "Content-Length"),
("content-type", "Content-Type"),
("cookie", "Cookie"),
("date", "Date"),
("host", "Host"),
("if-match", "If-Match"),
("if-modified-since", "If-Modified-Since"),
("if-none-match", "If-None-Match"),
("if-unmodified-since", "If-Unmodified-Since"),
("origin", "Origin"),
("pragma", "Pragma"),
("range", "Range"),
("referer", "Referer"),
("sec-fetch-dest", "Sec-Fetch-Dest"),
("sec-fetch-mode", "Sec-Fetch-Mode"),
("sec-fetch-site", "Sec-Fetch-Site"),
("te", "TE"),
("transfer-encoding", "Transfer-Encoding"),
("upgrade", "Upgrade"),
("user-agent", "User-Agent"),
("x-forwarded-for", "X-Forwarded-For"),
("x-forwarded-host", "X-Forwarded-Host"),
("x-forwarded-proto", "X-Forwarded-Proto"),
("x-real-ip", "X-Real-IP"),
("x-request-id", "X-Request-Id"),
];
#[inline]
pub fn intern_header_name(name: &str) -> Cow<'static, str> {
let needle = name.as_bytes();
let found = COMMON_HEADERS.binary_search_by(|(key, _)| {
let key = key.as_bytes();
let common = key.len().min(needle.len());
for i in 0..common {
match key[i].cmp(&needle[i].to_ascii_lowercase()) {
Ordering::Equal => {}
non_eq => return non_eq,
}
}
key.len().cmp(&needle.len())
});
if let Ok(idx) = found {
Cow::Borrowed(COMMON_HEADERS[idx].1)
} else {
Cow::Owned(name.to_string())
}
}
#[inline]
pub fn parse_query_string_fast(query: &str) -> HashMap<String, String> {
let bytes = query.as_bytes();
let mut params = HashMap::with_capacity(8); let mut pos = 0;
while pos < bytes.len() {
let amp_pos = match memchr(b'&', &bytes[pos..]) {
Some(p) => pos + p,
None => bytes.len(),
};
let segment = &bytes[pos..amp_pos];
let segment_str = &query[pos..amp_pos];
if let Some(eq_offset) = memchr(b'=', segment) {
let key = &segment_str[..eq_offset];
let value = &segment_str[eq_offset + 1..];
if !key.is_empty() {
params.insert(key.to_string(), value.to_string());
}
} else {
if !segment_str.is_empty() {
params.insert(segment_str.to_string(), String::new());
}
}
pos = amp_pos + 1;
}
params
}
#[inline]
pub fn parse_query_string_decoded(query: &str) -> HashMap<String, String> {
let bytes = query.as_bytes();
let mut params = HashMap::with_capacity(8);
let mut pos = 0;
while pos < bytes.len() {
let amp_pos = match memchr(b'&', &bytes[pos..]) {
Some(p) => pos + p,
None => bytes.len(),
};
let segment = &bytes[pos..amp_pos];
let segment_str = &query[pos..amp_pos];
if let Some(eq_offset) = memchr(b'=', segment) {
let key = url_decode(&segment_str[..eq_offset]);
let value = url_decode(&segment_str[eq_offset + 1..]);
if !key.is_empty() {
params.insert(key, value);
}
} else {
let key = url_decode(segment_str);
if !key.is_empty() {
params.insert(key, String::new());
}
}
pos = amp_pos + 1;
}
params
}
#[inline]
pub fn url_decode(input: &str) -> String {
let bytes = input.as_bytes();
if memchr(b'%', bytes).is_none() && memchr(b'+', bytes).is_none() {
return input.to_string();
}
let mut result = Vec::with_capacity(input.len());
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'%' if i + 2 < bytes.len() => {
if let (Some(h1), Some(h2)) = (hex_digit(bytes[i + 1]), hex_digit(bytes[i + 2])) {
result.push(h1 << 4 | h2);
i += 3;
} else {
result.push(b'%');
i += 1;
}
}
b'+' => {
result.push(b' ');
i += 1;
}
c => {
result.push(c);
i += 1;
}
}
}
String::from_utf8_lossy(&result).into_owned()
}
#[inline(always)]
fn hex_digit(c: u8) -> Option<u8> {
match c {
b'0'..=b'9' => Some(c - b'0'),
b'A'..=b'F' => Some(c - b'A' + 10),
b'a'..=b'f' => Some(c - b'a' + 10),
_ => None,
}
}
#[inline]
pub fn split_path(path: &str) -> impl Iterator<Item = &str> {
path.split('/').filter(|s| !s.is_empty())
}
#[inline]
pub fn split_uri(uri: &str) -> (&str, Option<&str>) {
let bytes = uri.as_bytes();
if let Some(pos) = memchr(b'?', bytes) {
(&uri[..pos], Some(&uri[pos + 1..]))
} else {
(uri, None)
}
}
#[inline]
pub fn parse_headers(buf: &[u8]) -> Result<Vec<(&str, &str)>, httparse::Error> {
let mut headers = [httparse::EMPTY_HEADER; 64];
let mut req = httparse::Request::new(&mut headers);
match req.parse(buf)? {
httparse::Status::Complete(_) => {
let result = req
.headers
.iter()
.filter(|h| !h.name.is_empty())
.map(|h| (h.name, std::str::from_utf8(h.value).unwrap_or("")))
.collect();
Ok(result)
}
httparse::Status::Partial => {
let result = req
.headers
.iter()
.filter(|h| !h.name.is_empty())
.map(|h| (h.name, std::str::from_utf8(h.value).unwrap_or("")))
.collect();
Ok(result)
}
}
}
#[inline]
pub fn parse_request_line(buf: &[u8]) -> Result<(&str, &str, u8), httparse::Error> {
let mut headers = [httparse::EMPTY_HEADER; 0];
let mut req = httparse::Request::new(&mut headers);
match req.parse(buf)? {
httparse::Status::Complete(_) | httparse::Status::Partial => {
let method = req.method.unwrap_or("GET");
let path = req.path.unwrap_or("/");
let version = req.version.unwrap_or(1);
Ok((method, path, version))
}
}
}
#[inline]
pub fn is_valid_header_name(name: &[u8]) -> bool {
name.iter()
.all(|&c| c.is_ascii_alphanumeric() || c == b'-' || c == b'_')
}
#[inline]
pub fn extract_path_params(pattern: &str, path: &str) -> HashMap<String, String> {
let mut params = HashMap::with_capacity(4);
let pattern_segments: Vec<_> = split_path(pattern).collect();
let path_segments: Vec<_> = split_path(path).collect();
if pattern_segments.len() != path_segments.len() {
return params;
}
for (pat, seg) in pattern_segments.iter().zip(path_segments.iter()) {
if let Some(param_name) = pat.strip_prefix(':') {
params.insert(param_name.to_string(), (*seg).to_string());
}
}
params
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_intern_header_name() {
assert_eq!(intern_header_name("content-type"), "Content-Type");
assert_eq!(intern_header_name("Content-Type"), "Content-Type");
assert_eq!(intern_header_name("CONTENT-TYPE"), "Content-Type");
assert_eq!(intern_header_name("authorization"), "Authorization");
assert_eq!(intern_header_name("AcCePt"), "Accept");
assert_eq!(intern_header_name("X-FORWARDED-FOR"), "X-Forwarded-For");
assert_eq!(intern_header_name("te"), "TE");
assert_eq!(intern_header_name("TE"), "TE");
let custom = intern_header_name("X-Custom-Header");
assert_eq!(custom.as_ref(), "X-Custom-Header");
assert!(matches!(custom, Cow::Owned(_)));
assert_eq!(intern_header_name("content-typ").as_ref(), "content-typ");
assert_eq!(
intern_header_name("content-type-extra").as_ref(),
"content-type-extra"
);
assert!(matches!(intern_header_name("host"), Cow::Borrowed(_)));
}
#[test]
fn test_parse_query_string_fast() {
let params = parse_query_string_fast("name=john&age=30&city=NYC");
assert_eq!(params.get("name"), Some(&"john".to_string()));
assert_eq!(params.get("age"), Some(&"30".to_string()));
assert_eq!(params.get("city"), Some(&"NYC".to_string()));
let params = parse_query_string_fast("");
assert!(params.is_empty());
let params = parse_query_string_fast("key=value");
assert_eq!(params.get("key"), Some(&"value".to_string()));
let params = parse_query_string_fast("flag&debug=true");
assert!(params.contains_key("flag"));
assert_eq!(params.get("debug"), Some(&"true".to_string()));
}
#[test]
fn test_parse_query_string_decoded() {
let params = parse_query_string_decoded("name=john%20doe&age=30");
assert_eq!(params.get("name"), Some(&"john doe".to_string()));
assert_eq!(params.get("age"), Some(&"30".to_string()));
let params = parse_query_string_decoded("name=john+doe");
assert_eq!(params.get("name"), Some(&"john doe".to_string()));
let params = parse_query_string_decoded("email=test%40example.com");
assert_eq!(params.get("email"), Some(&"test@example.com".to_string()));
}
#[test]
fn test_url_decode() {
assert_eq!(url_decode("hello%20world"), "hello world");
assert_eq!(url_decode("hello+world"), "hello world");
assert_eq!(url_decode("test%40example.com"), "test@example.com");
assert_eq!(url_decode("normal"), "normal");
assert_eq!(url_decode("%2F"), "/");
assert_eq!(url_decode("%3A%2F%2F"), "://");
}
#[test]
fn test_url_decode_multibyte_utf8() {
assert_eq!(url_decode("%E2%82%AC"), "\u{20AC}"); assert_eq!(url_decode("caf%C3%A9"), "caf\u{e9}"); assert_eq!(url_decode("%F0%9F%A6%80"), "\u{1F980}");
let params = parse_query_string_decoded("price=%E2%82%AC10&name=caf%C3%A9");
assert_eq!(params.get("price"), Some(&"\u{20AC}10".to_string()));
assert_eq!(params.get("name"), Some(&"caf\u{e9}".to_string()));
}
#[test]
fn test_split_uri() {
let (path, query) = split_uri("/api/users?page=1&limit=10");
assert_eq!(path, "/api/users");
assert_eq!(query, Some("page=1&limit=10"));
let (path, query) = split_uri("/api/users");
assert_eq!(path, "/api/users");
assert_eq!(query, None);
}
#[test]
fn test_split_path() {
let segments: Vec<_> = split_path("/api/v1/users/123").collect();
assert_eq!(segments, vec!["api", "v1", "users", "123"]);
let segments: Vec<_> = split_path("/").collect();
assert!(segments.is_empty());
let segments: Vec<_> = split_path("/api//users/").collect();
assert_eq!(segments, vec!["api", "users"]);
}
#[test]
fn test_extract_path_params() {
let params = extract_path_params("/users/:id/posts/:post_id", "/users/123/posts/456");
assert_eq!(params.get("id"), Some(&"123".to_string()));
assert_eq!(params.get("post_id"), Some(&"456".to_string()));
let params = extract_path_params("/users/list", "/users/list");
assert!(params.is_empty());
}
#[test]
fn test_is_valid_header_name() {
assert!(is_valid_header_name(b"Content-Type"));
assert!(is_valid_header_name(b"X-Request-Id"));
assert!(is_valid_header_name(b"X_Custom_Header"));
assert!(!is_valid_header_name(b"Header: Invalid"));
assert!(!is_valid_header_name(b"Header\n"));
}
#[test]
fn test_parse_request_line() {
let result = parse_request_line(b"GET /api/users HTTP/1.1\r\n\r\n");
assert!(result.is_ok());
let (method, path, version) = result.unwrap();
assert_eq!(method, "GET");
assert_eq!(path, "/api/users");
assert_eq!(version, 1);
}
}