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 delimiter = boundary_marker.as_bytes();
185
186 let finder = memchr::memmem::Finder::new(delimiter);
188 let positions: Vec<usize> = finder.find_iter(body).collect();
189
190 for pair in positions.windows(2) {
192 let mut part = &body[pair[0] + delimiter.len()..pair[1]];
193
194 if part.starts_with(b"--") {
196 continue;
197 }
198
199 if let Some(rest) = part.strip_prefix(b"\r\n") {
201 part = rest;
202 } else if let Some(rest) = part.strip_prefix(b"\n") {
203 part = rest;
204 }
205
206 if let Some(rest) = part.strip_suffix(b"\r\n") {
208 part = rest;
209 } else if let Some(rest) = part.strip_suffix(b"\n") {
210 part = rest;
211 }
212
213 if let Some(field) = self.parse_part(part)? {
215 fields.push(field);
216 }
217 }
218
219 Ok(fields)
220 }
221
222 fn parse_part(&self, part: &[u8]) -> Result<Option<FormField>, Error> {
224 if part.is_empty() {
225 return Ok(None);
226 }
227
228 let (header_block, content) = match memchr::memmem::find(part, b"\r\n\r\n") {
231 Some(pos) => (&part[..pos], &part[pos + 4..]),
232 None => match memchr::memmem::find(part, b"\n\n") {
233 Some(pos) => (&part[..pos], &part[pos + 2..]),
234 None => (part, &part[part.len()..]),
235 },
236 };
237
238 let headers = String::from_utf8_lossy(header_block);
240
241 let mut name = None;
243 let mut filename = None;
244 let mut content_type = None;
245
246 for line in headers.lines() {
247 if line.starts_with("Content-Disposition:") {
248 for attr in line.split(';') {
250 let attr = attr.trim();
251 if attr.starts_with("name=") {
252 name = Some(
253 attr.trim_start_matches("name=")
254 .trim_matches('"')
255 .to_string(),
256 );
257 } else if attr.starts_with("filename=") {
258 filename = Some(
259 attr.trim_start_matches("filename=")
260 .trim_matches('"')
261 .to_string(),
262 );
263 }
264 }
265 } else if line.starts_with("Content-Type:") {
266 content_type = Some(line.trim_start_matches("Content-Type:").trim().to_string());
267 }
268 }
269
270 let name = name.ok_or_else(|| Error::BadRequest("Missing field name".to_string()))?;
271
272 if let Some(filename) = filename {
274 let file = FormFile::new(
276 filename,
277 content_type.unwrap_or_else(|| "application/octet-stream".to_string()),
278 content.to_vec(),
279 );
280 Ok(Some(FormField {
281 name,
282 value: None,
283 file: Some(file),
284 }))
285 } else {
286 Ok(Some(FormField {
288 name,
289 value: Some(String::from_utf8_lossy(content).into_owned()),
290 file: None,
291 }))
292 }
293 }
294
295 pub fn to_map(fields: Vec<FormField>) -> HashMap<String, String> {
297 fields
298 .into_iter()
299 .filter_map(|field| field.value.map(|value| (field.name, value)))
300 .collect()
301 }
302
303 pub fn get_files(fields: &[FormField]) -> Vec<(String, &FormFile)> {
305 fields
306 .iter()
307 .filter_map(|field| field.file.as_ref().map(|file| (field.name.clone(), file)))
308 .collect()
309 }
310}
311
312#[cfg(test)]
313mod tests {
314 use super::*;
315
316 #[test]
317 fn test_parse_form_map() {
318 let body = b"name=John+Doe&email=john%40example.com&age=30";
319 let form = parse_form_map(body).unwrap();
320
321 assert_eq!(form.get("name"), Some(&"John Doe".to_string()));
322 assert_eq!(form.get("email"), Some(&"john@example.com".to_string()));
323 assert_eq!(form.get("age"), Some(&"30".to_string()));
324 }
325
326 #[test]
327 fn test_form_file_extension() {
328 let file = FormFile::new(
329 "document.pdf".to_string(),
330 "application/pdf".to_string(),
331 vec![1, 2, 3],
332 );
333
334 assert_eq!(file.extension(), Some("pdf"));
335 }
336
337 #[test]
338 fn test_form_file_is_image() {
339 let image = FormFile::new("photo.jpg".to_string(), "image/jpeg".to_string(), vec![]);
340 assert!(image.is_image());
341
342 let doc = FormFile::new("doc.pdf".to_string(), "application/pdf".to_string(), vec![]);
343 assert!(!doc.is_image());
344 }
345
346 #[test]
347 fn test_form_file_size_check() {
348 let file = FormFile::new(
349 "file.txt".to_string(),
350 "text/plain".to_string(),
351 vec![0; 1024], );
353
354 assert!(!file.exceeds_size(2048)); assert!(file.exceeds_size(512)); }
357
358 #[test]
359 fn test_multipart_parser_from_content_type() {
360 let content_type = "multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW";
361 let parser = MultipartParser::from_content_type(content_type).unwrap();
362
363 assert_eq!(parser.boundary, "----WebKitFormBoundary7MA4YWxkTrZu0gW");
364 }
365
366 #[test]
367 fn test_multipart_binary_roundtrip() {
368 let file_data: Vec<u8> = vec![
371 b' ', b'\t', 0x00, 0xFF, 0xFE, b'\r', b'\n', 0x80, 0xC3, 0x01, b'\n', b' ',
372 ];
373
374 let mut body = Vec::new();
375 body.extend_from_slice(b"--XBOUNDARY\r\n");
376 body.extend_from_slice(
377 b"Content-Disposition: form-data; name=\"file\"; filename=\"blob.bin\"\r\n",
378 );
379 body.extend_from_slice(b"Content-Type: application/octet-stream\r\n\r\n");
380 body.extend_from_slice(&file_data);
381 body.extend_from_slice(b"\r\n--XBOUNDARY\r\n");
382 body.extend_from_slice(b"Content-Disposition: form-data; name=\"note\"\r\n\r\n");
383 body.extend_from_slice(b"hello world");
384 body.extend_from_slice(b"\r\n--XBOUNDARY--\r\n");
385
386 let parser =
387 MultipartParser::from_content_type("multipart/form-data; boundary=XBOUNDARY").unwrap();
388 let fields = parser.parse(&body).unwrap();
389
390 assert_eq!(fields.len(), 2);
391
392 let file = fields[0].file.as_ref().unwrap();
393 assert_eq!(fields[0].name, "file");
394 assert_eq!(file.filename, "blob.bin");
395 assert_eq!(file.content_type, "application/octet-stream");
396 assert_eq!(file.data, file_data);
397 assert_eq!(file.size, file_data.len());
398
399 assert_eq!(fields[1].name, "note");
400 assert_eq!(fields[1].value.as_deref(), Some("hello world"));
401 }
402
403 #[test]
404 fn test_multipart_parser_missing_boundary() {
405 let content_type = "multipart/form-data";
406 let result = MultipartParser::from_content_type(content_type);
407
408 assert!(result.is_err());
409 }
410}