use crate::core::req_body::ReqBody;
use crate::header::{CONTENT_TYPE, HeaderMap};
use crate::multer::{Field, Multipart};
use crate::{SilentError, StatusCode};
use async_fs::File;
use futures::io::AsyncWriteExt;
use multimap::MultiMap;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use tempfile::Builder;
use textnonce::TextNonce;
#[derive(Debug)]
pub struct FormData {
pub fields: MultiMap<String, String>,
#[cfg(feature = "server")]
pub files: MultiMap<String, FilePart>,
}
impl FormData {
#[inline]
pub fn new() -> FormData {
FormData {
fields: MultiMap::new(),
#[cfg(feature = "server")]
files: MultiMap::new(),
}
}
pub(crate) async fn read(headers: &HeaderMap, body: ReqBody) -> Result<FormData, SilentError> {
let mut form_data = FormData::new();
if let Some(boundary) = headers
.get(CONTENT_TYPE)
.and_then(|ct| ct.to_str().ok())
.and_then(|ct| multer::parse_boundary(ct).ok())
{
let mut multipart = Multipart::new(body, boundary);
while let Some(mut field) = multipart.next_field().await? {
if let Some(name) = field.name().map(|s| s.to_owned()) {
if field.headers().get(CONTENT_TYPE).is_some() {
form_data
.files
.insert(name, FilePart::create(&mut field).await?);
} else {
form_data.fields.insert(name, field.text().await?);
}
}
}
}
Ok(form_data)
}
}
impl Default for FormData {
#[inline]
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Debug)]
pub struct FilePart {
name: Option<String>,
headers: HeaderMap,
path: PathBuf,
size: u64,
temp_dir: Option<PathBuf>,
}
impl FilePart {
#[inline]
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
#[inline]
pub fn name_mut(&mut self) -> Option<&mut String> {
self.name.as_mut()
}
#[inline]
pub fn headers(&self) -> &HeaderMap {
&self.headers
}
pub fn headers_mut(&mut self) -> &mut HeaderMap {
&mut self.headers
}
#[inline]
pub fn path(&self) -> &PathBuf {
&self.path
}
#[inline]
pub fn size(&self) -> u64 {
self.size
}
#[inline]
pub fn do_not_delete_on_drop(&mut self) {
self.temp_dir = None;
}
#[inline]
pub fn save(&self, path: String) -> Result<u64, SilentError> {
std::fs::copy(self.path(), Path::new(&path)).map_err(|e| SilentError::BusinessError {
code: StatusCode::INTERNAL_SERVER_ERROR,
msg: format!("Failed to save file: {e}"),
})
}
#[inline]
pub async fn create(field: &mut Field<'_>) -> Result<FilePart, SilentError> {
let mut path = Builder::new()
.prefix("silent_http_multipart")
.tempdir()?
.keep();
let temp_dir = Some(path.clone());
let name = field.file_name().map(|s| s.to_owned());
path.push(format!(
"{}.{}",
TextNonce::sized_urlsafe(32)?.into_string(),
name.as_deref()
.and_then(|name| { Path::new(name).extension().and_then(OsStr::to_str) })
.unwrap_or("unknown")
));
let mut file = File::create(&path).await?;
let mut size = 0;
while let Some(chunk) = field.chunk().await? {
size += chunk.len() as u64;
file.write_all(&chunk).await?;
}
Ok(FilePart {
name,
headers: field.headers().to_owned(),
path,
size,
temp_dir,
})
}
}
impl Drop for FilePart {
fn drop(&mut self) {
if let Some(temp_dir) = &self.temp_dir {
let path = self.path.clone();
let temp_dir = temp_dir.to_owned();
std::thread::spawn(move || {
let _ = std::fs::remove_file(&path);
let _ = std::fs::remove_dir(temp_dir);
});
}
}
}
#[cfg(all(test, feature = "server", feature = "multipart"))]
mod tests {
use super::*;
use crate::header::{HeaderMap, HeaderValue};
use bytes::Bytes;
#[test]
fn test_form_data_new() {
let form_data = FormData::new();
assert_eq!(form_data.fields.len(), 0);
assert_eq!(form_data.files.len(), 0);
}
#[test]
fn test_form_data_default() {
let form_data = FormData::default();
assert_eq!(form_data.fields.len(), 0);
assert_eq!(form_data.files.len(), 0);
}
#[tokio::test]
async fn test_form_data_read_no_content_type() {
let headers = HeaderMap::new();
let body = ReqBody::Once(Bytes::from("test data"));
let result = FormData::read(&headers, body).await;
assert!(result.is_ok());
let form_data = result.unwrap();
assert_eq!(form_data.fields.len(), 0);
assert_eq!(form_data.files.len(), 0);
}
#[tokio::test]
async fn test_form_data_read_empty_body() {
let mut headers = HeaderMap::new();
headers.insert(
CONTENT_TYPE,
HeaderValue::from_static("multipart/form-data; boundary=----WebKitFormBoundary"),
);
let body = ReqBody::Empty;
let result = FormData::read(&headers, body).await;
let _ = result;
}
#[tokio::test]
async fn test_form_data_read_invalid_content_type() {
let mut headers = HeaderMap::new();
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
let body = ReqBody::Once(Bytes::from("test data"));
let result = FormData::read(&headers, body).await;
assert!(result.is_ok());
}
#[test]
fn test_file_part_name() {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = temp_dir.path().join("test_file.txt");
std::fs::write(&file_path, b"test content").unwrap();
let file_part = FilePart {
name: Some("test_file.txt".to_string()),
headers: HeaderMap::new(),
path: file_path.clone(),
size: 12,
temp_dir: Some(temp_dir.path().to_path_buf()),
};
assert_eq!(file_part.name(), Some("test_file.txt"));
}
#[test]
fn test_file_part_name_none() {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = temp_dir.path().join("test_file.txt");
std::fs::write(&file_path, b"test content").unwrap();
let file_part = FilePart {
name: None,
headers: HeaderMap::new(),
path: file_path,
size: 12,
temp_dir: Some(temp_dir.path().to_path_buf()),
};
assert_eq!(file_part.name(), None);
}
#[test]
fn test_file_part_name_mut() {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = temp_dir.path().join("test_file.txt");
std::fs::write(&file_path, b"test content").unwrap();
let mut file_part = FilePart {
name: Some("old_name.txt".to_string()),
headers: HeaderMap::new(),
path: file_path,
size: 12,
temp_dir: Some(temp_dir.path().to_path_buf()),
};
if let Some(name) = file_part.name_mut() {
*name = "new_name.txt".to_string();
}
assert_eq!(file_part.name(), Some("new_name.txt"));
}
#[test]
fn test_file_part_headers() {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = temp_dir.path().join("test_file.txt");
std::fs::write(&file_path, b"test content").unwrap();
let mut headers = HeaderMap::new();
headers.insert("content-type", HeaderValue::from_static("text/plain"));
let file_part = FilePart {
name: Some("test_file.txt".to_string()),
headers: headers.clone(),
path: file_path,
size: 12,
temp_dir: Some(temp_dir.path().to_path_buf()),
};
assert_eq!(
file_part.headers().get("content-type").unwrap(),
"text/plain"
);
}
#[test]
fn test_file_part_headers_mut() {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = temp_dir.path().join("test_file.txt");
std::fs::write(&file_path, b"test content").unwrap();
let mut headers = HeaderMap::new();
headers.insert("content-type", HeaderValue::from_static("text/plain"));
let mut file_part = FilePart {
name: Some("test_file.txt".to_string()),
headers,
path: file_path,
size: 12,
temp_dir: Some(temp_dir.path().to_path_buf()),
};
file_part
.headers_mut()
.insert("content-type", HeaderValue::from_static("application/json"));
assert_eq!(
file_part.headers().get("content-type").unwrap(),
"application/json"
);
}
#[test]
fn test_file_part_path() {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = temp_dir.path().join("test_file.txt");
std::fs::write(&file_path, b"test content").unwrap();
let file_part = FilePart {
name: Some("test_file.txt".to_string()),
headers: HeaderMap::new(),
path: file_path.clone(),
size: 12,
temp_dir: Some(temp_dir.path().to_path_buf()),
};
assert_eq!(file_part.path(), &file_path);
}
#[test]
fn test_file_part_size() {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = temp_dir.path().join("test_file.txt");
std::fs::write(&file_path, b"test content").unwrap();
let file_part = FilePart {
name: Some("test_file.txt".to_string()),
headers: HeaderMap::new(),
path: file_path,
size: 1024,
temp_dir: Some(temp_dir.path().to_path_buf()),
};
assert_eq!(file_part.size(), 1024);
}
#[test]
fn test_file_part_save() {
let temp_dir = tempfile::tempdir().unwrap();
let source_dir = tempfile::tempdir().unwrap();
let source_path = source_dir.path().join("source.txt");
std::fs::write(&source_path, b"test content").unwrap();
let file_part = FilePart {
name: Some("source.txt".to_string()),
headers: HeaderMap::new(),
path: source_path.clone(),
size: 12,
temp_dir: Some(source_dir.path().to_path_buf()),
};
let dest_path = temp_dir.path().join("dest.txt");
let result = file_part.save(dest_path.to_str().unwrap().to_string());
assert!(result.is_ok());
assert!(dest_path.exists());
assert_eq!(std::fs::read_to_string(&dest_path).unwrap(), "test content");
}
#[test]
fn test_file_part_save_invalid_path() {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = temp_dir.path().join("test_file.txt");
std::fs::write(&file_path, b"test content").unwrap();
let file_part = FilePart {
name: Some("test_file.txt".to_string()),
headers: HeaderMap::new(),
path: file_path,
size: 12,
temp_dir: Some(temp_dir.path().to_path_buf()),
};
let result = file_part.save("/nonexistent/directory/file.txt".to_string());
assert!(result.is_err());
}
#[test]
fn test_file_part_do_not_delete_on_drop() {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = temp_dir.path().join("test_file.txt");
std::fs::write(&file_path, b"test content").unwrap();
let mut file_part = FilePart {
name: Some("test_file.txt".to_string()),
headers: HeaderMap::new(),
path: file_path.clone(),
size: 12,
temp_dir: Some(temp_dir.path().to_path_buf()),
};
file_part.do_not_delete_on_drop();
assert!(file_part.temp_dir.is_none());
}
#[test]
fn test_file_part_size_and_alignment() {
let size = std::mem::size_of::<FilePart>();
let align = std::mem::align_of::<FilePart>();
assert!(size > 0);
assert!(align >= std::mem::align_of::<usize>());
}
#[test]
fn test_file_part_clone() {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = temp_dir.path().join("test_file.txt");
std::fs::write(&file_path, b"test content").unwrap();
let file_part = FilePart {
name: Some("test_file.txt".to_string()),
headers: HeaderMap::new(),
path: file_path.clone(),
size: 12,
temp_dir: Some(temp_dir.path().to_path_buf()),
};
let cloned = file_part.clone();
assert_eq!(cloned.name(), file_part.name());
assert_eq!(cloned.path(), file_part.path());
assert_eq!(cloned.size(), file_part.size());
}
#[test]
fn test_form_data_fields_multimap() {
let mut form_data = FormData::new();
form_data
.fields
.insert("username".to_string(), "alice".to_string());
form_data
.fields
.insert("username".to_string(), "bob".to_string());
let values = form_data.fields.get_vec("username").unwrap();
assert_eq!(values.len(), 2);
assert!(values.contains(&"alice".to_string()));
assert!(values.contains(&"bob".to_string()));
}
#[test]
fn test_form_data_files_multimap() {
let temp_dir = tempfile::tempdir().unwrap();
let file_path1 = temp_dir.path().join("file1.txt");
let file_path2 = temp_dir.path().join("file2.txt");
std::fs::write(&file_path1, b"content1").unwrap();
std::fs::write(&file_path2, b"content2").unwrap();
let mut form_data = FormData::new();
form_data.files.insert(
"files".to_string(),
FilePart {
name: Some("file1.txt".to_string()),
headers: HeaderMap::new(),
path: file_path1,
size: 8,
temp_dir: Some(temp_dir.path().to_path_buf()),
},
);
form_data.files.insert(
"files".to_string(),
FilePart {
name: Some("file2.txt".to_string()),
headers: HeaderMap::new(),
path: file_path2,
size: 8,
temp_dir: Some(temp_dir.path().to_path_buf()),
},
);
let files = form_data.files.get_vec("files").unwrap();
assert_eq!(files.len(), 2);
}
#[tokio::test]
async fn test_form_data_read_malformed_boundary() {
let mut headers = HeaderMap::new();
headers.insert(
CONTENT_TYPE,
HeaderValue::from_static("multipart/form-data; boundary=unterminated"),
);
let body = ReqBody::Once(Bytes::from("------WebKitFormBoundary\r\n"));
let result = FormData::read(&headers, body).await;
assert!(result.is_ok() || result.is_err());
}
#[test]
fn test_file_part_zero_size() {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = temp_dir.path().join("empty_file.txt");
std::fs::write(&file_path, b"").unwrap();
let file_part = FilePart {
name: Some("empty_file.txt".to_string()),
headers: HeaderMap::new(),
path: file_path,
size: 0,
temp_dir: Some(temp_dir.path().to_path_buf()),
};
assert_eq!(file_part.size(), 0);
assert!(file_part.path().exists());
}
#[test]
fn test_file_part_large_size() {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = temp_dir.path().join("large_file.bin");
let large_size = u64::MAX / 2;
let file_part = FilePart {
name: Some("large_file.bin".to_string()),
headers: HeaderMap::new(),
path: file_path,
size: large_size,
temp_dir: Some(temp_dir.path().to_path_buf()),
};
assert_eq!(file_part.size(), large_size);
}
#[test]
fn test_form_data_debug() {
let form_data = FormData::new();
let debug_str = format!("{:?}", form_data);
assert!(debug_str.contains("FormData"));
}
#[test]
fn test_file_part_debug() {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = temp_dir.path().join("test_file.txt");
std::fs::write(&file_path, b"test content").unwrap();
let file_part = FilePart {
name: Some("test_file.txt".to_string()),
headers: HeaderMap::new(),
path: file_path,
size: 12,
temp_dir: Some(temp_dir.path().to_path_buf()),
};
let debug_str = format!("{:?}", file_part);
assert!(debug_str.contains("FilePart"));
}
#[test]
fn test_file_part_with_custom_headers() {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = temp_dir.path().join("test_file.txt");
std::fs::write(&file_path, b"test content").unwrap();
let mut headers = HeaderMap::new();
headers.insert("content-type", HeaderValue::from_static("image/jpeg"));
headers.insert(
"content-disposition",
HeaderValue::from_static("attachment"),
);
let file_part = FilePart {
name: Some("photo.jpg".to_string()),
headers: headers.clone(),
path: file_path,
size: 12,
temp_dir: Some(temp_dir.path().to_path_buf()),
};
assert_eq!(file_part.headers().len(), 2);
assert_eq!(
file_part.headers().get("content-type").unwrap(),
"image/jpeg"
);
assert_eq!(
file_part.headers().get("content-disposition").unwrap(),
"attachment"
);
}
#[test]
fn test_file_part_path_with_special_characters() {
let temp_dir = tempfile::tempdir().unwrap();
let file_name = "test file-@#$.txt";
let file_path = temp_dir.path().join(file_name);
std::fs::write(&file_path, b"test content").unwrap();
let file_part = FilePart {
name: Some(file_name.to_string()),
headers: HeaderMap::new(),
path: file_path.clone(),
size: 12,
temp_dir: Some(temp_dir.path().to_path_buf()),
};
assert_eq!(file_part.path(), &file_path);
assert_eq!(file_part.name(), Some(file_name));
}
#[test]
fn test_file_part_temp_dir_none() {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = temp_dir.path().join("test_file.txt");
std::fs::write(&file_path, b"test content").unwrap();
let file_part = FilePart {
name: Some("test_file.txt".to_string()),
headers: HeaderMap::new(),
path: file_path.clone(),
size: 12,
temp_dir: None,
};
assert!(file_part.temp_dir.is_none());
assert!(file_path.exists());
}
#[test]
fn test_file_part_temp_dir_some() {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = temp_dir.path().join("test_file.txt");
std::fs::write(&file_path, b"test content").unwrap();
let file_part = FilePart {
name: Some("test_file.txt".to_string()),
headers: HeaderMap::new(),
path: file_path.clone(),
size: 12,
temp_dir: Some(temp_dir.path().to_path_buf()),
};
assert!(file_part.temp_dir.is_some());
assert_eq!(file_part.temp_dir.as_ref().unwrap(), temp_dir.path());
}
#[test]
fn test_form_data_duplicate_fields() {
let mut form_data = FormData::new();
form_data
.fields
.insert("key".to_string(), "value1".to_string());
form_data
.fields
.insert("key".to_string(), "value2".to_string());
form_data
.fields
.insert("key".to_string(), "value3".to_string());
let values = form_data.fields.get_vec("key").unwrap();
assert_eq!(values.len(), 3);
}
#[test]
fn test_file_part_rename() {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = temp_dir.path().join("old_name.txt");
std::fs::write(&file_path, b"test content").unwrap();
let mut file_part = FilePart {
name: Some("old_name.txt".to_string()),
headers: HeaderMap::new(),
path: file_path,
size: 12,
temp_dir: Some(temp_dir.path().to_path_buf()),
};
if let Some(name) = file_part.name_mut() {
*name = "new_name.txt".to_string();
}
assert_eq!(file_part.name(), Some("new_name.txt"));
}
#[test]
fn test_file_part_empty_filename() {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = temp_dir.path().join(".txt");
std::fs::write(&file_path, b"test content").unwrap();
let file_part = FilePart {
name: Some("".to_string()),
headers: HeaderMap::new(),
path: file_path,
size: 12,
temp_dir: Some(temp_dir.path().to_path_buf()),
};
assert_eq!(file_part.name(), Some(""));
}
#[test]
fn test_file_part_unicode_filename() {
let temp_dir = tempfile::tempdir().unwrap();
let unicode_name = "测试文件🎉.txt";
let file_path = temp_dir.path().join(unicode_name);
std::fs::write(&file_path, b"test content").unwrap();
let file_part = FilePart {
name: Some(unicode_name.to_string()),
headers: HeaderMap::new(),
path: file_path,
size: 12,
temp_dir: Some(temp_dir.path().to_path_buf()),
};
assert_eq!(file_part.name(), Some(unicode_name));
}
#[test]
fn test_form_data_multiple_fields_and_files() {
let mut form_data = FormData::new();
form_data
.fields
.insert("username".to_string(), "alice".to_string());
form_data
.fields
.insert("email".to_string(), "alice@example.com".to_string());
let temp_dir = tempfile::tempdir().unwrap();
for i in 1..=3 {
let file_path = temp_dir.path().join(format!("file{}.txt", i));
std::fs::write(&file_path, format!("content{}", i)).unwrap();
form_data.files.insert(
format!("file{}", i),
FilePart {
name: Some(format!("file{}.txt", i)),
headers: HeaderMap::new(),
path: file_path,
size: 8,
temp_dir: Some(temp_dir.path().to_path_buf()),
},
);
}
assert_eq!(form_data.fields.len(), 2);
assert_eq!(form_data.files.len(), 3);
}
}