pub(crate)ify the multipart facade
This commit is contained in:
@@ -179,7 +179,6 @@ mod connect_async;
|
||||
mod body;
|
||||
mod client;
|
||||
mod into_url;
|
||||
mod multipart_;
|
||||
mod proxy;
|
||||
mod redirect;
|
||||
mod request;
|
||||
|
||||
422
src/multipart.rs
422
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<String>,
|
||||
}
|
||||
|
||||
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<T, U>(self, name: T, value: U) -> Form
|
||||
where T: Into<Cow<'static, str>>,
|
||||
U: Into<Cow<'static, str>>,
|
||||
{
|
||||
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<T, U>(self, name: T, path: U) -> io::Result<Form>
|
||||
where T: Into<Cow<'static, str>>,
|
||||
U: AsRef<Path>
|
||||
{
|
||||
Ok(self.part(name, Part::file(path)?))
|
||||
}
|
||||
|
||||
/// Adds a customized Part.
|
||||
pub fn part<T>(mut self, name: T, part: Part) -> Form
|
||||
where T: Into<Cow<'static, str>>,
|
||||
{
|
||||
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<u64> {
|
||||
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<Mime>,
|
||||
file_name: Option<Cow<'static, str>>,
|
||||
}
|
||||
|
||||
impl Part {
|
||||
/// Makes a text parameter.
|
||||
pub fn text<T>(value: T) -> Part
|
||||
where T: Into<Cow<'static, str>>,
|
||||
{
|
||||
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<T: Read + Send + 'static>(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<T: Read + Send + 'static>(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<T: AsRef<Path>>(path: T) -> io::Result<Part> {
|
||||
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<T: Into<Cow<'static, str>>>(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<Box<Read + Send>>,
|
||||
}
|
||||
|
||||
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<usize> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String>,
|
||||
}
|
||||
|
||||
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<T, U>(self, name: T, value: U) -> Form
|
||||
where T: Into<Cow<'static, str>>,
|
||||
U: Into<Cow<'static, str>>,
|
||||
{
|
||||
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<T, U>(self, name: T, path: U) -> io::Result<Form>
|
||||
where T: Into<Cow<'static, str>>,
|
||||
U: AsRef<Path>
|
||||
{
|
||||
Ok(self.part(name, Part::file(path)?))
|
||||
}
|
||||
|
||||
/// Adds a customized Part.
|
||||
pub fn part<T>(mut self, name: T, part: Part) -> Form
|
||||
where T: Into<Cow<'static, str>>,
|
||||
{
|
||||
self.fields.push((name.into(), part));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Form {
|
||||
fn reader(self) -> Reader {
|
||||
Reader::new(self)
|
||||
}
|
||||
|
||||
fn compute_length(&mut self) -> Option<u64> {
|
||||
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<Mime>,
|
||||
file_name: Option<Cow<'static, str>>,
|
||||
}
|
||||
|
||||
impl Part {
|
||||
/// Makes a text parameter.
|
||||
pub fn text<T>(value: T) -> Part
|
||||
where T: Into<Cow<'static, str>>,
|
||||
{
|
||||
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<T: Read + Send + 'static>(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<T: Read + Send + 'static>(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<T: AsRef<Path>>(path: T) -> io::Result<Part> {
|
||||
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<T: Into<Cow<'static, str>>>(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<u64> {
|
||||
form.compute_length()
|
||||
}
|
||||
|
||||
pub fn boundary(form: &Form) -> &str {
|
||||
&form.boundary
|
||||
}
|
||||
|
||||
pub struct Reader {
|
||||
form: Form,
|
||||
active_reader: Option<Box<Read + Send>>,
|
||||
}
|
||||
|
||||
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<usize> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user