├── .gitignore ├── .travis.yml ├── Cargo.toml ├── LICENSE-MIT ├── README.md └── src ├── error.rs ├── form_data.rs ├── lib.rs └── mock.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "formdata" 3 | version = "0.14.0-unstable" 4 | description = "Parsing of multipart/form-data" 5 | authors = [ "Mike Dilger " ] 6 | readme = "README.md" 7 | repository = "https://github.com/mikedilger/formdata" 8 | documentation = "https://mikedilger.github.io/formdata" 9 | license = "MIT" 10 | keywords = ["multipart", "form-data", "hyper", "http", "mime"] 11 | 12 | [dev-dependencies] 13 | tempdir = "0.3" 14 | 15 | [dependencies] 16 | hyper = { version = "0.10" } 17 | httparse = "1.2" 18 | mime = "0.2" 19 | textnonce = "1.0" 20 | log = "0.4" 21 | encoding = "0.2" 22 | clippy = { version = "0.0", optional = true } 23 | mime_multipart = "0.6" 24 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Michael Dilger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # formdata 2 | 3 | [![Build Status](https://travis-ci.org/mikedilger/formdata.svg?branch=master)](https://travis-ci.org/mikedilger/formdata) 4 | [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) 5 | 6 | THIS LIBRARY AND REPOSITORY ARE UNMAINTAINED 7 | 8 | Documentation is available at https://mikedilger.github.io/formdata 9 | 10 | This library provides a type for storing `multipart/form-data` data, as well as functions 11 | to stream (read or write) such data over HTTP. 12 | 13 | `multipart/form-data` format as described by [RFC 7578](https://tools.ietf.org/html/rfc7578). 14 | HTML forms with enctype=`multipart/form-data` `POST` their data in this format. 15 | This `enctype` is typically used whenever a form has file upload input fields, 16 | as the default `application/x-www-form-urlencoded` cannot handle file uploads. 17 | 18 | Whether reading from a stream or writing out to a stream, files are never stored entirely 19 | in memory, but instead streamed through a buffer. 20 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | // Copyright © 2015 by Michael Dilger (of New Zealand) 2 | // This code is licensed under the MIT license (see LICENSE-MIT for details) 3 | 4 | use std::borrow::Cow; 5 | use std::error::Error as StdError; 6 | use std::fmt::{self, Display}; 7 | use std::io; 8 | use std::string::FromUtf8Error; 9 | 10 | use super::{httparse, hyper, mime_multipart}; 11 | 12 | /// An error type for the `formdata` crate. 13 | pub enum Error { 14 | /// The Hyper request did not have a Content-Type header. 15 | NoRequestContentType, 16 | /// The Hyper request Content-Type top-level Mime was not `Multipart`. 17 | NotMultipart, 18 | /// The Hyper request Content-Type sub-level Mime was not `FormData`. 19 | NotFormData, 20 | /// The Content-Type header failed to specify boundary token. 21 | BoundaryNotSpecified, 22 | /// A multipart section contained only partial headers. 23 | PartialHeaders, 24 | /// A multipart section did not have the required Content-Disposition header. 25 | MissingDisposition, 26 | /// A multipart section did not have a valid corresponding Content-Disposition. 27 | InvalidDisposition, 28 | /// A multipart section Content-Disposition header failed to specify a name. 29 | NoName, 30 | /// The request body ended prior to reaching the expected terminating boundary. 31 | Eof, 32 | /// An HTTP parsing error from a multipart section. 33 | Httparse(httparse::Error), 34 | /// An I/O error. 35 | Io(io::Error), 36 | /// An error was returned from Hyper. 37 | Hyper(hyper::Error), 38 | /// An error occurred during UTF-8 processing. 39 | Utf8(FromUtf8Error), 40 | /// An error occurred during character decoding 41 | Decoding(Cow<'static, str>), 42 | /// A MIME multipart error 43 | Multipart(mime_multipart::Error), 44 | /// Filepart is not a file 45 | NotAFile, 46 | } 47 | 48 | impl From for Error { 49 | fn from(err: io::Error) -> Error { 50 | Error::Io(err) 51 | } 52 | } 53 | 54 | impl From for Error { 55 | fn from(err: httparse::Error) -> Error { 56 | Error::Httparse(err) 57 | } 58 | } 59 | 60 | impl From for Error { 61 | fn from(err: hyper::Error) -> Error { 62 | Error::Hyper(err) 63 | } 64 | } 65 | 66 | impl From for Error { 67 | fn from(err: FromUtf8Error) -> Error { 68 | Error::Utf8(err) 69 | } 70 | } 71 | 72 | impl From for Error { 73 | fn from(err: mime_multipart::Error) -> Error { 74 | Error::Multipart(err) 75 | } 76 | } 77 | 78 | impl Display for Error { 79 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 80 | match *self { 81 | Error::Httparse(ref e) => 82 | format!("{}: {:?}", self, e).fmt(f), 83 | Error::Io(ref e) => 84 | format!("{}: {}", self, e).fmt(f), 85 | Error::Hyper(ref e) => 86 | format!("{}: {}", self, e).fmt(f), 87 | Error::Utf8(ref e) => 88 | format!("{}: {}", self, e).fmt(f), 89 | Error::Decoding(ref e) => 90 | format!("{}: {}", self, e).fmt(f), 91 | Error::Multipart(ref e) => 92 | format!("{}: {}", self, e).fmt(f), 93 | _ => format!("{}", self).fmt(f), 94 | } 95 | } 96 | } 97 | 98 | impl fmt::Debug for Error { 99 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 100 | write!(f, "{}", self)?; 101 | if self.source().is_some() { 102 | write!(f, ": {:?}", self.source().unwrap())?; // recurse 103 | } 104 | Ok(()) 105 | } 106 | } 107 | 108 | impl StdError for Error { 109 | fn description(&self) -> &str{ 110 | match *self { 111 | Error::NoRequestContentType => "The Hyper request did not have a Content-Type header.", 112 | Error::NotMultipart => 113 | "The Hyper request Content-Type top-level Mime was not multipart.", 114 | Error::NotFormData => 115 | "The Hyper request Content-Type sub-level Mime was not form-data.", 116 | Error::BoundaryNotSpecified => 117 | "The Content-Type header failed to specify a boundary token.", 118 | Error::PartialHeaders => "A multipart section contained only partial headers.", 119 | Error::MissingDisposition => 120 | "A multipart section did not have the required Content-Disposition header.", 121 | Error::InvalidDisposition => 122 | "A multipart section did not have a valid corresponding Content-Disposition.", 123 | Error::NoName => 124 | "A multipart section Content-Disposition header failed to specify a name.", 125 | Error::Eof => 126 | "The request body ended prior to reaching the expected terminating boundary.", 127 | Error::Httparse(_) => 128 | "A parse error occurred while parsing the headers of a multipart section.", 129 | Error::Io(_) => "An I/O error occurred.", 130 | Error::Hyper(_) => "A Hyper error occurred.", 131 | Error::Utf8(_) => "A UTF-8 error occurred.", 132 | Error::Decoding(_) => "A decoding error occurred.", 133 | Error::Multipart(_) => "A MIME multipart error occurred.", 134 | Error::NotAFile => "FilePart is not a file.", 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/form_data.rs: -------------------------------------------------------------------------------- 1 | // Copyright © 2015 by Michael Dilger (of New Zealand) 2 | // This code is licensed under the MIT license (see LICENSE-MIT for details) 3 | 4 | use mime_multipart::{Node, Part, FilePart}; 5 | use hyper::header::{Headers, ContentDisposition, DispositionParam, DispositionType, 6 | ContentType}; 7 | use mime::{Mime, TopLevel, SubLevel}; 8 | use error::Error; 9 | 10 | /// The extracted text fields and uploaded files from a `multipart/form-data` request. 11 | /// 12 | /// Use `parse_multipart` to devise this object from a request. 13 | #[derive(Clone, Debug, PartialEq)] 14 | pub struct FormData { 15 | /// Name-value pairs for plain text fields. Technically, these are form data parts with no 16 | /// filename specified in the part's `Content-Disposition`. 17 | pub fields: Vec<(String, String)>, 18 | /// Name-value pairs for temporary files. Technically, these are form data parts with a filename 19 | /// specified in the part's `Content-Disposition`. 20 | pub files: Vec<(String, FilePart)>, 21 | } 22 | 23 | impl FormData { 24 | pub fn new() -> FormData { 25 | FormData { fields: vec![], files: vec![] } 26 | } 27 | 28 | /// Create a mime-multipart Vec from this FormData 29 | pub fn to_multipart(&self) -> Result, Error> { 30 | // Translate to Nodes 31 | let mut nodes: Vec = Vec::with_capacity(self.fields.len() + self.files.len()); 32 | 33 | for &(ref name, ref value) in &self.fields { 34 | let mut h = Headers::new(); 35 | h.set(ContentType(Mime(TopLevel::Text, SubLevel::Plain, vec![]))); 36 | h.set(ContentDisposition { 37 | disposition: DispositionType::Ext("form-data".to_owned()), 38 | parameters: vec![DispositionParam::Ext("name".to_owned(), name.clone())], 39 | }); 40 | nodes.push( Node::Part( Part { 41 | headers: h, 42 | body: value.as_bytes().to_owned(), 43 | })); 44 | } 45 | 46 | for &(ref name, ref filepart) in &self.files { 47 | let mut filepart = filepart.clone(); 48 | // We leave all headers that the caller specified, except that we rewrite 49 | // Content-Disposition. 50 | while filepart.headers.remove::() { }; 51 | let filename = match filepart.path.file_name() { 52 | Some(fname) => fname.to_string_lossy().into_owned(), 53 | None => return Err(Error::NotAFile), 54 | }; 55 | filepart.headers.set(ContentDisposition { 56 | disposition: DispositionType::Ext("form-data".to_owned()), 57 | parameters: vec![DispositionParam::Ext("name".to_owned(), name.clone()), 58 | DispositionParam::Ext("filename".to_owned(), filename)], 59 | }); 60 | nodes.push( Node::File( filepart ) ); 61 | } 62 | 63 | Ok(nodes) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright © 2015 by Michael Dilger (of New Zealand) 2 | // This code is licensed under the MIT license (see LICENSE-MIT for details) 3 | 4 | //! This library provides a type for storing `multipart/form-data` data, as well as functions 5 | //! to stream (read or write) such data over HTTP. 6 | //! 7 | //! `multipart/form-data` format as described by [RFC 7578](https://tools.ietf.org/html/rfc7578). 8 | //! HTML forms with enctype=`multipart/form-data` `POST` their data in this format. 9 | //! This `enctype` is typically used whenever a form has file upload input fields, 10 | //! as the default `application/x-www-form-urlencoded` cannot handle file uploads. 11 | //! 12 | //! Whether reading from a stream or writing out to a stream, files are never stored entirely 13 | //! in memory, but instead streamed through a buffer. 14 | //! 15 | //! ## Read Example 16 | //! 17 | //! ```no_run 18 | //! extern crate mime; 19 | //! extern crate hyper; 20 | //! extern crate formdata; 21 | //! 22 | //! use hyper::server::{Server, Request, Response}; 23 | //! 24 | //! fn main() { 25 | //! let server = Server::http("0.0.0.0:0").unwrap().handle(handler).unwrap(); 26 | //! } 27 | //! 28 | //! fn handler(hyper_request: Request, res: Response) { 29 | //! let (_, _, headers, _, _, mut reader) = hyper_request.deconstruct(); 30 | //! let form_data = formdata::read_formdata(&mut reader, &headers).unwrap(); 31 | //! 32 | //! for (name, value) in form_data.fields { 33 | //! println!("Posted field name={} value={}", name, value); 34 | //! } 35 | //! 36 | //! for (name, file) in form_data.files { 37 | //! println!("Posted file name={} path={:?}", name, file.path); 38 | //! } 39 | //! } 40 | //! ``` 41 | //! 42 | //! ## Write Example 43 | //! 44 | //! ```no_run 45 | //! extern crate mime; 46 | //! extern crate hyper; 47 | //! extern crate formdata; 48 | //! 49 | //! use std::path::Path; 50 | //! use hyper::header::{Headers, ContentType}; 51 | //! use mime::{Mime, TopLevel, SubLevel}; 52 | //! use formdata::{FormData, FilePart}; 53 | //! 54 | //! fn main() { 55 | //! let mut stream = ::std::io::stdout(); 56 | //! let mut photo_headers = Headers::new(); 57 | //! photo_headers.set(ContentType(Mime(TopLevel::Image, SubLevel::Gif, vec![]))); 58 | //! // no need to set Content-Disposition (in fact it will be rewritten) 59 | //! 60 | //! let formdata = FormData { 61 | //! fields: vec![ ("name".to_owned(), "Baxter".to_owned()), 62 | //! ("age".to_owned(), "1 month".to_owned()) ], 63 | //! files: vec![ ("photo".to_owned(), FilePart::new( 64 | //! photo_headers, Path::new("/tmp/puppy.gif"))) ], 65 | //! }; 66 | //! 67 | //! let boundary = formdata::generate_boundary(); 68 | //! let count = formdata::write_formdata(&mut stream, &boundary, &formdata).unwrap(); 69 | //! println!("COUNT = {}", count); 70 | //! } 71 | //! ``` 72 | 73 | #![cfg_attr(feature="clippy", feature(plugin))] 74 | #![cfg_attr(feature="clippy", plugin(clippy))] 75 | 76 | extern crate httparse; 77 | extern crate hyper; 78 | #[cfg_attr(test, macro_use)] 79 | extern crate mime; 80 | extern crate textnonce; 81 | extern crate log; 82 | extern crate encoding; 83 | 84 | extern crate mime_multipart; 85 | 86 | mod error; 87 | mod form_data; 88 | #[cfg(test)] 89 | mod mock; 90 | 91 | pub use error::Error; 92 | pub use form_data::FormData; 93 | 94 | use std::io::{Read, Write}; 95 | use hyper::header::{Headers, ContentDisposition, DispositionParam}; 96 | use mime_multipart::Node; 97 | pub use mime_multipart::FilePart; 98 | pub use mime_multipart::generate_boundary; 99 | 100 | /// Parse MIME `multipart/form-data` information from a stream as a `FormData`. 101 | pub fn read_formdata(stream: &mut S, headers: &Headers) -> Result 102 | { 103 | let nodes = mime_multipart::read_multipart_body(stream, headers, false)?; 104 | 105 | let mut formdata = FormData::new(); 106 | fill_formdata(&mut formdata, nodes)?; 107 | Ok(formdata) 108 | } 109 | 110 | // order and nesting are irrelevant, so we interate through the nodes and put them 111 | // into one of two buckets (fields and files); If a multipart node is found, it uses 112 | // the name in its headers as the key (rather than the name in the headers of the 113 | // subparts), which is how multiple file uploads work. 114 | fn fill_formdata(formdata: &mut FormData, nodes: Vec) -> Result<(), Error> 115 | { 116 | for node in nodes { 117 | match node { 118 | Node::Part(part) => { 119 | let cd_name: Option = { 120 | let cd: &ContentDisposition = match part.headers.get() { 121 | Some(cd) => cd, 122 | None => return Err(Error::MissingDisposition), 123 | }; 124 | get_content_disposition_name(&cd) 125 | }; 126 | let key = cd_name.ok_or(Error::NoName)?; 127 | let val = String::from_utf8(part.body)?; 128 | formdata.fields.push((key, val)); 129 | }, 130 | Node::File(part) => { 131 | let cd_name: Option = { 132 | let cd: &ContentDisposition = match part.headers.get() { 133 | Some(cd) => cd, 134 | None => return Err(Error::MissingDisposition), 135 | }; 136 | get_content_disposition_name(&cd) 137 | }; 138 | let key = cd_name.ok_or(Error::NoName)?; 139 | formdata.files.push((key, part)); 140 | } 141 | Node::Multipart((headers, nodes)) => { 142 | let cd_name: Option = { 143 | let cd: &ContentDisposition = match headers.get() { 144 | Some(cd) => cd, 145 | None => return Err(Error::MissingDisposition), 146 | }; 147 | get_content_disposition_name(&cd) 148 | }; 149 | let key = cd_name.ok_or(Error::NoName)?; 150 | for node in nodes { 151 | match node { 152 | Node::Part(part) => { 153 | let val = String::from_utf8(part.body)?; 154 | formdata.fields.push((key.clone(), val)); 155 | }, 156 | Node::File(part) => { 157 | formdata.files.push((key.clone(), part)); 158 | }, 159 | _ => { } // don't recurse deeper 160 | } 161 | } 162 | } 163 | } 164 | } 165 | Ok(()) 166 | } 167 | 168 | #[inline] 169 | fn get_content_disposition_name(cd: &ContentDisposition) -> Option { 170 | if let Some(&DispositionParam::Ext(_, ref value)) = cd.parameters.iter() 171 | .find(|&x| match *x { 172 | DispositionParam::Ext(ref token,_) => &*token == "name", 173 | _ => false, 174 | }) 175 | { 176 | Some(value.clone()) 177 | } else { 178 | None 179 | } 180 | } 181 | 182 | 183 | /// Stream out `multipart/form-data` body content matching the passed in `formdata`. This 184 | /// does not stream out headers, so the caller must stream those out before calling 185 | /// write_formdata(). 186 | pub fn write_formdata(stream: &mut S, boundary: &Vec, formdata: &FormData) 187 | -> Result 188 | { 189 | let nodes = formdata.to_multipart()?; 190 | 191 | // Write out 192 | let count = ::mime_multipart::write_multipart(stream, boundary, &nodes)?; 193 | 194 | Ok(count) 195 | } 196 | 197 | /// Stream out `multipart/form-data` body content matching the passed in `formdata` as 198 | /// Transfer-Encoding: Chunked. This does not stream out headers, so the caller must stream 199 | /// those out before calling write_formdata(). 200 | pub fn write_formdata_chunked(stream: &mut S, boundary: &Vec, formdata: &FormData) 201 | -> Result<(), Error> 202 | { 203 | let nodes = formdata.to_multipart()?; 204 | 205 | // Write out 206 | ::mime_multipart::write_multipart_chunked(stream, boundary, &nodes)?; 207 | 208 | Ok(()) 209 | } 210 | 211 | 212 | #[cfg(test)] 213 | mod tests { 214 | extern crate tempdir; 215 | 216 | use super::{FormData, read_formdata, write_formdata, write_formdata_chunked, 217 | FilePart, generate_boundary}; 218 | 219 | use std::net::SocketAddr; 220 | use std::fs::File; 221 | use std::io::Write; 222 | 223 | use hyper::buffer::BufReader; 224 | use hyper::net::NetworkStream; 225 | use hyper::server::Request as HyperRequest; 226 | use hyper::header::{Headers, ContentDisposition, DispositionParam, ContentType, 227 | DispositionType}; 228 | use mime::{Mime, TopLevel, SubLevel}; 229 | 230 | use mock::MockStream; 231 | 232 | #[test] 233 | fn parser() { 234 | let input = b"POST / HTTP/1.1\r\n\ 235 | Host: example.domain\r\n\ 236 | Content-Type: multipart/form-data; boundary=\"abcdefg\"\r\n\ 237 | Content-Length: 1000\r\n\ 238 | \r\n\ 239 | --abcdefg\r\n\ 240 | Content-Disposition: form-data; name=\"field1\"\r\n\ 241 | \r\n\ 242 | data1\r\n\ 243 | --abcdefg\r\n\ 244 | Content-Disposition: form-data; name=\"field2\"; filename=\"image.gif\"\r\n\ 245 | Content-Type: image/gif\r\n\ 246 | \r\n\ 247 | This is a file\r\n\ 248 | with two lines\r\n\ 249 | --abcdefg\r\n\ 250 | Content-Disposition: form-data; name=\"field3\"; filename=\"file.txt\"\r\n\ 251 | \r\n\ 252 | This is a file\r\n\ 253 | --abcdefg--"; 254 | 255 | let mut mock = MockStream::with_input(input); 256 | 257 | let mock: &mut dyn NetworkStream = &mut mock; 258 | let mut stream = BufReader::new(mock); 259 | let sock: SocketAddr = "127.0.0.1:80".parse().unwrap(); 260 | let req = HyperRequest::new(&mut stream, sock).unwrap(); 261 | let (_, _, headers, _, _, mut reader) = req.deconstruct(); 262 | 263 | match read_formdata(&mut reader, &headers) { 264 | Ok(form_data) => { 265 | assert_eq!(form_data.fields.len(), 1); 266 | for (key, val) in form_data.fields { 267 | if &key == "field1" { 268 | assert_eq!(&val, "data1"); 269 | } 270 | } 271 | 272 | assert_eq!(form_data.files.len(), 2); 273 | for (key, file) in form_data.files { 274 | if &key == "field2" { 275 | assert_eq!(file.size, Some(30)); 276 | assert_eq!(&*file.filename().unwrap().unwrap(), "image.gif"); 277 | assert_eq!(file.content_type().unwrap(), mime!(Image/Gif)); 278 | } else if &key == "field3" { 279 | assert_eq!(file.size, Some(14)); 280 | assert_eq!(&*file.filename().unwrap().unwrap(), "file.txt"); 281 | assert!(file.content_type().is_none()); 282 | } 283 | } 284 | }, 285 | Err(err) => panic!("{}", err), 286 | } 287 | } 288 | 289 | #[test] 290 | fn multi_file_parser() { 291 | let input = b"POST / HTTP/1.1\r\n\ 292 | Host: example.domain\r\n\ 293 | Content-Type: multipart/form-data; boundary=\"abcdefg\"\r\n\ 294 | Content-Length: 1000\r\n\ 295 | \r\n\ 296 | --abcdefg\r\n\ 297 | Content-Disposition: form-data; name=\"field1\"\r\n\ 298 | \r\n\ 299 | data1\r\n\ 300 | --abcdefg\r\n\ 301 | Content-Disposition: form-data; name=\"field2\"; filename=\"image.gif\"\r\n\ 302 | Content-Type: image/gif\r\n\ 303 | \r\n\ 304 | This is a file\r\n\ 305 | with two lines\r\n\ 306 | --abcdefg\r\n\ 307 | Content-Disposition: form-data; name=\"field2\"; filename=\"file.txt\"\r\n\ 308 | \r\n\ 309 | This is a file\r\n\ 310 | --abcdefg--"; 311 | 312 | let mut mock = MockStream::with_input(input); 313 | 314 | let mock: &mut dyn NetworkStream = &mut mock; 315 | let mut stream = BufReader::new(mock); 316 | let sock: SocketAddr = "127.0.0.1:80".parse().unwrap(); 317 | let req = HyperRequest::new(&mut stream, sock).unwrap(); 318 | let (_, _, headers, _, _, mut reader) = req.deconstruct(); 319 | 320 | match read_formdata(&mut reader, &headers) { 321 | Ok(form_data) => { 322 | assert_eq!(form_data.fields.len(), 1); 323 | for (key, val) in form_data.fields { 324 | if &key == "field1" { 325 | assert_eq!(&val, "data1"); 326 | } 327 | } 328 | 329 | assert_eq!(form_data.files.len(), 2); 330 | let (ref key, ref file) = form_data.files[0]; 331 | 332 | assert_eq!(key, "field2"); 333 | assert_eq!(file.size, Some(30)); 334 | assert_eq!(&*file.filename().unwrap().unwrap(), "image.gif"); 335 | assert_eq!(file.content_type().unwrap(), mime!(Image/Gif)); 336 | 337 | let (ref key, ref file) = form_data.files[1]; 338 | assert!(key == "field2"); 339 | assert_eq!(file.size, Some(14)); 340 | assert_eq!(&*file.filename().unwrap().unwrap(), "file.txt"); 341 | assert!(file.content_type().is_none()); 342 | 343 | }, 344 | Err(err) => panic!("{}", err), 345 | } 346 | } 347 | 348 | #[test] 349 | fn mixed_parser() { 350 | let input = b"POST / HTTP/1.1\r\n\ 351 | Host: example.domain\r\n\ 352 | Content-Type: multipart/form-data; boundary=AaB03x\r\n\ 353 | Content-Length: 1000\r\n\ 354 | \r\n\ 355 | --AaB03x\r\n\ 356 | Content-Disposition: form-data; name=\"submit-name\"\r\n\ 357 | \r\n\ 358 | Larry\r\n\ 359 | --AaB03x\r\n\ 360 | Content-Disposition: form-data; name=\"files\"\r\n\ 361 | Content-Type: multipart/mixed; boundary=BbC04y\r\n\ 362 | \r\n\ 363 | --BbC04y\r\n\ 364 | Content-Disposition: file; filename=\"file1.txt\"\r\n\ 365 | \r\n\ 366 | ... contents of file1.txt ...\r\n\ 367 | --BbC04y\r\n\ 368 | Content-Disposition: file; filename=\"awesome_image.gif\"\r\n\ 369 | Content-Type: image/gif\r\n\ 370 | Content-Transfer-Encoding: binary\r\n\ 371 | \r\n\ 372 | ... contents of awesome_image.gif ...\r\n\ 373 | --BbC04y--\r\n\ 374 | --AaB03x--"; 375 | 376 | let mut mock = MockStream::with_input(input); 377 | 378 | let mock: &mut dyn NetworkStream = &mut mock; 379 | let mut stream = BufReader::new(mock); 380 | let sock: SocketAddr = "127.0.0.1:80".parse().unwrap(); 381 | let req = HyperRequest::new(&mut stream, sock).unwrap(); 382 | let (_, _, headers, _, _, mut reader) = req.deconstruct(); 383 | 384 | match read_formdata(&mut reader, &headers) { 385 | Ok(form_data) => { 386 | assert_eq!(form_data.fields.len(), 1); 387 | for (key, val) in form_data.fields { 388 | if &key == "submit-name" { 389 | assert_eq!(&val, "Larry"); 390 | } 391 | } 392 | 393 | assert_eq!(form_data.files.len(), 2); 394 | for (key, file) in form_data.files { 395 | assert_eq!(&key, "files"); 396 | match &file.filename().unwrap().unwrap()[..] { 397 | "file1.txt" => { 398 | assert_eq!(file.size, Some(29)); 399 | assert!(file.content_type().is_none()); 400 | } 401 | "awesome_image.gif" => { 402 | assert_eq!(file.size, Some(37)); 403 | assert_eq!(file.content_type().unwrap(), mime!(Image/Gif)); 404 | }, 405 | _ => unreachable!(), 406 | } 407 | } 408 | }, 409 | Err(err) => panic!("{}", err), 410 | } 411 | } 412 | 413 | #[test] 414 | fn simple_writer() { 415 | // Create a simple short file for testing 416 | let tmpdir = tempdir::TempDir::new("formdata_test").unwrap(); 417 | let tmppath = tmpdir.path().join("testfile"); 418 | let mut tmpfile = File::create(tmppath.clone()).unwrap(); 419 | writeln!(tmpfile, "this is example file content").unwrap(); 420 | 421 | let mut photo_headers = Headers::new(); 422 | photo_headers.set(ContentType(Mime(TopLevel::Image, SubLevel::Gif, vec![]))); 423 | photo_headers.set(ContentDisposition { 424 | disposition: DispositionType::Ext("form-data".to_owned()), 425 | parameters: vec![DispositionParam::Ext("name".to_owned(), "photo".to_owned()), 426 | DispositionParam::Ext("filename".to_owned(), "mike.gif".to_owned())], 427 | }); 428 | 429 | let formdata = FormData { 430 | fields: vec![ ("name".to_owned(), "Mike".to_owned()), 431 | ("age".to_owned(), "46".to_owned()) ], 432 | files: vec![ ("photo".to_owned(), FilePart::new(photo_headers, &tmppath)) ], 433 | }; 434 | 435 | let mut output: Vec = Vec::new(); 436 | let boundary = generate_boundary(); 437 | match write_formdata(&mut output, &boundary, &formdata) { 438 | Ok(count) => assert_eq!(count, 568), 439 | Err(e) => panic!("Unable to write formdata: {}", e), 440 | } 441 | 442 | println!("{}", String::from_utf8_lossy(&output)); 443 | } 444 | 445 | 446 | #[test] 447 | fn chunked_writer() { 448 | // Create a simple short file for testing 449 | let tmpdir = tempdir::TempDir::new("formdata_test").unwrap(); 450 | let tmppath = tmpdir.path().join("testfile"); 451 | let mut tmpfile = File::create(tmppath.clone()).unwrap(); 452 | writeln!(tmpfile, "this is example file content").unwrap(); 453 | 454 | let mut photo_headers = Headers::new(); 455 | photo_headers.set(ContentType(Mime(TopLevel::Image, SubLevel::Gif, vec![]))); 456 | photo_headers.set(ContentDisposition { 457 | disposition: DispositionType::Ext("form-data".to_owned()), 458 | parameters: vec![DispositionParam::Ext("name".to_owned(), "photo".to_owned()), 459 | DispositionParam::Ext("filename".to_owned(), "mike.gif".to_owned())], 460 | }); 461 | 462 | let formdata = FormData { 463 | fields: vec![ ("name".to_owned(), "Mike".to_owned()), 464 | ("age".to_owned(), "46".to_owned()) ], 465 | files: vec![ ("photo".to_owned(), FilePart::new(photo_headers, &tmppath)) ], 466 | }; 467 | 468 | let mut output: Vec = Vec::new(); 469 | let boundary = generate_boundary(); 470 | assert!(write_formdata_chunked(&mut output, &boundary, &formdata).is_ok()); 471 | println!("{}", String::from_utf8_lossy(&output)); 472 | } 473 | } 474 | -------------------------------------------------------------------------------- /src/mock.rs: -------------------------------------------------------------------------------- 1 | //! Code taken from Hyper, stripped down and with modification. 2 | //! 3 | //! See [https://github.com/hyperium/hyper](Hyper) for more information 4 | 5 | // Copyright (c) 2014 Sean McArthur 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | use std::fmt; 26 | use std::io::{self, Read, Write, Cursor}; 27 | use std::net::SocketAddr; 28 | use std::time::Duration; 29 | 30 | use hyper::net::NetworkStream; 31 | 32 | pub struct MockStream { 33 | pub read: Cursor>, 34 | pub write: Vec, 35 | } 36 | 37 | impl Clone for MockStream { 38 | fn clone(&self) -> MockStream { 39 | MockStream { 40 | read: Cursor::new(self.read.get_ref().clone()), 41 | write: self.write.clone(), 42 | } 43 | } 44 | } 45 | 46 | impl fmt::Debug for MockStream { 47 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 48 | write!(f, "MockStream {{ read: {:?}, write: {:?} }}", self.read.get_ref(), self.write) 49 | } 50 | } 51 | 52 | impl PartialEq for MockStream { 53 | fn eq(&self, other: &MockStream) -> bool { 54 | self.read.get_ref() == other.read.get_ref() && self.write == other.write 55 | } 56 | } 57 | 58 | impl MockStream { 59 | #[allow(dead_code)] 60 | pub fn with_input(input: &[u8]) -> MockStream { 61 | MockStream { 62 | read: Cursor::new(input.to_vec()), 63 | write: vec![], 64 | } 65 | } 66 | } 67 | 68 | impl Read for MockStream { 69 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 70 | self.read.read(buf) 71 | } 72 | } 73 | 74 | impl Write for MockStream { 75 | fn write(&mut self, msg: &[u8]) -> io::Result { 76 | Write::write(&mut self.write, msg) 77 | } 78 | 79 | fn flush(&mut self) -> io::Result<()> { 80 | Ok(()) 81 | } 82 | } 83 | 84 | impl NetworkStream for MockStream { 85 | fn peer_addr(&mut self) -> io::Result { 86 | Ok("127.0.0.1:1337".parse().unwrap()) 87 | } 88 | 89 | fn set_read_timeout(&self, _: Option) -> io::Result<()> { 90 | Ok(()) 91 | } 92 | 93 | fn set_write_timeout(&self, _: Option) -> io::Result<()> { 94 | Ok(()) 95 | } 96 | } 97 | --------------------------------------------------------------------------------