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}