use super::collection::{Collection, HashMapCollection};
#[derive(Debug, Clone, Default)]
pub struct RequestData {
pub method: String,
pub uri: String,
pub uri_raw: String,
pub path: String,
pub query_string: String,
pub protocol: String,
pub headers: HashMapCollection,
pub args_get: HashMapCollection,
pub args_post: HashMapCollection,
pub cookies: HashMapCollection,
pub multipart_part_headers: HashMapCollection,
pub files: HashMapCollection,
pub body_processor: String,
pub body: Vec<u8>,
pub client_ip: String,
pub client_port: u16,
pub server_name: String,
pub server_port: u16,
}
impl RequestData {
pub fn new() -> Self {
Self::default()
}
pub fn set_uri(&mut self, uri: &str) {
self.uri = uri.to_string();
self.uri_raw = uri.to_string();
if let Some(pos) = uri.find('?') {
self.path = uri[..pos].to_string();
self.query_string = uri[pos + 1..].to_string();
self.parse_query_string(&self.query_string.clone());
} else {
self.path = uri.to_string();
self.query_string.clear();
}
}
pub fn set_method(&mut self, method: &str) {
self.method = method.to_string();
}
pub fn set_protocol(&mut self, protocol: &str) {
self.protocol = protocol.to_string();
}
pub fn add_header(&mut self, name: &str, value: &str) {
let lower = name.to_lowercase();
if lower == "cookie" {
self.parse_cookie_header(value);
}
self.headers.add(lower, value.to_string());
}
fn parse_cookie_header(&mut self, value: &str) {
for pair in value.split(';') {
let pair = pair.trim();
if pair.is_empty() {
continue;
}
match pair.split_once('=') {
Some((k, v)) => self.cookies.add(k.trim().to_string(), v.trim().to_string()),
None => self.cookies.add(pair.to_string(), String::new()),
}
}
}
pub fn append_body(&mut self, data: &[u8]) {
self.body.extend_from_slice(data);
}
pub fn body_str(&self) -> String {
String::from_utf8_lossy(&self.body).to_string()
}
pub fn body_length(&self) -> usize {
self.body.len()
}
fn parse_query_string(&mut self, qs: &str) {
for pair in qs.split('&') {
if let Some(pos) = pair.find('=') {
let key = &pair[..pos];
let value = &pair[pos + 1..];
let key = percent_encoding::percent_decode_str(key)
.decode_utf8_lossy()
.to_string();
let value = percent_encoding::percent_decode_str(value)
.decode_utf8_lossy()
.to_string();
self.args_get.add(key, value);
} else if !pair.is_empty() {
let key = percent_encoding::percent_decode_str(pair)
.decode_utf8_lossy()
.to_string();
self.args_get.add(key, String::new());
}
}
}
pub fn parse_multipart_body(&mut self, content_type: &str) -> bool {
let Some(boundary) = extract_multipart_boundary(content_type) else {
return false;
};
let body = self.body_str();
let delimiter = format!("--{boundary}");
let mut sections = body.split(delimiter.as_str());
let _preamble = sections.next();
for section in sections {
if section.starts_with("--") {
break;
}
let section = section
.strip_prefix("\r\n")
.or_else(|| section.strip_prefix("\n"))
.unwrap_or(section);
let (head, content) = match section.split_once("\r\n\r\n") {
Some(split) => split,
None => match section.split_once("\n\n") {
Some(split) => split,
None => (section, ""),
},
};
let content = content
.strip_suffix("\r\n")
.or_else(|| content.strip_suffix("\n"))
.unwrap_or(content);
let mut part_name: Option<String> = None;
let mut filename: Option<String> = None;
let mut header_lines: Vec<String> = Vec::new();
for line in head.lines() {
let line = line.trim_end_matches('\r');
if line.is_empty() {
continue;
}
header_lines.push(line.to_string());
if let Some((hname, hval)) = line.split_once(':') {
if hname.trim().eq_ignore_ascii_case("content-disposition") {
part_name = extract_disposition_param(hval, "name");
filename = extract_disposition_param(hval, "filename");
}
}
}
let Some(name) = part_name else {
continue;
};
for line in header_lines {
self.multipart_part_headers.add(name.clone(), line);
}
if let Some(fname) = filename {
self.files.add(name, fname);
} else {
self.args_post.add(name, content.to_string());
}
}
self.body_processor = "MULTIPART".to_string();
true
}
pub fn parse_form_body(&mut self) {
let body_str = self.body_str();
for pair in body_str.split('&') {
if let Some(pos) = pair.find('=') {
let key = &pair[..pos];
let value = &pair[pos + 1..];
let key = percent_encoding::percent_decode_str(key)
.decode_utf8_lossy()
.to_string();
let value = percent_encoding::percent_decode_str(value)
.decode_utf8_lossy()
.to_string();
self.args_post.add(key, value);
}
}
}
pub fn all_args(&self) -> Vec<(&str, &str)> {
let mut all = self.args_get.all();
all.extend(self.args_post.all());
all
}
}
fn extract_multipart_boundary(content_type: &str) -> Option<String> {
for param in content_type.split(';').skip(1) {
let param = param.trim();
if let Some((key, value)) = param.split_once('=') {
if key.trim().eq_ignore_ascii_case("boundary") {
let value = value.trim().trim_matches('"');
if !value.is_empty() {
return Some(value.to_string());
}
}
}
}
None
}
fn extract_disposition_param(header_value: &str, param: &str) -> Option<String> {
for part in header_value.split(';') {
let part = part.trim();
if let Some((key, value)) = part.split_once('=') {
if key.trim().eq_ignore_ascii_case(param) {
return Some(value.trim().trim_matches('"').to_string());
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_cookie_header() {
let mut req = RequestData::new();
req.add_header("Cookie", "session=abc123; theme=dark; bare");
assert_eq!(req.cookies.get("session"), Some(vec!["abc123"]));
assert_eq!(req.cookies.get("theme"), Some(vec!["dark"]));
assert_eq!(req.cookies.get("bare"), Some(vec![""]));
}
#[test]
fn test_parse_multipart_body_fields_and_headers() {
let mut req = RequestData::new();
let body = "--XyZ\r\n\
Content-Disposition: form-data; name=\"field1\"\r\n\
\r\n\
value1\r\n\
--XyZ\r\n\
Content-Disposition: form-data; name=\"upload\"; filename=\"a.txt\"\r\n\
Content-Type: text/plain\r\n\
\r\n\
file contents\r\n\
--XyZ--\r\n";
req.append_body(body.as_bytes());
assert!(req.parse_multipart_body("multipart/form-data; boundary=XyZ"));
assert_eq!(req.args_post.get("field1"), Some(vec!["value1"]));
assert_eq!(req.args_post.get("upload"), None);
assert_eq!(req.files.get("upload"), Some(vec!["a.txt"]));
let field1_headers = req.multipart_part_headers.get("field1").unwrap();
assert_eq!(field1_headers, vec!["Content-Disposition: form-data; name=\"field1\""]);
let upload_headers = req.multipart_part_headers.get("upload").unwrap();
assert!(upload_headers.contains(&"Content-Type: text/plain"));
assert_eq!(req.body_processor, "MULTIPART");
}
#[test]
fn test_parse_multipart_body_quoted_boundary_and_lf_only() {
let mut req = RequestData::new();
let body = "--b1\nContent-Disposition: form-data; name=\"k\"\n\nv\n--b1--\n";
req.append_body(body.as_bytes());
assert!(req.parse_multipart_body("multipart/form-data; boundary=\"b1\""));
assert_eq!(req.args_post.get("k"), Some(vec!["v"]));
}
#[test]
fn test_parse_multipart_body_missing_boundary() {
let mut req = RequestData::new();
req.append_body(b"irrelevant");
assert!(!req.parse_multipart_body("multipart/form-data"));
assert!(req.body_processor.is_empty());
}
}