wiremock_multipart/
part.rs

1use 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}