use crate::Error;
use serde::de::DeserializeOwned;
use std::collections::HashMap;
pub fn parse_form<T: DeserializeOwned>(body: &[u8]) -> Result<T, Error> {
serde_urlencoded::from_bytes(body)
.map_err(|e| Error::BadRequest(format!("Failed to parse form data: {}", e)))
}
pub fn parse_form_map(body: &[u8]) -> Result<HashMap<String, String>, Error> {
let form_data: Vec<(String, String)> = serde_urlencoded::from_bytes(body)
.map_err(|e| Error::BadRequest(format!("Failed to parse form data: {}", e)))?;
Ok(form_data.into_iter().collect())
}
#[derive(Debug, Clone)]
pub struct FormField {
pub name: String,
pub value: Option<String>,
pub file: Option<FormFile>,
}
#[derive(Debug, Clone)]
pub struct FormFile {
pub filename: String,
pub content_type: String,
pub size: usize,
pub data: Vec<u8>,
}
impl FormFile {
pub fn new(filename: String, content_type: String, data: Vec<u8>) -> Self {
let size = data.len();
Self {
filename,
content_type,
size,
data,
}
}
pub fn extension(&self) -> Option<&str> {
self.filename.rsplit('.').next()
}
pub fn is_image(&self) -> bool {
self.content_type.starts_with("image/")
}
pub fn exceeds_size(&self, max_bytes: usize) -> bool {
self.size > max_bytes
}
pub fn save_to(&self, path: &str) -> Result<(), Error> {
std::fs::write(path, &self.data)
.map_err(|e| Error::Internal(format!("Failed to save file: {}", e)))
}
pub async fn save_to_async(&self, path: &str) -> Result<(), Error> {
tokio::fs::write(path, &self.data)
.await
.map_err(|e| Error::Internal(format!("Failed to save file: {}", e)))
}
}
pub async fn save_files_parallel(files: Vec<(&FormFile, String)>) -> Result<Vec<String>, Error> {
use tokio::task::JoinSet;
let mut set = JoinSet::new();
for (file, path) in files {
let data = file.data.clone();
let path_clone = path.clone();
set.spawn(async move {
tokio::fs::write(&path_clone, &data)
.await
.map_err(|e| Error::Internal(format!("Failed to save file: {}", e)))?;
Ok::<_, Error>(path_clone)
});
}
let mut saved_paths = Vec::new();
while let Some(result) = set.join_next().await {
saved_paths.push(result.map_err(|e| Error::Internal(e.to_string()))??);
}
Ok(saved_paths)
}
pub struct MultipartParser {
boundary: String,
}
impl MultipartParser {
pub fn from_content_type(content_type: &str) -> Result<Self, Error> {
let boundary = content_type
.split(';')
.find_map(|part| {
let part = part.trim();
if part.starts_with("boundary=") {
Some(
part.trim_start_matches("boundary=")
.trim_matches('"')
.to_string(),
)
} else {
None
}
})
.ok_or_else(|| Error::BadRequest("Missing boundary in Content-Type".to_string()))?;
Ok(Self { boundary })
}
pub fn parse(&self, body: &[u8]) -> Result<Vec<FormField>, Error> {
let mut fields = Vec::new();
let boundary_marker = format!("--{}", self.boundary);
let body_str = String::from_utf8_lossy(body);
let parts: Vec<&str> = body_str.split(&boundary_marker).collect();
for part in parts.iter().skip(1) {
if part.trim() == "--" || part.trim().is_empty() {
continue;
}
if let Some(field) = self.parse_part(part)? {
fields.push(field);
}
}
Ok(fields)
}
fn parse_part(&self, part: &str) -> Result<Option<FormField>, Error> {
let lines: Vec<&str> = part.lines().collect();
if lines.is_empty() {
return Ok(None);
}
let mut name = None;
let mut filename = None;
let mut content_type = None;
let mut content_start = 0;
for (i, line) in lines.iter().enumerate() {
if line.trim().is_empty() {
content_start = i + 1;
break;
}
if line.starts_with("Content-Disposition:") {
for attr in line.split(';') {
let attr = attr.trim();
if attr.starts_with("name=") {
name = Some(
attr.trim_start_matches("name=")
.trim_matches('"')
.to_string(),
);
} else if attr.starts_with("filename=") {
filename = Some(
attr.trim_start_matches("filename=")
.trim_matches('"')
.to_string(),
);
}
}
} else if line.starts_with("Content-Type:") {
content_type = Some(line.trim_start_matches("Content-Type:").trim().to_string());
}
}
let name = name.ok_or_else(|| Error::BadRequest("Missing field name".to_string()))?;
let content_lines = &lines[content_start..];
let content = content_lines.join("\n").trim().to_string();
if let Some(filename) = filename {
let file = FormFile::new(
filename,
content_type.unwrap_or_else(|| "application/octet-stream".to_string()),
content.into_bytes(),
);
Ok(Some(FormField {
name,
value: None,
file: Some(file),
}))
} else {
Ok(Some(FormField {
name,
value: Some(content),
file: None,
}))
}
}
pub fn to_map(fields: Vec<FormField>) -> HashMap<String, String> {
fields
.into_iter()
.filter_map(|field| field.value.map(|value| (field.name, value)))
.collect()
}
pub fn get_files(fields: &[FormField]) -> Vec<(String, &FormFile)> {
fields
.iter()
.filter_map(|field| field.file.as_ref().map(|file| (field.name.clone(), file)))
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_form_map() {
let body = b"name=John+Doe&email=john%40example.com&age=30";
let form = parse_form_map(body).unwrap();
assert_eq!(form.get("name"), Some(&"John Doe".to_string()));
assert_eq!(form.get("email"), Some(&"john@example.com".to_string()));
assert_eq!(form.get("age"), Some(&"30".to_string()));
}
#[test]
fn test_form_file_extension() {
let file = FormFile::new(
"document.pdf".to_string(),
"application/pdf".to_string(),
vec![1, 2, 3],
);
assert_eq!(file.extension(), Some("pdf"));
}
#[test]
fn test_form_file_is_image() {
let image = FormFile::new("photo.jpg".to_string(), "image/jpeg".to_string(), vec![]);
assert!(image.is_image());
let doc = FormFile::new("doc.pdf".to_string(), "application/pdf".to_string(), vec![]);
assert!(!doc.is_image());
}
#[test]
fn test_form_file_size_check() {
let file = FormFile::new(
"file.txt".to_string(),
"text/plain".to_string(),
vec![0; 1024], );
assert!(!file.exceeds_size(2048)); assert!(file.exceeds_size(512)); }
#[test]
fn test_multipart_parser_from_content_type() {
let content_type = "multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW";
let parser = MultipartParser::from_content_type(content_type).unwrap();
assert_eq!(parser.boundary, "----WebKitFormBoundary7MA4YWxkTrZu0gW");
}
#[test]
fn test_multipart_parser_missing_boundary() {
let content_type = "multipart/form-data";
let result = MultipartParser::from_content_type(content_type);
assert!(result.is_err());
}
}