wiremock_multipart/
request_utils.rs1use std::str::FromStr;
2
3use wiremock::http::HeaderName;
4use wiremock::Request;
5
6use crate::part::Part;
7
8pub trait RequestUtils {
9 fn multipart_contenttype(&self) -> Option<MultipartContentType>;
10 fn parts(&self) -> Vec<Part>;
11}
12
13impl RequestUtils for Request {
14 fn multipart_contenttype(&self) -> Option<MultipartContentType> {
15 let content_type = self
16 .headers
17 .get_all(&HeaderName::from_str("content-type").unwrap())
18 .iter()
19 .find(|value| {
20 value
21 .to_str()
22 .unwrap_or_default()
23 .to_lowercase()
24 .starts_with("multipart/")
25 });
26 match content_type {
27 None => None,
28 Some(value) => {
29 let parts = value
30 .to_str()
31 .unwrap_or_default()
32 .split(";")
33 .collect::<Vec<_>>();
34
35 let multipart_type = parts[0].split("/").nth(1).unwrap().trim();
36
37 let boundary = parts
38 .iter()
39 .map(|part| part.trim())
40 .find(|part| part.starts_with("boundary="))
41 .map(|whole| whole.split("=").nth(1).unwrap().trim());
42
43 Some(MultipartContentType {
44 multipart_type,
45 boundary,
46 })
47 }
48 }
49 }
50
51 fn parts(&self) -> Vec<Part> {
52 if let Some(content_type) = self.multipart_contenttype() {
53 if content_type.multipart_type == "form-data" {
54 if let Some(boundary) = content_type.boundary {
55 let boundary = {
56 let mut tmp: Vec<u8> = vec!['-' as u8; boundary.as_bytes().len() + 2];
57 tmp[0] = '-' as u8;
58 tmp[1] = '-' as u8;
59 tmp[2..].copy_from_slice(boundary.as_bytes());
60 tmp
61 };
62
63 let boundary_start_indexes = self
64 .body
65 .windows(boundary.len())
66 .enumerate()
67 .filter(|(_, window)| window == &boundary)
68 .map(|(index, _)| index)
69 .collect::<Vec<_>>();
70
71 boundary_start_indexes
72 .windows(2)
73 .map(|w| (boundary.len() + 1 + w[0], w[1]))
74 .map(|(start, end)| &self.body[start..end])
75 .map(|body| trim_single_linebreak_from_start(body))
76 .map(|body| trim_single_linebreak_from_end(body))
77 .map(|it| Part::from(it))
78 .collect::<Vec<_>>()
79 } else {
80 vec![]
81 }
82 } else {
83 vec![]
84 }
85 } else {
86 vec![]
87 }
88 }
89}
90
91fn trim_single_linebreak_from_start(body: &[u8]) -> &[u8] {
92 if body.len() >= 2 {
93 match body[..2] {
94 [b'\r', b'\n'] => &body[2..],
95 [b'\n', _] => &body[1..],
96 _ => body,
97 }
98 } else if body.len() >= 1 {
99 match body[0] {
100 b'\n' => &body[1..],
101 _ => body,
102 }
103 } else {
104 body
105 }
106}
107
108fn trim_single_linebreak_from_end(body: &[u8]) -> &[u8] {
109 if body.len() >= 2 {
110 match body[(body.len() - 2)..(body.len())] {
111 [b'\r', b'\n'] => &body[..body.len() - 2],
112 [_, b'\n'] => &body[..body.len() - 1],
113 _ => body,
114 }
115 } else if body.len() >= 1 {
116 match body[(body.len() - 1)..(body.len())] {
117 [_, b'\n'] => &body[..body.len() - 1],
118 _ => body,
119 }
120 } else {
121 body
122 }
123}
124
125#[derive(PartialEq, Eq, Debug, Clone, Copy)]
126pub struct MultipartContentType<'a> {
127 pub multipart_type: &'a str,
128 pub boundary: Option<&'a str>,
129}
130
131#[cfg(test)]
132mod tests {
133 use indoc::indoc;
134 use maplit::hashmap;
135
136 use crate::test_utils::{multipart_header, name, request, requestb, values};
137
138 use super::*;
139
140 #[test]
141 fn multipart_contenttype_should_return_none_if_no_multipart_request() {
142 assert_eq!(request(hashmap! {},).multipart_contenttype(), None);
143
144 assert_eq!(
145 request(hashmap! {
146 name("accept") => values("application/json"),
147 },)
148 .multipart_contenttype(),
149 None
150 );
151
152 assert_eq!(
153 request(hashmap! {
154 name("content-type") => values("image/jpeg"),
155 },)
156 .multipart_contenttype(),
157 None
158 );
159 }
160
161 #[test]
162 fn multipart_contenttype_should_return_some_if_multipart_request() {
163 assert_eq!(
164 request(hashmap! {
165 name("content-type") => values("multipart/foo"),
166 },)
167 .multipart_contenttype(),
168 Some(MultipartContentType {
169 multipart_type: "foo",
170 boundary: None,
171 })
172 );
173
174 assert_eq!(
175 request(hashmap! {
176 name("content-type") => values("multipart/bar; boundary=xyz"),
177 },)
178 .multipart_contenttype(),
179 Some(MultipartContentType {
180 multipart_type: "bar",
181 boundary: Some("xyz"),
182 })
183 );
184 }
185
186 #[test]
187 fn parts_should_find_single_text_part() {
188 assert_eq!(
189 requestb(
190 hashmap! {
191 name("content-type") => values("multipart/form-data; boundary=xyz"),
192 },
193 indoc! {"
194 --xyz
195 Content-Disposition: form-data; name=\"part1\"
196
197 content
198 --xyz--
199 "}
200 .as_bytes()
201 .into(),
202 )
203 .parts(),
204 vec![Part::from(
205 "Content-Disposition: form-data; name=\"part1\"\n\ncontent"
206 ),],
207 );
208 }
209
210 #[test]
211 fn parts_should_find_single_text_part_with_crnr() {
212 assert_eq!(
213 requestb(
214 multipart_header(),
215 "--xyz\r\nContent-Disposition: form-data; name=\"part1\"\r\n\r\ncontent\r\n--xyz--"
216 .as_bytes()
217 .into()
218 )
219 .parts(),
220 vec![Part::from(
221 "Content-Disposition: form-data; name=\"part1\"\r\n\r\ncontent"
222 ),],
223 );
224 }
225
226 #[test]
227 fn parts_should_find_two_text_parts() {
228 assert_eq!(
229 requestb(
230 hashmap! {
231 name("content-type") => values("multipart/form-data; boundary=xyz"),
232 },
233 indoc! {r#"
234 --xyz
235 Content-Disposition: form-data; name="part1"
236
237 content
238 --xyz
239 Content-Disposition: form-data; name="file"; filename="Cargo.toml"
240 Content-Type: plain/text
241
242 [workspace]
243 members = [
244 "fhttp",
245 "fhttp-core",
246 ]
247
248 --xyz--
249 "#}
250 .as_bytes()
251 .into(),
252 )
253 .parts(),
254 vec![
255 Part::from("Content-Disposition: form-data; name=\"part1\"\n\ncontent"),
256 Part::from(indoc! {r#"
257 Content-Disposition: form-data; name="file"; filename="Cargo.toml"
258 Content-Type: plain/text
259
260 [workspace]
261 members = [
262 "fhttp",
263 "fhttp-core",
264 ]
265 "#}),
266 ],
267 );
268 }
269
270 #[test]
271 fn parts_should_find_two_text_parts_with_crnl() {
272 let part2 = {
273 let mut tmp = String::new();
274 tmp += "Content-Disposition: form-data; name=\"file\"; filename=\"Cargo.toml\"";
275 tmp += "\r\nContent-Type: plain/text";
276 tmp += "\r\n\r\n";
277 tmp += "[workspace]\nmembers = [\n \"fhttp\",\n \"fhttp-core\",\n]\n";
278
279 tmp
280 };
281 let mut body = String::new();
282 body += "--xyz";
283 body += "\r\nContent-Disposition: form-data; name=\"part1\"";
284 body += "\r\n\r\ncontent";
285 body += "\r\n--xyz\r\n";
286 body += &part2;
287 body += "\r\n--xyz--\r\n";
288
289 assert_eq!(
290 requestb(multipart_header(), body.as_bytes().into(),).parts(),
291 vec![
292 Part::from("Content-Disposition: form-data; name=\"part1\"\r\n\r\ncontent"),
293 Part::from(&part2),
294 ],
295 );
296 }
297}