1use crate::Error;
4use serde::de::DeserializeOwned;
5use std::collections::HashMap;
6
7pub fn parse_form<T: DeserializeOwned>(body: &[u8]) -> Result<T, Error> {
9 serde_urlencoded::from_bytes(body)
10 .map_err(|e| Error::BadRequest(format!("Failed to parse form data: {}", e)))
11}
12
13pub fn parse_form_map(body: &[u8]) -> Result<HashMap<String, String>, Error> {
15 let form_data: Vec<(String, String)> = serde_urlencoded::from_bytes(body)
16 .map_err(|e| Error::BadRequest(format!("Failed to parse form data: {}", e)))?;
17
18 Ok(form_data.into_iter().collect())
19}
20
21#[derive(Debug, Clone)]
23pub struct FormField {
24 pub name: String,
26
27 pub value: Option<String>,
29
30 pub file: Option<FormFile>,
32}
33
34#[derive(Debug, Clone)]
36pub struct FormFile {
37 pub filename: String,
39
40 pub content_type: String,
42
43 pub size: usize,
45
46 pub data: Vec<u8>,
48}
49
50impl FormFile {
51 pub fn new(filename: String, content_type: String, data: Vec<u8>) -> Self {
53 let size = data.len();
54 Self {
55 filename,
56 content_type,
57 size,
58 data,
59 }
60 }
61
62 pub fn extension(&self) -> Option<&str> {
64 self.filename.rsplit('.').next()
65 }
66
67 pub fn is_image(&self) -> bool {
69 self.content_type.starts_with("image/")
70 }
71
72 pub fn exceeds_size(&self, max_bytes: usize) -> bool {
74 self.size > max_bytes
75 }
76
77 pub fn save_to(&self, path: &str) -> Result<(), Error> {
79 std::fs::write(path, &self.data)
80 .map_err(|e| Error::Internal(format!("Failed to save file: {}", e)))
81 }
82
83 pub async fn save_to_async(&self, path: &str) -> Result<(), Error> {
85 tokio::fs::write(path, &self.data)
86 .await
87 .map_err(|e| Error::Internal(format!("Failed to save file: {}", e)))
88 }
89}
90
91pub async fn save_files_parallel(files: Vec<(&FormFile, String)>) -> Result<Vec<String>, Error> {
127 use tokio::task::JoinSet;
128
129 let mut set = JoinSet::new();
130
131 for (file, path) in files {
132 let data = file.data.clone();
133 let path_clone = path.clone();
134
135 set.spawn(async move {
136 tokio::fs::write(&path_clone, &data)
137 .await
138 .map_err(|e| Error::Internal(format!("Failed to save file: {}", e)))?;
139 Ok::<_, Error>(path_clone)
140 });
141 }
142
143 let mut saved_paths = Vec::new();
144 while let Some(result) = set.join_next().await {
145 saved_paths.push(result.map_err(|e| Error::Internal(e.to_string()))??);
146 }
147
148 Ok(saved_paths)
149}
150
151pub struct MultipartParser {
153 boundary: String,
154}
155
156impl MultipartParser {
157 pub fn from_content_type(content_type: &str) -> Result<Self, Error> {
159 let boundary = content_type
162 .split(';')
163 .find_map(|part| {
164 let part = part.trim();
165 if part.starts_with("boundary=") {
166 Some(
167 part.trim_start_matches("boundary=")
168 .trim_matches('"')
169 .to_string(),
170 )
171 } else {
172 None
173 }
174 })
175 .ok_or_else(|| Error::BadRequest("Missing boundary in Content-Type".to_string()))?;
176
177 Ok(Self { boundary })
178 }
179
180 pub fn parse(&self, body: &[u8]) -> Result<Vec<FormField>, Error> {
182 let mut fields = Vec::new();
183 let boundary_marker = format!("--{}", self.boundary);
184 let body_str = String::from_utf8_lossy(body);
185
186 let parts: Vec<&str> = body_str.split(&boundary_marker).collect();
188
189 for part in parts.iter().skip(1) {
190 if part.trim() == "--" || part.trim().is_empty() {
191 continue;
192 }
193
194 if let Some(field) = self.parse_part(part)? {
196 fields.push(field);
197 }
198 }
199
200 Ok(fields)
201 }
202
203 fn parse_part(&self, part: &str) -> Result<Option<FormField>, Error> {
205 let lines: Vec<&str> = part.lines().collect();
206
207 if lines.is_empty() {
208 return Ok(None);
209 }
210
211 let mut name = None;
213 let mut filename = None;
214 let mut content_type = None;
215 let mut content_start = 0;
216
217 for (i, line) in lines.iter().enumerate() {
218 if line.trim().is_empty() {
219 content_start = i + 1;
220 break;
221 }
222
223 if line.starts_with("Content-Disposition:") {
224 for attr in line.split(';') {
226 let attr = attr.trim();
227 if attr.starts_with("name=") {
228 name = Some(
229 attr.trim_start_matches("name=")
230 .trim_matches('"')
231 .to_string(),
232 );
233 } else if attr.starts_with("filename=") {
234 filename = Some(
235 attr.trim_start_matches("filename=")
236 .trim_matches('"')
237 .to_string(),
238 );
239 }
240 }
241 } else if line.starts_with("Content-Type:") {
242 content_type = Some(line.trim_start_matches("Content-Type:").trim().to_string());
243 }
244 }
245
246 let name = name.ok_or_else(|| Error::BadRequest("Missing field name".to_string()))?;
247
248 let content_lines = &lines[content_start..];
250 let content = content_lines.join("\n").trim().to_string();
251
252 if let Some(filename) = filename {
254 let file = FormFile::new(
256 filename,
257 content_type.unwrap_or_else(|| "application/octet-stream".to_string()),
258 content.into_bytes(),
259 );
260 Ok(Some(FormField {
261 name,
262 value: None,
263 file: Some(file),
264 }))
265 } else {
266 Ok(Some(FormField {
268 name,
269 value: Some(content),
270 file: None,
271 }))
272 }
273 }
274
275 pub fn to_map(fields: Vec<FormField>) -> HashMap<String, String> {
277 fields
278 .into_iter()
279 .filter_map(|field| field.value.map(|value| (field.name, value)))
280 .collect()
281 }
282
283 pub fn get_files(fields: &[FormField]) -> Vec<(String, &FormFile)> {
285 fields
286 .iter()
287 .filter_map(|field| field.file.as_ref().map(|file| (field.name.clone(), file)))
288 .collect()
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295
296 #[test]
297 fn test_parse_form_map() {
298 let body = b"name=John+Doe&email=john%40example.com&age=30";
299 let form = parse_form_map(body).unwrap();
300
301 assert_eq!(form.get("name"), Some(&"John Doe".to_string()));
302 assert_eq!(form.get("email"), Some(&"john@example.com".to_string()));
303 assert_eq!(form.get("age"), Some(&"30".to_string()));
304 }
305
306 #[test]
307 fn test_form_file_extension() {
308 let file = FormFile::new(
309 "document.pdf".to_string(),
310 "application/pdf".to_string(),
311 vec![1, 2, 3],
312 );
313
314 assert_eq!(file.extension(), Some("pdf"));
315 }
316
317 #[test]
318 fn test_form_file_is_image() {
319 let image = FormFile::new("photo.jpg".to_string(), "image/jpeg".to_string(), vec![]);
320 assert!(image.is_image());
321
322 let doc = FormFile::new("doc.pdf".to_string(), "application/pdf".to_string(), vec![]);
323 assert!(!doc.is_image());
324 }
325
326 #[test]
327 fn test_form_file_size_check() {
328 let file = FormFile::new(
329 "file.txt".to_string(),
330 "text/plain".to_string(),
331 vec![0; 1024], );
333
334 assert!(!file.exceeds_size(2048)); assert!(file.exceeds_size(512)); }
337
338 #[test]
339 fn test_multipart_parser_from_content_type() {
340 let content_type = "multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW";
341 let parser = MultipartParser::from_content_type(content_type).unwrap();
342
343 assert_eq!(parser.boundary, "----WebKitFormBoundary7MA4YWxkTrZu0gW");
344 }
345
346 #[test]
347 fn test_multipart_parser_missing_boundary() {
348 let content_type = "multipart/form-data";
349 let result = MultipartParser::from_content_type(content_type);
350
351 assert!(result.is_err());
352 }
353}