form_data_builder/lib.rs
1//! This is a simple `multipart/form-data` ([RFC 7578][rfc7578]) document builder.
2//!
3//! ```
4//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
5//! use form_data_builder::FormData;
6//!
7//! let mut form = FormData::new(Vec::new()); // use a Vec<u8> as a writer
8//! form.content_type_header(); // add this `Content-Type` header to your HTTP request
9//!
10//! form.write_path("ferris", "testdata/rustacean-flat-noshadow.png", "image/png")?;
11//! form.write_field("cute", "yes")?;
12//! form.finish(); // returns the writer
13//! # Ok(())
14//! # }
15//! ```
16//!
17//! Looking for a feature-packed, asynchronous, robust, and well-tested `multipart/form-data`
18//! library that validates things like content types? We hope you find one somewhere!
19//!
20//! [rfc7578]: https://www.rfc-editor.org/rfc/rfc7578.html
21
22#![warn(clippy::pedantic)]
23
24use rand::{thread_rng, RngCore};
25use std::ffi::OsStr;
26use std::fs::File;
27use std::io::{Error, ErrorKind, Read, Result, Write};
28use std::path::Path;
29use std::time::SystemTime;
30
31/// `multipart/form-data` document builder.
32///
33/// See the [module documentation][`crate`] for an example.
34#[derive(Debug, Clone)]
35pub struct FormData<W> {
36 writer: Option<W>,
37 boundary: String,
38}
39
40impl<W: Write> FormData<W> {
41 /// Starts writing a `multipart/form-data` document to `writer`.
42 ///
43 /// ```
44 /// # use form_data_builder::FormData;
45 /// let mut form = FormData::new(Vec::new());
46 /// ```
47 ///
48 /// This generates a nonce as a multipart boundary by combining the current system time with a
49 /// random string.
50 ///
51 /// # Panics
52 ///
53 /// Panics if the random number generator fails or if the current system time is prior to the
54 /// Unix epoch.
55 pub fn new(writer: W) -> FormData<W> {
56 let mut buf = [0; 24];
57
58 let now = SystemTime::now()
59 .duration_since(SystemTime::UNIX_EPOCH)
60 .expect("system time should be after the Unix epoch");
61 (&mut buf[..4]).copy_from_slice(&now.subsec_nanos().to_ne_bytes());
62 (&mut buf[4..12]).copy_from_slice(&now.as_secs().to_ne_bytes());
63 thread_rng().fill_bytes(&mut buf[12..]);
64
65 let boundary = format!("{:->68}", base64::encode_config(&buf, base64::URL_SAFE));
66
67 FormData {
68 writer: Some(writer),
69 boundary,
70 }
71 }
72
73 /// Finish the `multipart/form-data` document, returning the writer.
74 ///
75 /// ```
76 /// # use form_data_builder::FormData;
77 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
78 /// let mut form = FormData::new(Vec::new());
79 /// // ... things happen ...
80 /// let document: Vec<u8> = form.finish()?;
81 /// # Ok(())
82 /// # }
83 /// ```
84 ///
85 /// # Errors
86 ///
87 /// Returns an error if `finish()` has already been called or if the writer fails.
88 pub fn finish(&mut self) -> Result<W> {
89 let mut writer = self
90 .writer
91 .take()
92 .ok_or_else(|| Error::new(ErrorKind::Other, "you can only finish once"))?;
93 write!(writer, "--{}--\r\n", self.boundary)?;
94 Ok(writer)
95 }
96
97 fn write_header(
98 &mut self,
99 name: &str,
100 filename: Option<&OsStr>,
101 content_type: Option<&str>,
102 ) -> Result<&mut W> {
103 let writer = self.writer.as_mut().ok_or_else(|| {
104 Error::new(
105 ErrorKind::Other,
106 "this method cannot be used after using `finish()`",
107 )
108 })?;
109
110 write!(writer, "--{}\r\n", self.boundary)?;
111
112 write!(writer, "Content-Disposition: form-data; name=\"{}\"", name)?;
113 if let Some(filename) = filename {
114 write!(writer, "; filename=\"{}\"", filename.to_string_lossy())?;
115 }
116 write!(writer, "\r\n")?;
117
118 if let Some(content_type) = content_type {
119 write!(writer, "Content-Type: {}\r\n", content_type)?;
120 }
121
122 write!(writer, "\r\n")?;
123 Ok(writer)
124 }
125
126 /// Write a non-file field to the document.
127 ///
128 /// ```
129 /// # use form_data_builder::FormData;
130 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
131 /// # let mut form = FormData::new(Vec::new());
132 /// form.write_field("butts", "lol")?;
133 /// # Ok(())
134 /// # }
135 /// ```
136 ///
137 /// # Errors
138 ///
139 /// Returns an error if `finish()` has already been called or if the writer fails.
140 pub fn write_field(&mut self, name: &str, value: &str) -> Result<()> {
141 let writer = self.write_header(name, None, None)?;
142 write!(writer, "{}\r\n", value)
143 }
144
145 /// Write a file field to the document, copying the data from `reader`.
146 ///
147 /// [RFC 7578 ยง 4.2](rfc7578sec4.2) advises "a name for the file SHOULD be supplied", but
148 /// "isn't mandatory for cases where the file name isn't availbale or is meaningless or
149 /// private".
150 ///
151 /// [rfc7578sec4.2]: https://www.rfc-editor.org/rfc/rfc7578.html#section-4.2
152 ///
153 /// ```
154 /// # use form_data_builder::FormData;
155 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
156 /// # let mut form = FormData::new(Vec::new());
157 /// use std::io::Cursor;
158 ///
159 /// const CORRO: &[u8] = include_bytes!("../testdata/corro.svg");
160 /// form.write_file("corro", Cursor::new(CORRO), None, "image/svg+xml")?;
161 /// # Ok(())
162 /// # }
163 /// ```
164 ///
165 /// # Errors
166 ///
167 /// Returns an error if `finish()` has already been called or if the writer fails.
168 pub fn write_file<R: Read>(
169 &mut self,
170 name: &str,
171 mut reader: R,
172 filename: Option<&OsStr>,
173 content_type: &str,
174 ) -> Result<()> {
175 let writer = self.write_header(name, filename, Some(content_type))?;
176 std::io::copy(&mut reader, writer)?;
177 write!(writer, "\r\n")
178 }
179
180 /// Write a file field to the document, opening the file at `path` and copying its data.
181 ///
182 /// This method detects the `filename` parameter from the `path`. To avoid this, use
183 /// [`FormData::write_file`].
184 ///
185 /// ```
186 /// # use form_data_builder::FormData;
187 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
188 /// # let mut form = FormData::new(Vec::new());
189 /// form.write_path("corro", "testdata/corro.svg", "image/svg+xml")?;
190 /// # Ok(())
191 /// # }
192 /// ```
193 ///
194 /// # Errors
195 ///
196 /// Returns an error if `finish()` has already been called or if the writer fails.
197 pub fn write_path<P: AsRef<Path>>(
198 &mut self,
199 name: &str,
200 path: P,
201 content_type: &str,
202 ) -> Result<()> {
203 self.write_file(
204 name,
205 &mut File::open(path.as_ref())?,
206 path.as_ref().file_name(),
207 content_type,
208 )
209 }
210
211 /// Returns the value of the `Content-Type` header that corresponds with the document.
212 ///
213 /// ```
214 /// # use form_data_builder::FormData;
215 /// # struct Request;
216 /// # impl Request {
217 /// # fn with_header(&mut self, key: &str, value: String) {}
218 /// # }
219 /// # let mut request = Request;
220 /// # let mut form = FormData::new(Vec::new());
221 /// // your HTTP client's API may vary
222 /// request.with_header("Content-Type", form.content_type_header());
223 /// ```
224 pub fn content_type_header(&self) -> String {
225 format!("multipart/form-data; boundary={}", self.boundary)
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use crate::FormData;
232 use std::ffi::OsString;
233 use std::io::Cursor;
234 use std::path::Path;
235
236 /// This test uses a `multipart/form-data` document generated by Firefox as a test case.
237 #[test]
238 fn smoke_test() {
239 const CORRECT: &[u8] = include_bytes!(concat!(
240 env!("CARGO_MANIFEST_DIR"),
241 "/testdata/form-data.bin"
242 ));
243 const CORRO: &str =
244 include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/testdata/corro.svg"));
245 const TEXT_A: &str =
246 include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/testdata/text-a.txt"));
247 const TEXT_B: &str =
248 include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/testdata/text-b.txt"));
249
250 let mut form = FormData::new(Vec::new());
251 assert_eq!(form.boundary.len(), 68);
252 assert_eq!(form.boundary[..(36)], "-".repeat(36));
253 // cheat and use the boundary Firefox generated
254 form.boundary = "---------------------------20598614689265574691413388431".to_owned();
255
256 form.write_path(
257 "file-a",
258 Path::new(env!("CARGO_MANIFEST_DIR"))
259 .join("testdata")
260 .join("rustacean-flat-noshadow.png"),
261 "image/png",
262 )
263 .unwrap();
264
265 form.write_field("text-a", TEXT_A.trim()).unwrap();
266
267 form.write_file(
268 "file-b",
269 &mut Cursor::new(CORRO.as_bytes()),
270 Some(&OsString::from("corro.svg")),
271 "image/svg+xml",
272 )
273 .unwrap();
274
275 form.write_field("text-b", TEXT_B.trim()).unwrap();
276
277 assert_eq!(form.finish().unwrap(), CORRECT);
278 }
279}