wiremock_multipart/
part.rs1use lazy_regex::regex;
2
3#[derive(Debug, PartialEq, Eq)]
4pub struct Part<'a> {
5 pub content: &'a [u8],
6}
7
8impl<'a> Part<'a> {
9 pub fn name(&self) -> Option<&'a str> {
10 let header = self.header();
11 match header {
12 None => None,
13 Some(header) => {
14 let regex = regex!(r#";\s*name="([^"].*?)""#i);
15 regex.captures(header)
16 .and_then(|cap| cap.get(1))
17 .map(|mtch| mtch.as_str())
18 },
19 }
20 }
21
22 pub fn filename(&self) -> Option<&'a str> {
23 let header = self.header();
24 match header {
25 None => None,
26 Some(header) => {
27 let regex = regex!(r#";\s*filename="([^"].*?)""#i);
28 regex.captures(header)
29 .and_then(|cap| cap.get(1))
30 .map(|mtch| mtch.as_str())
31 },
32 }
33 }
34
35 pub fn content_type(&self) -> Option<&'a str> {
36 let header = self.header();
37 match header {
38 None => None,
39 Some(header) => {
40 let regex = regex!(r#"content-type:\s*([^\n].*)"#i);
41 regex.captures(header)
42 .and_then(|cap| cap.get(1))
43 .map(|mtch| mtch.as_str())
44 },
45 }
46 }
47
48 pub fn header(&self) -> Option<&'a str> {
49 match self.header_body_boundary() {
50 None => None,
51 Some((end_of_header_index, _)) => {
52 Some(std::str::from_utf8(&self.content[0..end_of_header_index]).unwrap())
53 },
54 }
55 }
56
57 pub fn body(&self) -> Option<&'a [u8]> {
58 match self.header_body_boundary() {
59 None => None,
60 Some((end_of_header_index, separator_len)) => {
61 Some(&self.content[(end_of_header_index + separator_len)..])
62 },
63 }
64 }
65
66 fn header_body_boundary(&self) -> Option<(usize, usize)> {
67 self.content
68 .windows(4)
69 .enumerate()
70 .find_map(|(index, window)| {
71 match window {
72 [b'\n', b'\n', _, _] => Some((index, 2)),
73 [b'\r', b'\n', b'\r', b'\n'] => Some((index, 4)),
74 _ => None,
75 }
76 })
77 }
78}
79
80impl<'a> From<&'a [u8]> for Part<'a> {
81 fn from(content: &'a [u8]) -> Self {
82 Part {
83 content,
84 }
85 }
86}
87
88impl<'a> From<&'a str> for Part<'a> {
89 fn from(text: &'a str) -> Self {
90 Part {
91 content: text.as_bytes(),
92 }
93 }
94}
95
96impl<'a> From<&'a String> for Part<'a> {
97 fn from(text: &'a String) -> Self {
98 Part {
99 content: text.as_bytes(),
100 }
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[test]
109 fn should_extract_header_and_body() {
110 let part = Part::from("Content-Disposition: form-data; name=\"text\"\nContent-Type: plain/text\n\ncontent");
111
112 assert_eq!(
113 part.header(),
114 Some("Content-Disposition: form-data; name=\"text\"\nContent-Type: plain/text"),
115 );
116
117 assert_eq!(
118 part.body(),
119 Some("content".as_bytes()),
120 );
121 }
122
123 #[test]
124 fn should_extract_header_and_body_with_cr_and_newline() {
125 let part = Part::from("Content-Disposition: form-data; name=\"text\"\r\nContent-Type: plain/text\r\n\r\ncontent");
126
127 assert_eq!(
128 part.header(),
129 Some("Content-Disposition: form-data; name=\"text\"\r\nContent-Type: plain/text"),
130 );
131
132 assert_eq!(
133 part.body(),
134 Some("content".as_bytes()),
135 );
136 }
137
138 #[test]
139 fn should_extract_part_name() {
140 assert_eq!(
141 Part::from("Content-Disposition: form-data; name=\"text\"; filename=\"filename\"\nContent-Type: plain/text\n\ncontent").name(),
142 Some("text"),
143 );
144 }
145
146 #[test]
147 fn should_extract_file_name() {
148 assert_eq!(
149 Part::from("Content-Disposition: form-data; filename=\"my-file.txt\"; name=\"text\"\n\nContent-Type: plain/text\n\ncontent").filename(),
150 Some("my-file.txt"),
151 );
152 }
153
154 #[test]
155 fn should_extract_content_type() {
156 assert_eq!(
157 Part::from("Content-Disposition: form-data; name=\"text\"\n; filename=\"my-file.txt\"\nContent-Type: plain/text\n\ncontent").content_type(),
158 Some("plain/text"),
159 );
160 }
161
162 #[test]
163 fn should_extract_part_body() {
164 assert_eq!(
165 Part::from("Content-Disposition: form-data; name=\"text\"\nContent-Type: plain/text\n\ncontent").body(),
166 Some("content".as_bytes()),
167 );
168 }
169}