spikard_http/testing/
multipart.rs

1use std::time::{SystemTime, UNIX_EPOCH};
2
3/// File part metadata for multipart/form-data payloads.
4#[derive(Debug, Clone)]
5pub struct MultipartFilePart {
6    pub field_name: String,
7    pub filename: String,
8    pub content_type: Option<String>,
9    pub content: Vec<u8>,
10}
11
12/// Build a multipart/form-data body from fields and files.
13pub fn build_multipart_body(form_fields: &[(String, String)], files: &[MultipartFilePart]) -> (Vec<u8>, String) {
14    let boundary = generate_boundary();
15    let mut body = Vec::new();
16
17    for (name, value) in form_fields {
18        body.extend_from_slice(b"--");
19        body.extend_from_slice(boundary.as_bytes());
20        body.extend_from_slice(b"\r\n");
21        body.extend_from_slice(b"Content-Disposition: form-data; name=\"");
22        body.extend_from_slice(name.as_bytes());
23        body.extend_from_slice(b"\"\r\n\r\n");
24        body.extend_from_slice(value.as_bytes());
25        body.extend_from_slice(b"\r\n");
26    }
27
28    for file in files {
29        body.extend_from_slice(b"--");
30        body.extend_from_slice(boundary.as_bytes());
31        body.extend_from_slice(b"\r\n");
32        body.extend_from_slice(b"Content-Disposition: form-data; name=\"");
33        body.extend_from_slice(file.field_name.as_bytes());
34        body.extend_from_slice(b"\"; filename=\"");
35        body.extend_from_slice(file.filename.as_bytes());
36        body.extend_from_slice(b"\"\r\n");
37        if let Some(content_type) = &file.content_type {
38            body.extend_from_slice(b"Content-Type: ");
39            body.extend_from_slice(content_type.as_bytes());
40            body.extend_from_slice(b"\r\n");
41        }
42        body.extend_from_slice(b"\r\n");
43        body.extend_from_slice(&file.content);
44        body.extend_from_slice(b"\r\n");
45    }
46
47    body.extend_from_slice(b"--");
48    body.extend_from_slice(boundary.as_bytes());
49    body.extend_from_slice(b"--\r\n");
50
51    (body, boundary)
52}
53
54fn generate_boundary() -> String {
55    let nanos = SystemTime::now()
56        .duration_since(UNIX_EPOCH)
57        .map(|duration| duration.as_nanos())
58        .unwrap_or_default();
59    format!("spikard-boundary-{nanos}")
60}