use super::Request;
use hyper::Uri;
use percent_encoding::percent_decode_str;
use std::collections::HashMap;
impl Request {
pub(super) fn parse_query_params(uri: &Uri) -> HashMap<String, String> {
uri.query()
.map(|q| {
q.split('&')
.filter_map(|pair| {
let mut parts = pair.splitn(2, '=');
Some((
parts.next()?.to_string(),
parts.next().unwrap_or("").to_string(),
))
})
.collect()
})
.unwrap_or_default()
}
pub fn path(&self) -> &str {
self.uri.path()
}
pub fn decoded_query_params(&self) -> HashMap<String, String> {
self.query_params
.iter()
.map(|(k, v)| {
let decoded_key = percent_decode_str(k).decode_utf8_lossy().to_string();
let decoded_value = percent_decode_str(v).decode_utf8_lossy().to_string();
(decoded_key, decoded_value)
})
.collect()
}
pub fn set_path_param(&mut self, key: impl Into<String>, value: impl Into<String>) {
self.path_params.insert(key, value);
}
pub fn get_accepted_languages(&self) -> Vec<(String, f32)> {
use hyper::header::ACCEPT_LANGUAGE;
self.headers
.get(ACCEPT_LANGUAGE)
.and_then(|h| h.to_str().ok())
.map(Self::parse_accept_language)
.unwrap_or_default()
}
pub fn get_preferred_language(&self) -> Option<String> {
self.get_accepted_languages()
.into_iter()
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
.map(|(lang, _)| lang)
}
fn parse_accept_language(header: &str) -> Vec<(String, f32)> {
let mut languages: Vec<(String, f32)> = header
.split(',')
.filter_map(|lang_part| {
let lang_part = lang_part.trim();
if lang_part.is_empty() {
return None;
}
let parts: Vec<&str> = lang_part.split(';').collect();
let language = parts[0].trim().to_string();
let quality = if parts.len() > 1 {
parts[1]
.trim()
.strip_prefix("q=")
.and_then(|q| q.parse::<f32>().ok())
.unwrap_or(1.0)
} else {
1.0
};
if Self::is_valid_language_code(&language) {
Some((language, quality))
} else {
None
}
})
.collect();
languages.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
languages
}
fn is_valid_language_code(code: &str) -> bool {
if code.is_empty() || code.len() > 255 {
return false;
}
if code.starts_with('-') || code.ends_with('-') {
return false;
}
code.chars().all(|c| c.is_alphanumeric() || c == '-')
}
pub fn get_language_from_cookie(&self, cookie_name: &str) -> Option<String> {
use hyper::header::COOKIE;
self.headers
.get(COOKIE)
.and_then(|h| h.to_str().ok())
.and_then(Self::parse_cookies)
.and_then(|parsed| {
parsed.into_iter().find_map(|(name, value)| {
if name == cookie_name {
Some(value)
} else {
None
}
})
})
.filter(|lang| Self::is_valid_language_code(lang))
}
fn parse_cookies(header: &str) -> Option<Vec<(String, String)>> {
let mut cookies = Vec::new();
for cookie in header.split(';') {
let cookie = cookie.trim();
if cookie.is_empty() {
continue;
}
let mut parts = cookie.splitn(2, '=');
let name = parts.next()?.trim();
let value = match parts.next() {
Some(v) => v.trim(),
None => continue,
};
if name.is_empty() || !Self::is_valid_cookie_name(name) {
continue;
}
cookies.push((name.to_string(), value.to_string()));
}
Some(cookies)
}
fn is_valid_cookie_name(name: &str) -> bool {
name.chars().all(|c| {
let code = c as u32;
(0x21..=0x7E).contains(&code)
&& !matches!(
c,
'(' | ')'
| '<' | '>' | '@' | ','
| ';' | ':' | '\\' | '"'
| '/' | '[' | ']' | '?'
| '=' | '{' | '}' | ' '
| '\t'
)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
#[rstest]
fn test_parse_query_params_preserves_equals_in_value() {
let uri: hyper::Uri = "/test?token=abc==".parse().unwrap();
let params = Request::parse_query_params(&uri);
assert_eq!(params.get("token"), Some(&"abc==".to_string()));
}
#[rstest]
fn test_parse_query_params_base64_encoded_value() {
let uri: hyper::Uri = "/test?data=dGVzdA==".parse().unwrap();
let params = Request::parse_query_params(&uri);
assert_eq!(params.get("data"), Some(&"dGVzdA==".to_string()));
}
#[rstest]
fn test_parse_query_params_multiple_equals_in_value() {
let uri: hyper::Uri = "/test?formula=a=b=c".parse().unwrap();
let params = Request::parse_query_params(&uri);
assert_eq!(params.get("formula"), Some(&"a=b=c".to_string()));
}
#[rstest]
fn test_parse_query_params_simple_key_value() {
let uri: hyper::Uri = "/test?key=value".parse().unwrap();
let params = Request::parse_query_params(&uri);
assert_eq!(params.get("key"), Some(&"value".to_string()));
}
#[rstest]
fn test_parse_query_params_key_without_value() {
let uri: hyper::Uri = "/test?key=".parse().unwrap();
let params = Request::parse_query_params(&uri);
assert_eq!(params.get("key"), Some(&"".to_string()));
}
#[rstest]
fn test_parse_query_params_no_query_string() {
let uri: hyper::Uri = "/test".parse().unwrap();
let params = Request::parse_query_params(&uri);
assert!(params.is_empty());
}
#[rstest]
fn test_parse_query_params_multiple_params_with_equals() {
let uri: hyper::Uri = "/test?a=1&b=x=y=z&c=3".parse().unwrap();
let params = Request::parse_query_params(&uri);
assert_eq!(params.get("a"), Some(&"1".to_string()));
assert_eq!(params.get("b"), Some(&"x=y=z".to_string()));
assert_eq!(params.get("c"), Some(&"3".to_string()));
}
}