1use actix_web::{
4 http::header::{self, HeaderMap},
5 web::{BufMut as _, Bytes, BytesMut},
6};
7use mime::Mime;
8use rand::{
9 distributions::{Alphanumeric, DistString as _},
10 thread_rng,
11};
12
13const CRLF: &[u8] = b"\r\n";
14const CRLF_CRLF: &[u8] = b"\r\n\r\n";
15const HYPHENS: &[u8] = b"--";
16const BOUNDARY_PREFIX: &str = "------------------------";
17
18pub fn create_form_data_payload_and_headers(
62 name: &str,
63 filename: Option<String>,
64 content_type: Option<Mime>,
65 file: Bytes,
66) -> (Bytes, HeaderMap) {
67 let boundary = Alphanumeric.sample_string(&mut thread_rng(), 32);
68
69 create_form_data_payload_and_headers_with_boundary(
70 &boundary,
71 name,
72 filename,
73 content_type,
74 file,
75 )
76}
77
78pub fn create_form_data_payload_and_headers_with_boundary(
82 boundary: &str,
83 name: &str,
84 filename: Option<String>,
85 content_type: Option<Mime>,
86 file: Bytes,
87) -> (Bytes, HeaderMap) {
88 let mut buf = BytesMut::with_capacity(file.len() + 128);
89
90 let boundary_str = [BOUNDARY_PREFIX, boundary].concat();
91 let boundary = boundary_str.as_bytes();
92
93 buf.put(HYPHENS);
94 buf.put(boundary);
95 buf.put(CRLF);
96
97 buf.put(format!("Content-Disposition: form-data; name=\"{name}\"").as_bytes());
98 if let Some(filename) = filename {
99 buf.put(format!("; filename=\"{filename}\"").as_bytes());
100 }
101 buf.put(CRLF);
102
103 if let Some(ct) = content_type {
104 buf.put(format!("Content-Type: {ct}").as_bytes());
105 buf.put(CRLF);
106 }
107
108 buf.put(format!("Content-Length: {}", file.len()).as_bytes());
109 buf.put(CRLF_CRLF);
110
111 buf.put(file);
112 buf.put(CRLF);
113
114 buf.put(HYPHENS);
115 buf.put(boundary);
116 buf.put(HYPHENS);
117 buf.put(CRLF);
118
119 let mut headers = HeaderMap::new();
120 headers.insert(
121 header::CONTENT_TYPE,
122 format!("multipart/form-data; boundary=\"{boundary_str}\"")
123 .parse()
124 .unwrap(),
125 );
126
127 (buf.freeze(), headers)
128}
129
130#[cfg(test)]
131mod tests {
132 use std::convert::Infallible;
133
134 use futures_util::stream;
135
136 use super::*;
137
138 fn find_boundary(headers: &HeaderMap) -> String {
139 headers
140 .get("content-type")
141 .unwrap()
142 .to_str()
143 .unwrap()
144 .parse::<mime::Mime>()
145 .unwrap()
146 .get_param(mime::BOUNDARY)
147 .unwrap()
148 .as_str()
149 .to_owned()
150 }
151
152 #[test]
153 fn wire_format() {
154 let (pl, headers) = create_form_data_payload_and_headers_with_boundary(
155 "qWeRtYuIoP",
156 "foo",
157 None,
158 None,
159 Bytes::from_static(b"Lorem ipsum dolor\nsit ame."),
160 );
161
162 assert_eq!(
163 find_boundary(&headers),
164 "------------------------qWeRtYuIoP",
165 );
166
167 assert_eq!(
168 std::str::from_utf8(&pl).unwrap(),
169 "--------------------------qWeRtYuIoP\r\n\
170 Content-Disposition: form-data; name=\"foo\"\r\n\
171 Content-Length: 26\r\n\
172 \r\n\
173 Lorem ipsum dolor\n\
174 sit ame.\r\n\
175 --------------------------qWeRtYuIoP--\r\n",
176 );
177
178 let (pl, _headers) = create_form_data_payload_and_headers_with_boundary(
179 "qWeRtYuIoP",
180 "foo",
181 Some("Lorem.txt".to_owned()),
182 Some(mime::TEXT_PLAIN_UTF_8),
183 Bytes::from_static(b"Lorem ipsum dolor\nsit ame."),
184 );
185
186 assert_eq!(
187 std::str::from_utf8(&pl).unwrap(),
188 "--------------------------qWeRtYuIoP\r\n\
189 Content-Disposition: form-data; name=\"foo\"; filename=\"Lorem.txt\"\r\n\
190 Content-Type: text/plain; charset=utf-8\r\n\
191 Content-Length: 26\r\n\
192 \r\n\
193 Lorem ipsum dolor\n\
194 sit ame.\r\n\
195 --------------------------qWeRtYuIoP--\r\n",
196 );
197 }
198
199 #[actix_web::test]
201 async fn ecosystem_compat() {
202 let (pl, headers) = create_form_data_payload_and_headers(
203 "foo",
204 None,
205 None,
206 Bytes::from_static(b"Lorem ipsum dolor\nsit ame."),
207 );
208
209 let boundary = find_boundary(&headers);
210
211 let pl = stream::once(async { Ok::<_, Infallible>(pl) });
212
213 let mut form = multer::Multipart::new(pl, boundary);
214 let field = form.next_field().await.unwrap().unwrap();
215 assert_eq!(field.name().unwrap(), "foo");
216 assert_eq!(field.file_name(), None);
217 assert_eq!(field.content_type(), None);
218 assert!(field.bytes().await.unwrap().starts_with(b"Lorem"));
219 }
220}