From 2143aad3cdabbd2e75f257224cfeb14dacafd645 Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Wed, 29 Aug 2018 18:22:55 -0700 Subject: [PATCH] pub(crate)ify the multipart facade --- src/lib.rs | 1 - src/multipart.rs | 422 +++++++++++++++++++++++++++++++++++++++++++- src/multipart_.rs | 436 ---------------------------------------------- src/request.rs | 8 +- 4 files changed, 424 insertions(+), 443 deletions(-) delete mode 100644 src/multipart_.rs diff --git a/src/lib.rs b/src/lib.rs index 7431d64..d4b26b3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -179,7 +179,6 @@ mod connect_async; mod body; mod client; mod into_url; -mod multipart_; mod proxy; mod redirect; mod request; diff --git a/src/multipart.rs b/src/multipart.rs index bd0321e..ac0a28f 100644 --- a/src/multipart.rs +++ b/src/multipart.rs @@ -1,2 +1,420 @@ -//! # multipart/form-data -pub use ::multipart_::{Form, Part}; +//! multipart/form-data +use std::borrow::Cow; +use std::fmt; +use std::fs::File; +use std::io::{self, Cursor, Read}; +use std::path::Path; + +use mime_guess::{self, Mime}; +use url::percent_encoding; +use uuid::Uuid; + +use {Body}; + +/// A multipart/form-data request. +pub struct Form { + boundary: String, + fields: Vec<(Cow<'static, str>, Part)>, + headers: Vec, +} + +impl Form { + /// Creates a new Form without any content. + pub fn new() -> Form { + Form { + boundary: format!("{}", Uuid::new_v4().simple()), + fields: Vec::new(), + headers: Vec::new(), + } + } + + /// Get the boundary that this form will use. + #[inline] + pub fn boundary(&self) -> &str { + &self.boundary + } + + /// Add a data field with supplied name and value. + /// + /// # Examples + /// + /// ``` + /// let form = reqwest::multipart::Form::new() + /// .text("username", "seanmonstar") + /// .text("password", "secret"); + /// ``` + pub fn text(self, name: T, value: U) -> Form + where T: Into>, + U: Into>, + { + self.part(name, Part::text(value)) + } + + /// Adds a file field. + /// + /// The path will be used to try to guess the filename and mime. + /// + /// # Examples + /// + /// ```no_run + /// # fn run() -> ::std::io::Result<()> { + /// let files = reqwest::multipart::Form::new() + /// .file("key", "/path/to/file")?; + /// # Ok(()) + /// # } + /// ``` + /// + /// # Errors + /// + /// Errors when the file cannot be opened. + pub fn file(self, name: T, path: U) -> io::Result
+ where T: Into>, + U: AsRef + { + Ok(self.part(name, Part::file(path)?)) + } + + /// Adds a customized Part. + pub fn part(mut self, name: T, part: Part) -> Form + where T: Into>, + { + self.fields.push((name.into(), part)); + self + } + + pub(crate) fn reader(self) -> Reader { + Reader::new(self) + } + + // If predictable, computes the length the request will have + // The length should be preditable if only String and file fields have been added, + // but not if a generic reader has been added; + pub(crate) fn compute_length(&mut self) -> Option { + let mut length = 0u64; + for &(ref name, ref field) in self.fields.iter() { + match ::body::len(&field.value) { + Some(value_length) => { + // We are constructing the header just to get its length. To not have to + // construct it again when the request is sent we cache these headers. + let header = header(name, field); + let header_length = header.len(); + self.headers.push(header); + // The additions mimick the format string out of which the field is constructed + // in Reader. Not the cleanest solution because if that format string is + // ever changed then this formula needs to be changed too which is not an + // obvious dependency in the code. + length += 2 + self.boundary.len() as u64 + 2 + header_length as u64 + 4 + value_length + 2 + } + _ => return None, + } + } + // If there is a at least one field there is a special boundary for the very last field. + if self.fields.len() != 0 { + length += 2 + self.boundary.len() as u64 + 4 + } + Some(length) + } +} + +impl fmt::Debug for Form { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("Form") + .field("boundary", &self.boundary) + .field("parts", &self.fields) + .finish() + } +} + + +/// A field in a multipart form. +pub struct Part { + value: Body, + mime: Option, + file_name: Option>, +} + +impl Part { + /// Makes a text parameter. + pub fn text(value: T) -> Part + where T: Into>, + { + let body = match value.into() { + Cow::Borrowed(slice) => Body::from(slice), + Cow::Owned(string) => Body::from(string), + }; + Part::new(body) + } + + /// Adds a generic reader. + /// + /// Does not set filename or mime. + pub fn reader(value: T) -> Part { + Part::new(Body::new(value)) + } + + /// Adds a generic reader with known length. + /// + /// Does not set filename or mime. + pub fn reader_with_length(value: T, length: u64) -> Part { + Part::new(Body::sized(value, length)) + } + + /// Makes a file parameter. + /// + /// # Errors + /// + /// Errors when the file cannot be opened. + pub fn file>(path: T) -> io::Result { + let path = path.as_ref(); + let file_name = path.file_name().and_then(|filename| { + Some(Cow::from(filename.to_string_lossy().into_owned())) + }); + let ext = path.extension() + .and_then(|ext| ext.to_str()) + .unwrap_or(""); + let mime = mime_guess::get_mime_type(ext); + let file = File::open(path)?; + let mut field = Part::new(Body::from(file)); + field.mime = Some(mime); + field.file_name = file_name; + Ok(field) + } + + fn new(value: Body) -> Part { + Part { + value: value, + mime: None, + file_name: None, + } + } + + /// Sets the mime, builder style. + pub fn mime(mut self, mime: Mime) -> Part { + self.mime = Some(mime); + self + } + + /// Sets the filename, builder style. + pub fn file_name>>(mut self, filename: T) -> Part { + self.file_name = Some(filename.into()); + self + } +} + +impl fmt::Debug for Part { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("Part") + .field("value", &self.value) + .field("mime", &self.mime) + .field("file_name", &self.file_name) + .finish() + } +} + +pub(crate) struct Reader { + form: Form, + active_reader: Option>, +} + +impl fmt::Debug for Reader { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("Reader") + .field("form", &self.form) + .finish() + } +} + +impl Reader { + fn new(form: Form) -> Reader { + let mut reader = Reader { + form: form, + active_reader: None, + }; + reader.next_reader(); + reader + } + + fn next_reader(&mut self) { + self.active_reader = if self.form.fields.len() != 0 { + // We need to move out of the vector here because we are consuming the field's reader + let (name, field) = self.form.fields.remove(0); + let reader = Cursor::new(format!( + "--{}\r\n{}\r\n\r\n", + self.form.boundary, + // Try to use cached headers created by compute_length + if self.form.headers.len() > 0 { + self.form.headers.remove(0) + } else { + header(&name, &field) + } + )).chain(::body::reader(field.value)) + .chain(Cursor::new("\r\n")); + // According to https://tools.ietf.org/html/rfc2046#section-5.1.1 + // the very last field has a special boundary + if self.form.fields.len() != 0 { + Some(Box::new(reader)) + } else { + Some(Box::new(reader.chain(Cursor::new( + format!("--{}--\r\n", self.form.boundary), + )))) + } + } else { + None + } + } +} + +impl Read for Reader { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + let mut total_bytes_read = 0usize; + let mut last_read_bytes; + loop { + match self.active_reader { + Some(ref mut reader) => { + last_read_bytes = reader.read(&mut buf[total_bytes_read..])?; + total_bytes_read += last_read_bytes; + if total_bytes_read == buf.len() { + return Ok(total_bytes_read); + } + } + None => return Ok(total_bytes_read), + }; + if last_read_bytes == 0 && buf.len() != 0 { + self.next_reader(); + } + } + } +} + + +fn header(name: &str, field: &Part) -> String { + format!( + "Content-Disposition: form-data; {}{}{}", + format_parameter("name", name), + match field.file_name { + Some(ref file_name) => format!("; {}", format_parameter("filename", file_name)), + None => String::new(), + }, + match field.mime { + Some(ref mime) => format!("\r\nContent-Type: {}", mime), + None => "".to_string(), + } + ) +} + +fn format_parameter(name: &str, value: &str) -> String { + let legal_value = + percent_encoding::utf8_percent_encode(value, percent_encoding::PATH_SEGMENT_ENCODE_SET) + .to_string(); + if value.len() == legal_value.len() { + // nothing has been percent encoded + format!("{}=\"{}\"", name, value) + } else { + // something has been percent encoded + format!("{}*=utf-8''{}", name, legal_value) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn form_empty() { + let mut output = Vec::new(); + let mut form = Form::new(); + let length = form.compute_length(); + form.reader().read_to_end(&mut output).unwrap(); + assert_eq!(output, b""); + assert_eq!(length.unwrap(), 0); + } + + #[test] + fn read_to_end() { + let mut output = Vec::new(); + let mut form = Form::new() + .part("reader1", Part::reader(::std::io::empty())) + .part("key1", Part::text("value1")) + .part( + "key2", + Part::text("value2").mime(::mime::IMAGE_BMP), + ) + .part("reader2", Part::reader(::std::io::empty())) + .part( + "key3", + Part::text("value3").file_name("filename"), + ); + form.boundary = "boundary".to_string(); + let length = form.compute_length(); + let expected = "--boundary\r\n\ + Content-Disposition: form-data; name=\"reader1\"\r\n\r\n\ + \r\n\ + --boundary\r\n\ + Content-Disposition: form-data; name=\"key1\"\r\n\r\n\ + value1\r\n\ + --boundary\r\n\ + Content-Disposition: form-data; name=\"key2\"\r\n\ + Content-Type: image/bmp\r\n\r\n\ + value2\r\n\ + --boundary\r\n\ + Content-Disposition: form-data; name=\"reader2\"\r\n\r\n\ + \r\n\ + --boundary\r\n\ + Content-Disposition: form-data; name=\"key3\"; filename=\"filename\"\r\n\r\n\ + value3\r\n--boundary--\r\n"; + form.reader().read_to_end(&mut output).unwrap(); + // These prints are for debug purposes in case the test fails + println!( + "START REAL\n{}\nEND REAL", + ::std::str::from_utf8(&output).unwrap() + ); + println!("START EXPECTED\n{}\nEND EXPECTED", expected); + assert_eq!(::std::str::from_utf8(&output).unwrap(), expected); + assert!(length.is_none()); + } + + #[test] + fn read_to_end_with_length() { + let mut output = Vec::new(); + let mut form = Form::new() + .text("key1", "value1") + .part( + "key2", + Part::text("value2").mime(::mime::IMAGE_BMP), + ) + .part( + "key3", + Part::text("value3").file_name("filename"), + ); + form.boundary = "boundary".to_string(); + let length = form.compute_length(); + let expected = "--boundary\r\n\ + Content-Disposition: form-data; name=\"key1\"\r\n\r\n\ + value1\r\n\ + --boundary\r\n\ + Content-Disposition: form-data; name=\"key2\"\r\n\ + Content-Type: image/bmp\r\n\r\n\ + value2\r\n\ + --boundary\r\n\ + Content-Disposition: form-data; name=\"key3\"; filename=\"filename\"\r\n\r\n\ + value3\r\n--boundary--\r\n"; + form.reader().read_to_end(&mut output).unwrap(); + // These prints are for debug purposes in case the test fails + println!( + "START REAL\n{}\nEND REAL", + ::std::str::from_utf8(&output).unwrap() + ); + println!("START EXPECTED\n{}\nEND EXPECTED", expected); + assert_eq!(::std::str::from_utf8(&output).unwrap(), expected); + assert_eq!(length.unwrap(), expected.len() as u64); + } + + #[test] + fn header_percent_encoding() { + let name = "start%'\"\r\nßend"; + let field = Part::text(""); + let expected = "Content-Disposition: form-data; name*=utf-8''start%25\'%22%0D%0A%C3%9Fend"; + + assert_eq!(header(name, &field), expected); + } +} diff --git a/src/multipart_.rs b/src/multipart_.rs deleted file mode 100644 index 552150a..0000000 --- a/src/multipart_.rs +++ /dev/null @@ -1,436 +0,0 @@ -use std::borrow::Cow; -use std::fmt; -use std::fs::File; -use std::io::{self, Cursor, Read}; -use std::path::Path; - -use mime_guess::{self, Mime}; -use url::percent_encoding; -use uuid::Uuid; - -use {Body}; - -/// A multipart/form-data request. -pub struct Form { - boundary: String, - fields: Vec<(Cow<'static, str>, Part)>, - headers: Vec, -} - -impl Form { - /// Creates a new Form without any content. - pub fn new() -> Form { - Form { - boundary: format!("{}", Uuid::new_v4().simple()), - fields: Vec::new(), - headers: Vec::new(), - } - } - - /// Get the boundary that this form will use. - #[inline] - pub fn boundary(&self) -> &str { - &self.boundary - } - - /// Add a data field with supplied name and value. - /// - /// # Examples - /// - /// ``` - /// let form = reqwest::multipart::Form::new() - /// .text("username", "seanmonstar") - /// .text("password", "secret"); - /// ``` - pub fn text(self, name: T, value: U) -> Form - where T: Into>, - U: Into>, - { - self.part(name, Part::text(value)) - } - - /// Adds a file field. - /// - /// The path will be used to try to guess the filename and mime. - /// - /// # Examples - /// - /// ```no_run - /// # fn run() -> ::std::io::Result<()> { - /// let files = reqwest::multipart::Form::new() - /// .file("key", "/path/to/file")?; - /// # Ok(()) - /// # } - /// ``` - /// - /// # Errors - /// - /// Errors when the file cannot be opened. - pub fn file(self, name: T, path: U) -> io::Result - where T: Into>, - U: AsRef - { - Ok(self.part(name, Part::file(path)?)) - } - - /// Adds a customized Part. - pub fn part(mut self, name: T, part: Part) -> Form - where T: Into>, - { - self.fields.push((name.into(), part)); - self - } -} - -impl Form { - fn reader(self) -> Reader { - Reader::new(self) - } - - fn compute_length(&mut self) -> Option { - let mut length = 0u64; - for &(ref name, ref field) in self.fields.iter() { - match ::body::len(&field.value) { - Some(value_length) => { - // We are constructing the header just to get its length. To not have to - // construct it again when the request is sent we cache these headers. - let header = header(name, field); - let header_length = header.len(); - self.headers.push(header); - // The additions mimick the format string out of which the field is constructed - // in Reader. Not the cleanest solution because if that format string is - // ever changed then this formula needs to be changed too which is not an - // obvious dependency in the code. - length += 2 + self.boundary.len() as u64 + 2 + header_length as u64 + 4 + value_length + 2 - } - _ => return None, - } - } - // If there is a at least one field there is a special boundary for the very last field. - if self.fields.len() != 0 { - length += 2 + self.boundary.len() as u64 + 4 - } - Some(length) - } -} - -impl fmt::Debug for Form { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("Form") - .field("boundary", &self.boundary) - .field("parts", &self.fields) - .finish() - } -} - - -/// A field in a multipart form. -pub struct Part { - value: Body, - mime: Option, - file_name: Option>, -} - -impl Part { - /// Makes a text parameter. - pub fn text(value: T) -> Part - where T: Into>, - { - let body = match value.into() { - Cow::Borrowed(slice) => Body::from(slice), - Cow::Owned(string) => Body::from(string), - }; - Part::new(body) - } - - /// Adds a generic reader. - /// - /// Does not set filename or mime. - pub fn reader(value: T) -> Part { - Part::new(Body::new(value)) - } - - /// Adds a generic reader with known length. - /// - /// Does not set filename or mime. - pub fn reader_with_length(value: T, length: u64) -> Part { - Part::new(Body::sized(value, length)) - } - - /// Makes a file parameter. - /// - /// # Errors - /// - /// Errors when the file cannot be opened. - pub fn file>(path: T) -> io::Result { - let path = path.as_ref(); - let file_name = path.file_name().and_then(|filename| { - Some(Cow::from(filename.to_string_lossy().into_owned())) - }); - let ext = path.extension() - .and_then(|ext| ext.to_str()) - .unwrap_or(""); - let mime = mime_guess::get_mime_type(ext); - let file = File::open(path)?; - let mut field = Part::new(Body::from(file)); - field.mime = Some(mime); - field.file_name = file_name; - Ok(field) - } - - fn new(value: Body) -> Part { - Part { - value: value, - mime: None, - file_name: None, - } - } - - /// Sets the mime, builder style. - pub fn mime(mut self, mime: Mime) -> Part { - self.mime = Some(mime); - self - } - - /// Sets the filename, builder style. - pub fn file_name>>(mut self, filename: T) -> Part { - self.file_name = Some(filename.into()); - self - } -} - -impl fmt::Debug for Part { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("Part") - .field("value", &self.value) - .field("mime", &self.mime) - .field("file_name", &self.file_name) - .finish() - } -} - -// pub(crate) - -// Turns this Form into a Reader which implements the Read trait. -pub fn reader(form: Form) -> Reader { - form.reader() -} - -// If predictable, computes the length the request will have -// The length should be preditable if only String and file fields have been added, -// but not if a generic reader has been added; -pub fn compute_length(form: &mut Form) -> Option { - form.compute_length() -} - -pub fn boundary(form: &Form) -> &str { - &form.boundary -} - -pub struct Reader { - form: Form, - active_reader: Option>, -} - -impl fmt::Debug for Reader { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("Reader") - .field("form", &self.form) - .finish() - } -} - -impl Reader { - fn new(form: Form) -> Reader { - let mut reader = Reader { - form: form, - active_reader: None, - }; - reader.next_reader(); - reader - } - - fn next_reader(&mut self) { - self.active_reader = if self.form.fields.len() != 0 { - // We need to move out of the vector here because we are consuming the field's reader - let (name, field) = self.form.fields.remove(0); - let reader = Cursor::new(format!( - "--{}\r\n{}\r\n\r\n", - self.form.boundary, - // Try to use cached headers created by compute_length - if self.form.headers.len() > 0 { - self.form.headers.remove(0) - } else { - header(&name, &field) - } - )).chain(::body::reader(field.value)) - .chain(Cursor::new("\r\n")); - // According to https://tools.ietf.org/html/rfc2046#section-5.1.1 - // the very last field has a special boundary - if self.form.fields.len() != 0 { - Some(Box::new(reader)) - } else { - Some(Box::new(reader.chain(Cursor::new( - format!("--{}--\r\n", self.form.boundary), - )))) - } - } else { - None - } - } -} - -impl Read for Reader { - fn read(&mut self, buf: &mut [u8]) -> io::Result { - let mut total_bytes_read = 0usize; - let mut last_read_bytes; - loop { - match self.active_reader { - Some(ref mut reader) => { - last_read_bytes = reader.read(&mut buf[total_bytes_read..])?; - total_bytes_read += last_read_bytes; - if total_bytes_read == buf.len() { - return Ok(total_bytes_read); - } - } - None => return Ok(total_bytes_read), - }; - if last_read_bytes == 0 && buf.len() != 0 { - self.next_reader(); - } - } - } -} - - -fn header(name: &str, field: &Part) -> String { - format!( - "Content-Disposition: form-data; {}{}{}", - format_parameter("name", name), - match field.file_name { - Some(ref file_name) => format!("; {}", format_parameter("filename", file_name)), - None => String::new(), - }, - match field.mime { - Some(ref mime) => format!("\r\nContent-Type: {}", mime), - None => "".to_string(), - } - ) -} - -fn format_parameter(name: &str, value: &str) -> String { - let legal_value = - percent_encoding::utf8_percent_encode(value, percent_encoding::PATH_SEGMENT_ENCODE_SET) - .to_string(); - if value.len() == legal_value.len() { - // nothing has been percent encoded - format!("{}=\"{}\"", name, value) - } else { - // something has been percent encoded - format!("{}*=utf-8''{}", name, legal_value) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn form_empty() { - let mut output = Vec::new(); - let mut form = Form::new(); - let length = form.compute_length(); - form.reader().read_to_end(&mut output).unwrap(); - assert_eq!(output, b""); - assert_eq!(length.unwrap(), 0); - } - - #[test] - fn read_to_end() { - let mut output = Vec::new(); - let mut form = Form::new() - .part("reader1", Part::reader(::std::io::empty())) - .part("key1", Part::text("value1")) - .part( - "key2", - Part::text("value2").mime(::mime::IMAGE_BMP), - ) - .part("reader2", Part::reader(::std::io::empty())) - .part( - "key3", - Part::text("value3").file_name("filename"), - ); - form.boundary = "boundary".to_string(); - let length = form.compute_length(); - let expected = "--boundary\r\n\ - Content-Disposition: form-data; name=\"reader1\"\r\n\r\n\ - \r\n\ - --boundary\r\n\ - Content-Disposition: form-data; name=\"key1\"\r\n\r\n\ - value1\r\n\ - --boundary\r\n\ - Content-Disposition: form-data; name=\"key2\"\r\n\ - Content-Type: image/bmp\r\n\r\n\ - value2\r\n\ - --boundary\r\n\ - Content-Disposition: form-data; name=\"reader2\"\r\n\r\n\ - \r\n\ - --boundary\r\n\ - Content-Disposition: form-data; name=\"key3\"; filename=\"filename\"\r\n\r\n\ - value3\r\n--boundary--\r\n"; - form.reader().read_to_end(&mut output).unwrap(); - // These prints are for debug purposes in case the test fails - println!( - "START REAL\n{}\nEND REAL", - ::std::str::from_utf8(&output).unwrap() - ); - println!("START EXPECTED\n{}\nEND EXPECTED", expected); - assert_eq!(::std::str::from_utf8(&output).unwrap(), expected); - assert!(length.is_none()); - } - - #[test] - fn read_to_end_with_length() { - let mut output = Vec::new(); - let mut form = Form::new() - .text("key1", "value1") - .part( - "key2", - Part::text("value2").mime(::mime::IMAGE_BMP), - ) - .part( - "key3", - Part::text("value3").file_name("filename"), - ); - form.boundary = "boundary".to_string(); - let length = form.compute_length(); - let expected = "--boundary\r\n\ - Content-Disposition: form-data; name=\"key1\"\r\n\r\n\ - value1\r\n\ - --boundary\r\n\ - Content-Disposition: form-data; name=\"key2\"\r\n\ - Content-Type: image/bmp\r\n\r\n\ - value2\r\n\ - --boundary\r\n\ - Content-Disposition: form-data; name=\"key3\"; filename=\"filename\"\r\n\r\n\ - value3\r\n--boundary--\r\n"; - form.reader().read_to_end(&mut output).unwrap(); - // These prints are for debug purposes in case the test fails - println!( - "START REAL\n{}\nEND REAL", - ::std::str::from_utf8(&output).unwrap() - ); - println!("START EXPECTED\n{}\nEND EXPECTED", expected); - assert_eq!(::std::str::from_utf8(&output).unwrap(), expected); - assert_eq!(length.unwrap(), expected.len() as u64); - } - - #[test] - fn header_percent_encoding() { - let name = "start%'\"\r\nßend"; - let field = Part::text(""); - let expected = "Content-Disposition: form-data; name*=utf-8''start%25\'%22%0D%0A%C3%9Fend"; - - assert_eq!(header(name, &field), expected); - } -} diff --git a/src/request.rs b/src/request.rs index 9aeafe4..cb11160 100644 --- a/src/request.rs +++ b/src/request.rs @@ -456,13 +456,13 @@ impl RequestBuilder { CONTENT_TYPE, format!( "multipart/form-data; boundary={}", - ::multipart_::boundary(&multipart) + multipart.boundary() ).as_str() ); if let Ok(ref mut req) = builder.request { - *req.body_mut() = Some(match ::multipart_::compute_length(&mut multipart) { - Some(length) => Body::sized(::multipart_::reader(multipart), length), - None => Body::new(::multipart_::reader(multipart)), + *req.body_mut() = Some(match multipart.compute_length() { + Some(length) => Body::sized(multipart.reader(), length), + None => Body::new(multipart.reader()), }) } builder