refactor multipart to reduce duplicate code between sync and async
This commit is contained in:
@@ -3,35 +3,57 @@ use std::borrow::Cow;
|
|||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use mime_guess::Mime;
|
use mime_guess::Mime;
|
||||||
|
use url::percent_encoding::{self, EncodeSet, PATH_SEGMENT_ENCODE_SET};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use http::HeaderMap;
|
use http::HeaderMap;
|
||||||
|
|
||||||
use futures::Stream;
|
use futures::Stream;
|
||||||
|
|
||||||
use super::Body;
|
use super::Body;
|
||||||
use multipart::{PercentEncoding, PartProp};
|
|
||||||
|
|
||||||
/// An async multipart/form-data request.
|
/// An async multipart/form-data request.
|
||||||
pub struct Form {
|
pub struct Form {
|
||||||
boundary: String,
|
inner: FormParts<Part>,
|
||||||
fields: Vec<(Cow<'static, str>, Part)>,
|
|
||||||
percent_encoding: PercentEncoding,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A field in a multipart form.
|
||||||
|
pub struct Part {
|
||||||
|
meta: PartMetadata,
|
||||||
|
value: Body,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct FormParts<P> {
|
||||||
|
pub(crate) boundary: String,
|
||||||
|
pub(crate) computed_headers: Vec<Vec<u8>>,
|
||||||
|
pub(crate) fields: Vec<(Cow<'static, str>, P)>,
|
||||||
|
pub(crate) percent_encoding: PercentEncoding,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct PartMetadata {
|
||||||
|
mime: Option<Mime>,
|
||||||
|
file_name: Option<Cow<'static, str>>,
|
||||||
|
pub(crate) headers: HeaderMap,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) trait PartProps {
|
||||||
|
fn value_len(&self) -> Option<u64>;
|
||||||
|
fn metadata(&self) -> &PartMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== impl Form =====
|
||||||
|
|
||||||
impl Form {
|
impl Form {
|
||||||
/// Creates a new async Form without any content.
|
/// Creates a new async Form without any content.
|
||||||
pub fn new() -> Form {
|
pub fn new() -> Form {
|
||||||
Form {
|
Form {
|
||||||
boundary: format!("{}", Uuid::new_v4().to_simple()),
|
inner: FormParts::new(),
|
||||||
fields: Vec::new(),
|
|
||||||
percent_encoding: PercentEncoding::PathSegment,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the boundary that this form will use.
|
/// Get the boundary that this form will use.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn boundary(&self) -> &str {
|
pub fn boundary(&self) -> &str {
|
||||||
&self.boundary
|
self.inner.boundary()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a data field with supplied name and value.
|
/// Add a data field with supplied name and value.
|
||||||
@@ -44,60 +66,62 @@ impl Form {
|
|||||||
/// .text("password", "secret");
|
/// .text("password", "secret");
|
||||||
/// ```
|
/// ```
|
||||||
pub fn text<T, U>(self, name: T, value: U) -> Form
|
pub fn text<T, U>(self, name: T, value: U) -> Form
|
||||||
where T: Into<Cow<'static, str>>,
|
where
|
||||||
|
T: Into<Cow<'static, str>>,
|
||||||
U: Into<Cow<'static, str>>,
|
U: Into<Cow<'static, str>>,
|
||||||
{
|
{
|
||||||
self.part(name, Part::text(value))
|
self.part(name, Part::text(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds a customized Part.
|
/// Adds a customized Part.
|
||||||
pub fn part<T>(mut self, name: T, part: Part) -> Form
|
pub fn part<T>(self, name: T, part: Part) -> Form
|
||||||
where T: Into<Cow<'static, str>>,
|
where
|
||||||
|
T: Into<Cow<'static, str>>,
|
||||||
{
|
{
|
||||||
self.fields.push((name.into(), part));
|
self.with_inner(move |inner| inner.part(name, part))
|
||||||
self
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configure this `Form` to percent-encode using the `path-segment` rules.
|
/// Configure this `Form` to percent-encode using the `path-segment` rules.
|
||||||
pub fn percent_encode_path_segment(mut self) -> Form {
|
pub fn percent_encode_path_segment(self) -> Form {
|
||||||
self.percent_encoding = PercentEncoding::PathSegment;
|
self.with_inner(|inner| inner.percent_encode_path_segment())
|
||||||
self
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configure this `Form` to percent-encode using the `attr-char` rules.
|
/// Configure this `Form` to percent-encode using the `attr-char` rules.
|
||||||
pub fn percent_encode_attr_chars(mut self) -> Form {
|
pub fn percent_encode_attr_chars(self) -> Form {
|
||||||
self.percent_encoding = PercentEncoding::AttrChar;
|
self.with_inner(|inner| inner.percent_encode_attr_chars())
|
||||||
self
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Consume this instance and transform into an instance of hyper::Body for use in a request.
|
/// Consume this instance and transform into an instance of hyper::Body for use in a request.
|
||||||
pub(crate) fn stream(mut self) -> hyper::Body {
|
pub(crate) fn stream(mut self) -> hyper::Body {
|
||||||
if self.fields.len() == 0 {
|
if self.inner.fields.len() == 0 {
|
||||||
return hyper::Body::empty();
|
return hyper::Body::empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
// create initial part to init reduce chain
|
// create initial part to init reduce chain
|
||||||
let (name, part) = self.fields.remove(0);
|
let (name, part) = self.inner.fields.remove(0);
|
||||||
let start = self.part_stream(name, part);
|
let start = self.part_stream(name, part);
|
||||||
|
|
||||||
let fields = self.take_fields();
|
let fields = self.inner.take_fields();
|
||||||
// for each field, chain an additional stream
|
// for each field, chain an additional stream
|
||||||
let stream = fields.into_iter().fold(start, |memo, (name, part)| {
|
let stream = fields.into_iter().fold(start, |memo, (name, part)| {
|
||||||
let part_stream = self.part_stream(name, part);
|
let part_stream = self.part_stream(name, part);
|
||||||
hyper::Body::wrap_stream(memo.chain(part_stream))
|
hyper::Body::wrap_stream(memo.chain(part_stream))
|
||||||
});
|
});
|
||||||
// append special ending boundary
|
// append special ending boundary
|
||||||
let last = hyper::Body::from(format!("--{}--\r\n", self.boundary));
|
let last = hyper::Body::from(format!("--{}--\r\n", self.boundary()));
|
||||||
hyper::Body::wrap_stream(stream.chain(last))
|
hyper::Body::wrap_stream(stream.chain(last))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate a hyper::Body stream for a single Part instance of a Form request.
|
/// Generate a hyper::Body stream for a single Part instance of a Form request.
|
||||||
pub(crate) fn part_stream<T: Into<Cow<'static, str>>>(&mut self, name: T, part: Part) -> hyper::Body {
|
pub(crate) fn part_stream<T>(&mut self, name: T, part: Part) -> hyper::Body
|
||||||
|
where
|
||||||
|
T: Into<Cow<'static, str>>,
|
||||||
|
{
|
||||||
// start with boundary
|
// start with boundary
|
||||||
let boundary = hyper::Body::from(format!("--{}\r\n", self.boundary));
|
let boundary = hyper::Body::from(format!("--{}\r\n", self.boundary()));
|
||||||
// append headers
|
// append headers
|
||||||
let header = hyper::Body::from({
|
let header = hyper::Body::from({
|
||||||
let mut h = self.percent_encoding.encode_headers(&name.into(), &part);
|
let mut h = self.inner.percent_encoding.encode_headers(&name.into(), &part.meta);
|
||||||
h.extend_from_slice(b"\r\n\r\n");
|
h.extend_from_slice(b"\r\n\r\n");
|
||||||
h
|
h
|
||||||
});
|
});
|
||||||
@@ -105,34 +129,33 @@ impl Form {
|
|||||||
hyper::Body::wrap_stream(boundary.chain(header).chain(hyper::Body::wrap_stream(part.value)).chain(hyper::Body::from("\r\n".to_owned())))
|
hyper::Body::wrap_stream(boundary.chain(header).chain(hyper::Body::wrap_stream(part.value)).chain(hyper::Body::from("\r\n".to_owned())))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Take the fields vector of this instance, replacing with an empty vector.
|
pub(crate) fn compute_length(&mut self) -> Option<u64> {
|
||||||
fn take_fields(&mut self) -> Vec<(Cow<'static, str>, Part)> {
|
self.inner.compute_length()
|
||||||
std::mem::replace(&mut self.fields, Vec::new())
|
}
|
||||||
|
|
||||||
|
fn with_inner<F>(self, func: F) -> Self
|
||||||
|
where
|
||||||
|
F: FnOnce(FormParts<Part>) -> FormParts<Part>,
|
||||||
|
{
|
||||||
|
Form {
|
||||||
|
inner: func(self.inner),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for Form {
|
impl fmt::Debug for Form {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
f.debug_struct("Form")
|
self.inner.fmt_fields("Form", f)
|
||||||
.field("boundary", &self.boundary)
|
|
||||||
.field("parts", &self.fields)
|
|
||||||
.finish()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== impl Part =====
|
||||||
/// A field in a multipart form.
|
|
||||||
pub struct Part {
|
|
||||||
value: Body,
|
|
||||||
mime: Option<Mime>,
|
|
||||||
file_name: Option<Cow<'static, str>>,
|
|
||||||
headers: HeaderMap,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Part {
|
impl Part {
|
||||||
/// Makes a text parameter.
|
/// Makes a text parameter.
|
||||||
pub fn text<T>(value: T) -> Part
|
pub fn text<T>(value: T) -> Part
|
||||||
where T: Into<Cow<'static, str>>,
|
where
|
||||||
|
T: Into<Cow<'static, str>>,
|
||||||
{
|
{
|
||||||
let body = match value.into() {
|
let body = match value.into() {
|
||||||
Cow::Borrowed(slice) => Body::from(slice),
|
Cow::Borrowed(slice) => Body::from(slice),
|
||||||
@@ -141,9 +164,10 @@ impl Part {
|
|||||||
Part::new(body)
|
Part::new(body)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Makes a new parameter from arbitrary bytes
|
/// Makes a new parameter from arbitrary bytes.
|
||||||
pub fn bytes<T>(value: T) -> Part
|
pub fn bytes<T>(value: T) -> Part
|
||||||
where T: Into<Cow<'static, [u8]>>
|
where
|
||||||
|
T: Into<Cow<'static, [u8]>>,
|
||||||
{
|
{
|
||||||
let body = match value.into() {
|
let body = match value.into() {
|
||||||
Cow::Borrowed(slice) => Body::from(slice),
|
Cow::Borrowed(slice) => Body::from(slice),
|
||||||
@@ -153,80 +177,263 @@ impl Part {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Makes a new parameter from an arbitrary stream.
|
/// Makes a new parameter from an arbitrary stream.
|
||||||
pub fn stream<T: Stream + Send + 'static>(value: T) -> Part
|
pub fn stream<T>(value: T) -> Part
|
||||||
where hyper::Chunk: std::convert::From<<T as futures::Stream>::Item>,
|
where
|
||||||
<T as futures::Stream>::Error: std::error::Error + Send + Sync {
|
T: Stream + Send + 'static,
|
||||||
|
T::Error: std::error::Error + Send + Sync,
|
||||||
|
hyper::Chunk: std::convert::From<T::Item>,
|
||||||
|
{
|
||||||
Part::new(Body::wrap(hyper::Body::wrap_stream(value)))
|
Part::new(Body::wrap(hyper::Body::wrap_stream(value)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new(value: Body) -> Part {
|
fn new(value: Body) -> Part {
|
||||||
Part {
|
Part {
|
||||||
value: value,
|
meta: PartMetadata::new(),
|
||||||
|
value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tries to set the mime of this part.
|
||||||
|
pub fn mime_str(self, mime: &str) -> ::Result<Part> {
|
||||||
|
Ok(self.mime(try_!(mime.parse())))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-export when mime 0.4 is available, with split MediaType/MediaRange.
|
||||||
|
fn mime(self, mime: Mime) -> Part {
|
||||||
|
self.with_inner(move |inner| inner.mime(mime))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the filename, builder style.
|
||||||
|
pub fn file_name<T>(self, filename: T) -> Part
|
||||||
|
where
|
||||||
|
T: Into<Cow<'static, str>>,
|
||||||
|
{
|
||||||
|
self.with_inner(move |inner| inner.file_name(filename))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_inner<F>(self, func: F) -> Self
|
||||||
|
where
|
||||||
|
F: FnOnce(PartMetadata) -> PartMetadata,
|
||||||
|
{
|
||||||
|
Part {
|
||||||
|
meta: func(self.meta),
|
||||||
|
value: self.value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for Part {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
let mut dbg = f.debug_struct("Part");
|
||||||
|
dbg.field("value", &self.value);
|
||||||
|
self.meta.fmt_fields(&mut dbg);
|
||||||
|
dbg.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartProps for Part {
|
||||||
|
fn value_len(&self) -> Option<u64> {
|
||||||
|
self.value.content_length()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn metadata(&self) -> &PartMetadata {
|
||||||
|
&self.meta
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== impl FormParts =====
|
||||||
|
|
||||||
|
impl<P: PartProps> FormParts<P> {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
FormParts {
|
||||||
|
boundary: format!("{}", Uuid::new_v4().to_simple()),
|
||||||
|
computed_headers: Vec::new(),
|
||||||
|
fields: Vec::new(),
|
||||||
|
percent_encoding: PercentEncoding::PathSegment,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn boundary(&self) -> &str {
|
||||||
|
&self.boundary
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a customized Part.
|
||||||
|
pub(crate) fn part<T>(mut self, name: T, part: P) -> Self
|
||||||
|
where
|
||||||
|
T: Into<Cow<'static, str>>,
|
||||||
|
{
|
||||||
|
self.fields.push((name.into(), part));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configure this `Form` to percent-encode using the `path-segment` rules.
|
||||||
|
pub(crate) fn percent_encode_path_segment(mut self) -> Self {
|
||||||
|
self.percent_encoding = PercentEncoding::PathSegment;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configure this `Form` to percent-encode using the `attr-char` rules.
|
||||||
|
pub(crate) fn percent_encode_attr_chars(mut self) -> Self {
|
||||||
|
self.percent_encoding = PercentEncoding::AttrChar;
|
||||||
|
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 field.value_len() {
|
||||||
|
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 = self.percent_encoding.encode_headers(name, field.metadata());
|
||||||
|
let header_length = header.len();
|
||||||
|
self.computed_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.is_empty() {
|
||||||
|
length += 2 + self.boundary().len() as u64 + 4
|
||||||
|
}
|
||||||
|
Some(length)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Take the fields vector of this instance, replacing with an empty vector.
|
||||||
|
fn take_fields(&mut self) -> Vec<(Cow<'static, str>, P)> {
|
||||||
|
std::mem::replace(&mut self.fields, Vec::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P: fmt::Debug> FormParts<P> {
|
||||||
|
pub(crate) fn fmt_fields(&self, ty_name: &'static str, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
f.debug_struct(ty_name)
|
||||||
|
.field("boundary", &self.boundary)
|
||||||
|
.field("parts", &self.fields)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== impl PartMetadata =====
|
||||||
|
|
||||||
|
impl PartMetadata {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
PartMetadata {
|
||||||
mime: None,
|
mime: None,
|
||||||
file_name: None,
|
file_name: None,
|
||||||
headers: HeaderMap::default()
|
headers: HeaderMap::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tries to set the mime of this part.
|
pub(crate) fn mime(mut self, mime: Mime) -> Self {
|
||||||
pub fn mime_str(mut self, mime: &str) -> ::Result<Part> {
|
|
||||||
self.mime = Some(try_!(mime.parse()));
|
|
||||||
Ok(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-enable when mime 0.4 is available, with split MediaType/MediaRange.
|
|
||||||
#[cfg(test)]
|
|
||||||
fn mime(mut self, mime: Mime) -> Part {
|
|
||||||
self.mime = Some(mime);
|
self.mime = Some(mime);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the filename, builder style.
|
pub(crate) fn file_name<T>(mut self, filename: T) -> Self
|
||||||
pub fn file_name<T: Into<Cow<'static, str>>>(mut self, filename: T) -> Part {
|
where
|
||||||
|
T: Into<Cow<'static, str>>,
|
||||||
|
{
|
||||||
self.file_name = Some(filename.into());
|
self.file_name = Some(filename.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartProp for Part {
|
|
||||||
fn mime(&self) -> &Option<Mime> {
|
|
||||||
&self.mime
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mime_mut(&mut self) -> &mut Option<Mime> {
|
impl PartMetadata {
|
||||||
&mut self.mime
|
pub(crate) fn fmt_fields<'f, 'fa, 'fb>(
|
||||||
}
|
&self,
|
||||||
|
debug_struct: &'f mut fmt::DebugStruct<'fa, 'fb>
|
||||||
fn file_name(&self) -> &Option<Cow<'static, str>> {
|
) -> &'f mut fmt::DebugStruct<'fa, 'fb> {
|
||||||
&self.file_name
|
debug_struct
|
||||||
}
|
|
||||||
|
|
||||||
fn file_name_mut(&mut self) -> &mut Option<Cow<'static, str>> {
|
|
||||||
&mut self.file_name
|
|
||||||
}
|
|
||||||
|
|
||||||
fn headers(&self) -> &HeaderMap {
|
|
||||||
&self.headers
|
|
||||||
}
|
|
||||||
|
|
||||||
fn headers_mut(&mut self) -> &mut HeaderMap {
|
|
||||||
&mut self.headers
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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("mime", &self.mime)
|
||||||
.field("file_name", &self.file_name)
|
.field("file_name", &self.file_name)
|
||||||
.field("headers", &self.headers)
|
.field("headers", &self.headers)
|
||||||
.finish()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub(crate) struct AttrCharEncodeSet;
|
||||||
|
|
||||||
|
impl EncodeSet for AttrCharEncodeSet {
|
||||||
|
fn contains(&self, ch: u8) -> bool {
|
||||||
|
match ch as char {
|
||||||
|
'!' => false,
|
||||||
|
'#' => false,
|
||||||
|
'$' => false,
|
||||||
|
'&' => false,
|
||||||
|
'+' => false,
|
||||||
|
'-' => false,
|
||||||
|
'.' => false,
|
||||||
|
'^' => false,
|
||||||
|
'_' => false,
|
||||||
|
'`' => false,
|
||||||
|
'|' => false,
|
||||||
|
'~' => false,
|
||||||
|
_ => {
|
||||||
|
let is_alpha_numeric = ch >= 0x41 && ch <= 0x5a || ch >= 0x61 && ch <= 0x7a || ch >= 0x30 && ch <= 0x39;
|
||||||
|
!is_alpha_numeric
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) enum PercentEncoding {
|
||||||
|
PathSegment,
|
||||||
|
AttrChar,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PercentEncoding {
|
||||||
|
pub(crate) fn encode_headers(&self, name: &str, field: &PartMetadata) -> Vec<u8> {
|
||||||
|
let s = format!(
|
||||||
|
"Content-Disposition: form-data; {}{}{}",
|
||||||
|
self.format_parameter("name", name),
|
||||||
|
match field.file_name {
|
||||||
|
Some(ref file_name) => format!("; {}", self.format_parameter("filename", file_name)),
|
||||||
|
None => String::new(),
|
||||||
|
},
|
||||||
|
match field.mime {
|
||||||
|
Some(ref mime) => format!("\r\nContent-Type: {}", mime),
|
||||||
|
None => "".to_string(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
field.headers.iter().fold(s.into_bytes(), |mut header, (k,v)| {
|
||||||
|
header.extend_from_slice(b"\r\n");
|
||||||
|
header.extend_from_slice(k.as_str().as_bytes());
|
||||||
|
header.extend_from_slice(b": ");
|
||||||
|
header.extend_from_slice(v.as_bytes());
|
||||||
|
header
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_parameter(&self, name: &str, value: &str) -> String {
|
||||||
|
let legal_value = match *self {
|
||||||
|
PercentEncoding::PathSegment => {
|
||||||
|
percent_encoding::utf8_percent_encode(value, PATH_SEGMENT_ENCODE_SET)
|
||||||
|
.to_string()
|
||||||
|
},
|
||||||
|
PercentEncoding::AttrChar => {
|
||||||
|
percent_encoding::utf8_percent_encode(value, AttrCharEncodeSet)
|
||||||
|
.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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
@@ -258,7 +465,7 @@ mod tests {
|
|||||||
"key3",
|
"key3",
|
||||||
Part::text("value3").file_name("filename"),
|
Part::text("value3").file_name("filename"),
|
||||||
);
|
);
|
||||||
form.boundary = "boundary".to_string();
|
form.inner.boundary = "boundary".to_string();
|
||||||
let expected = "--boundary\r\n\
|
let expected = "--boundary\r\n\
|
||||||
Content-Disposition: form-data; name=\"reader1\"\r\n\r\n\
|
Content-Disposition: form-data; name=\"reader1\"\r\n\r\n\
|
||||||
part1\r\n\
|
part1\r\n\
|
||||||
@@ -291,9 +498,9 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn stream_to_end_with_header() {
|
fn stream_to_end_with_header() {
|
||||||
let mut part = Part::text("value2").mime(::mime::IMAGE_BMP);
|
let mut part = Part::text("value2").mime(::mime::IMAGE_BMP);
|
||||||
part.headers_mut().insert("Hdr3", "/a/b/c".parse().unwrap());
|
part.meta.headers.insert("Hdr3", "/a/b/c".parse().unwrap());
|
||||||
let mut form = Form::new().part("key2", part);
|
let mut form = Form::new().part("key2", part);
|
||||||
form.boundary = "boundary".to_string();
|
form.inner.boundary = "boundary".to_string();
|
||||||
let expected = "--boundary\r\n\
|
let expected = "--boundary\r\n\
|
||||||
Content-Disposition: form-data; name=\"key2\"\r\n\
|
Content-Disposition: form-data; name=\"key2\"\r\n\
|
||||||
Content-Type: image/bmp\r\n\
|
Content-Type: image/bmp\r\n\
|
||||||
@@ -320,12 +527,12 @@ mod tests {
|
|||||||
let field = Part::text("");
|
let field = Part::text("");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
PercentEncoding::PathSegment.encode_headers(name, &field),
|
PercentEncoding::PathSegment.encode_headers(name, &field.meta),
|
||||||
&b"Content-Disposition: form-data; name*=utf-8''start%25'%22%0D%0A%C3%9Fend"[..]
|
&b"Content-Disposition: form-data; name*=utf-8''start%25'%22%0D%0A%C3%9Fend"[..]
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
PercentEncoding::AttrChar.encode_headers(name, &field),
|
PercentEncoding::AttrChar.encode_headers(name, &field.meta),
|
||||||
&b"Content-Disposition: form-data; name*=utf-8''start%25%27%22%0D%0A%C3%9Fend"[..]
|
&b"Content-Disposition: form-data; name*=utf-8''start%25%27%22%0D%0A%C3%9Fend"[..]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ use serde_json;
|
|||||||
use serde_urlencoded;
|
use serde_urlencoded;
|
||||||
|
|
||||||
use super::body::{Body};
|
use super::body::{Body};
|
||||||
use super::multipart as multipart_;
|
use super::multipart;
|
||||||
use super::client::{Client, Pending};
|
use super::client::{Client, Pending};
|
||||||
use header::{CONTENT_TYPE, HeaderMap, HeaderName, HeaderValue};
|
use header::{CONTENT_LENGTH, CONTENT_TYPE, HeaderMap, HeaderName, HeaderValue};
|
||||||
use http::HttpTryFrom;
|
use http::HttpTryFrom;
|
||||||
use {Method, Url};
|
use {Method, Url};
|
||||||
|
|
||||||
@@ -207,7 +207,7 @@ impl RequestBuilder {
|
|||||||
/// rt.block_on(response)
|
/// rt.block_on(response)
|
||||||
/// # }
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
pub fn multipart(self, multipart: multipart_::Form) -> RequestBuilder {
|
pub fn multipart(self, mut multipart: multipart::Form) -> RequestBuilder {
|
||||||
let mut builder = self.header(
|
let mut builder = self.header(
|
||||||
CONTENT_TYPE,
|
CONTENT_TYPE,
|
||||||
format!(
|
format!(
|
||||||
@@ -215,6 +215,12 @@ impl RequestBuilder {
|
|||||||
multipart.boundary()
|
multipart.boundary()
|
||||||
).as_str()
|
).as_str()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
builder = match multipart.compute_length() {
|
||||||
|
Some(length) => builder.header(CONTENT_LENGTH, length),
|
||||||
|
None => builder,
|
||||||
|
};
|
||||||
|
|
||||||
if let Ok(ref mut req) = builder.request {
|
if let Ok(ref mut req) = builder.request {
|
||||||
*req.body_mut() = Some(Body::wrap(multipart.stream()))
|
*req.body_mut() = Some(Body::wrap(multipart.stream()))
|
||||||
}
|
}
|
||||||
|
|||||||
299
src/multipart.rs
299
src/multipart.rs
@@ -6,40 +6,33 @@ use std::io::{self, Cursor, Read};
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use mime_guess::{self, Mime};
|
use mime_guess::{self, Mime};
|
||||||
use url::percent_encoding::{self, EncodeSet, PATH_SEGMENT_ENCODE_SET};
|
|
||||||
use uuid::Uuid;
|
|
||||||
use http::HeaderMap;
|
|
||||||
|
|
||||||
|
use async_impl::multipart::{FormParts, PartMetadata, PartProps};
|
||||||
use {Body};
|
use {Body};
|
||||||
|
|
||||||
/// A multipart/form-data request.
|
/// A multipart/form-data request.
|
||||||
pub struct Form {
|
pub struct Form {
|
||||||
boundary: String,
|
inner: FormParts<Part>,
|
||||||
fields: Vec<(Cow<'static, str>, Part)>,
|
|
||||||
headers: Vec<Vec<u8>>,
|
|
||||||
percent_encoding: PercentEncoding,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) enum PercentEncoding {
|
/// A field in a multipart form.
|
||||||
PathSegment,
|
pub struct Part {
|
||||||
AttrChar,
|
meta: PartMetadata,
|
||||||
|
value: Body,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Form {
|
impl Form {
|
||||||
/// Creates a new Form without any content.
|
/// Creates a new Form without any content.
|
||||||
pub fn new() -> Form {
|
pub fn new() -> Form {
|
||||||
Form {
|
Form {
|
||||||
boundary: format!("{}", Uuid::new_v4().to_simple()),
|
inner: FormParts::new(),
|
||||||
fields: Vec::new(),
|
|
||||||
headers: Vec::new(),
|
|
||||||
percent_encoding: PercentEncoding::PathSegment,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the boundary that this form will use.
|
/// Get the boundary that this form will use.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn boundary(&self) -> &str {
|
pub fn boundary(&self) -> &str {
|
||||||
&self.boundary
|
self.inner.boundary()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a data field with supplied name and value.
|
/// Add a data field with supplied name and value.
|
||||||
@@ -83,23 +76,21 @@ impl Form {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Adds a customized Part.
|
/// Adds a customized Part.
|
||||||
pub fn part<T>(mut self, name: T, part: Part) -> Form
|
pub fn part<T>(self, name: T, part: Part) -> Form
|
||||||
where T: Into<Cow<'static, str>>,
|
where
|
||||||
|
T: Into<Cow<'static, str>>,
|
||||||
{
|
{
|
||||||
self.fields.push((name.into(), part));
|
self.with_inner(move |inner| inner.part(name, part))
|
||||||
self
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configure this `Form` to percent-encode using the `path-segment` rules.
|
/// Configure this `Form` to percent-encode using the `path-segment` rules.
|
||||||
pub fn percent_encode_path_segment(mut self) -> Form {
|
pub fn percent_encode_path_segment(self) -> Form {
|
||||||
self.percent_encoding = PercentEncoding::PathSegment;
|
self.with_inner(|inner| inner.percent_encode_path_segment())
|
||||||
self
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configure this `Form` to percent-encode using the `attr-char` rules.
|
/// Configure this `Form` to percent-encode using the `attr-char` rules.
|
||||||
pub fn percent_encode_attr_chars(mut self) -> Form {
|
pub fn percent_encode_attr_chars(self) -> Form {
|
||||||
self.percent_encoding = PercentEncoding::AttrChar;
|
self.with_inner(|inner| inner.percent_encode_attr_chars())
|
||||||
self
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn reader(self) -> Reader {
|
pub(crate) fn reader(self) -> Reader {
|
||||||
@@ -110,58 +101,25 @@ impl Form {
|
|||||||
// The length should be preditable if only String and file fields have been added,
|
// The length should be preditable if only String and file fields have been added,
|
||||||
// but not if a generic reader has been added;
|
// but not if a generic reader has been added;
|
||||||
pub(crate) fn compute_length(&mut self) -> Option<u64> {
|
pub(crate) fn compute_length(&mut self) -> Option<u64> {
|
||||||
let mut length = 0u64;
|
self.inner.compute_length()
|
||||||
for &(ref name, ref field) in self.fields.iter() {
|
|
||||||
match field.value.len() {
|
|
||||||
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 = self.percent_encoding.encode_headers(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,
|
|
||||||
|
fn with_inner<F>(self, func: F) -> Self
|
||||||
|
where
|
||||||
|
F: FnOnce(FormParts<Part>) -> FormParts<Part>,
|
||||||
|
{
|
||||||
|
Form {
|
||||||
|
inner: func(self.inner),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 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 {
|
impl fmt::Debug for Form {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
f.debug_struct("Form")
|
self.inner.fmt_fields("Form", f)
|
||||||
.field("boundary", &self.boundary)
|
|
||||||
.field("parts", &self.fields)
|
|
||||||
.finish()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) trait PartProp {
|
|
||||||
fn mime(&self) -> &Option<Mime>;
|
|
||||||
fn mime_mut(&mut self) -> &mut Option<Mime>;
|
|
||||||
fn file_name(&self) -> &Option<Cow<'static, str>>;
|
|
||||||
fn file_name_mut(&mut self) -> &mut Option<Cow<'static, str>>;
|
|
||||||
fn headers(&self) -> &HeaderMap;
|
|
||||||
fn headers_mut(&mut self) -> &mut HeaderMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// A field in a multipart form.
|
|
||||||
pub struct Part {
|
|
||||||
value: Body,
|
|
||||||
mime: Option<Mime>,
|
|
||||||
file_name: Option<Cow<'static, str>>,
|
|
||||||
headers: HeaderMap,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Part {
|
impl Part {
|
||||||
/// Makes a text parameter.
|
/// Makes a text parameter.
|
||||||
@@ -175,7 +133,7 @@ impl Part {
|
|||||||
Part::new(body)
|
Part::new(body)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Makes a new parameter from arbitrary bytes
|
/// Makes a new parameter from arbitrary bytes.
|
||||||
pub fn bytes<T>(value: T) -> Part
|
pub fn bytes<T>(value: T) -> Part
|
||||||
where T: Into<Cow<'static, [u8]>>
|
where T: Into<Cow<'static, [u8]>>
|
||||||
{
|
{
|
||||||
@@ -208,84 +166,75 @@ impl Part {
|
|||||||
pub fn file<T: AsRef<Path>>(path: T) -> io::Result<Part> {
|
pub fn file<T: AsRef<Path>>(path: T) -> io::Result<Part> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
let file_name = path.file_name().and_then(|filename| {
|
let file_name = path.file_name().and_then(|filename| {
|
||||||
Some(Cow::from(filename.to_string_lossy().into_owned()))
|
Some(filename.to_string_lossy().into_owned())
|
||||||
});
|
});
|
||||||
let ext = path.extension()
|
let ext = path.extension()
|
||||||
.and_then(|ext| ext.to_str())
|
.and_then(|ext| ext.to_str())
|
||||||
.unwrap_or("");
|
.unwrap_or("");
|
||||||
let mime = mime_guess::get_mime_type(ext);
|
let mime = mime_guess::get_mime_type(ext);
|
||||||
let file = File::open(path)?;
|
let file = File::open(path)?;
|
||||||
let mut field = Part::new(Body::from(file));
|
let field = Part::new(Body::from(file))
|
||||||
field.mime = Some(mime);
|
.mime(mime);
|
||||||
field.file_name = file_name;
|
|
||||||
Ok(field)
|
Ok(if let Some(file_name) = file_name {
|
||||||
|
field.file_name(file_name)
|
||||||
|
} else {
|
||||||
|
field
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new(value: Body) -> Part {
|
fn new(value: Body) -> Part {
|
||||||
Part {
|
Part {
|
||||||
value: value,
|
meta: PartMetadata::new(),
|
||||||
mime: None,
|
value,
|
||||||
file_name: None,
|
|
||||||
headers: HeaderMap::default()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tries to set the mime of this part.
|
/// Tries to set the mime of this part.
|
||||||
pub fn mime_str(mut self, mime: &str) -> ::Result<Part> {
|
pub fn mime_str(self, mime: &str) -> ::Result<Part> {
|
||||||
self.mime = Some(try_!(mime.parse()));
|
Ok(self.mime(try_!(mime.parse())))
|
||||||
Ok(self)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-enable when mime 0.4 is available, with split MediaType/MediaRange.
|
// Re-export when mime 0.4 is available, with split MediaType/MediaRange.
|
||||||
#[cfg(test)]
|
fn mime(self, mime: Mime) -> Part {
|
||||||
fn mime(mut self, mime: Mime) -> Part {
|
self.with_inner(move |inner| inner.mime(mime))
|
||||||
self.mime = Some(mime);
|
|
||||||
self
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the filename, builder style.
|
/// Sets the filename, builder style.
|
||||||
pub fn file_name<T: Into<Cow<'static, str>>>(mut self, filename: T) -> Part {
|
pub fn file_name<T>(self, filename: T) -> Part
|
||||||
self.file_name = Some(filename.into());
|
where
|
||||||
self
|
T: Into<Cow<'static, str>>,
|
||||||
|
{
|
||||||
|
self.with_inner(move |inner| inner.file_name(filename))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn with_inner<F>(self, func: F) -> Self
|
||||||
|
where
|
||||||
|
F: FnOnce(PartMetadata) -> PartMetadata,
|
||||||
|
{
|
||||||
|
Part {
|
||||||
|
meta: func(self.meta),
|
||||||
|
value: self.value,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartProp for Part {
|
|
||||||
fn mime(&self) -> &Option<Mime> {
|
|
||||||
&self.mime
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mime_mut(&mut self) -> &mut Option<Mime> {
|
|
||||||
&mut self.mime
|
|
||||||
}
|
|
||||||
|
|
||||||
fn file_name(&self) -> &Option<Cow<'static, str>> {
|
|
||||||
&self.file_name
|
|
||||||
}
|
|
||||||
|
|
||||||
fn file_name_mut(&mut self) -> &mut Option<Cow<'static, str>> {
|
|
||||||
&mut self.file_name
|
|
||||||
}
|
|
||||||
|
|
||||||
fn headers(&self) -> &HeaderMap {
|
|
||||||
&self.headers
|
|
||||||
}
|
|
||||||
|
|
||||||
fn headers_mut(&mut self) -> &mut HeaderMap {
|
|
||||||
&mut self.headers
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for Part {
|
impl fmt::Debug for Part {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
f.debug_struct("Part")
|
let mut dbg = f.debug_struct("Part");
|
||||||
.field("value", &self.value)
|
dbg.field("value", &self.value);
|
||||||
.field("mime", &self.mime)
|
self.meta.fmt_fields(&mut dbg);
|
||||||
.field("file_name", &self.file_name)
|
dbg.finish()
|
||||||
.field("headers", &self.headers)
|
}
|
||||||
.finish()
|
}
|
||||||
|
|
||||||
|
impl PartProps for Part {
|
||||||
|
fn value_len(&self) -> Option<u64> {
|
||||||
|
self.value.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn metadata(&self) -> &PartMetadata {
|
||||||
|
&self.meta
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,16 +262,16 @@ impl Reader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn next_reader(&mut self) {
|
fn next_reader(&mut self) {
|
||||||
self.active_reader = if self.form.fields.len() != 0 {
|
self.active_reader = if self.form.inner.fields.len() != 0 {
|
||||||
// We need to move out of the vector here because we are consuming the field's reader
|
// 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 (name, field) = self.form.inner.fields.remove(0);
|
||||||
let boundary = Cursor::new(format!("--{}\r\n", self.form.boundary));
|
let boundary = Cursor::new(format!("--{}\r\n", self.form.boundary()));
|
||||||
let header = Cursor::new({
|
let header = Cursor::new({
|
||||||
// Try to use cached headers created by compute_length
|
// Try to use cached headers created by compute_length
|
||||||
let mut h = if self.form.headers.len() > 0 {
|
let mut h = if self.form.inner.computed_headers.len() > 0 {
|
||||||
self.form.headers.remove(0)
|
self.form.inner.computed_headers.remove(0)
|
||||||
} else {
|
} else {
|
||||||
self.form.percent_encoding.encode_headers(&name, &field)
|
self.form.inner.percent_encoding.encode_headers(&name, field.metadata())
|
||||||
};
|
};
|
||||||
h.extend_from_slice(b"\r\n\r\n");
|
h.extend_from_slice(b"\r\n\r\n");
|
||||||
h
|
h
|
||||||
@@ -333,11 +282,11 @@ impl Reader {
|
|||||||
.chain(Cursor::new("\r\n"));
|
.chain(Cursor::new("\r\n"));
|
||||||
// According to https://tools.ietf.org/html/rfc2046#section-5.1.1
|
// According to https://tools.ietf.org/html/rfc2046#section-5.1.1
|
||||||
// the very last field has a special boundary
|
// the very last field has a special boundary
|
||||||
if self.form.fields.len() != 0 {
|
if self.form.inner.fields.len() != 0 {
|
||||||
Some(Box::new(reader))
|
Some(Box::new(reader))
|
||||||
} else {
|
} else {
|
||||||
Some(Box::new(reader.chain(Cursor::new(
|
Some(Box::new(reader.chain(Cursor::new(
|
||||||
format!("--{}--\r\n", self.form.boundary),
|
format!("--{}--\r\n", self.form.boundary()),
|
||||||
))))
|
))))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -368,78 +317,6 @@ impl Read for Reader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub(crate) struct AttrCharEncodeSet;
|
|
||||||
|
|
||||||
impl EncodeSet for AttrCharEncodeSet {
|
|
||||||
fn contains(&self, ch: u8) -> bool {
|
|
||||||
match ch as char {
|
|
||||||
'!' => false,
|
|
||||||
'#' => false,
|
|
||||||
'$' => false,
|
|
||||||
'&' => false,
|
|
||||||
'+' => false,
|
|
||||||
'-' => false,
|
|
||||||
'.' => false,
|
|
||||||
'^' => false,
|
|
||||||
'_' => false,
|
|
||||||
'`' => false,
|
|
||||||
'|' => false,
|
|
||||||
'~' => false,
|
|
||||||
_ => {
|
|
||||||
let is_alpha_numeric = ch >= 0x41 && ch <= 0x5a || ch >= 0x61 && ch <= 0x7a || ch >= 0x30 && ch <= 0x39;
|
|
||||||
!is_alpha_numeric
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PercentEncoding {
|
|
||||||
pub fn encode_headers<T: PartProp>(&self, name: &str, field: &T) -> Vec<u8> {
|
|
||||||
let s = format!(
|
|
||||||
"Content-Disposition: form-data; {}{}{}",
|
|
||||||
self.format_parameter("name", name),
|
|
||||||
match *field.file_name() {
|
|
||||||
Some(ref file_name) => format!("; {}", self.format_parameter("filename", file_name)),
|
|
||||||
None => String::new(),
|
|
||||||
},
|
|
||||||
match *field.mime() {
|
|
||||||
Some(ref mime) => format!("\r\nContent-Type: {}", mime),
|
|
||||||
None => "".to_string(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
field.headers().iter().fold(s.into_bytes(), |mut header, (k,v)| {
|
|
||||||
header.extend_from_slice(b"\r\n");
|
|
||||||
header.extend_from_slice(k.as_str().as_bytes());
|
|
||||||
header.extend_from_slice(b": ");
|
|
||||||
header.extend_from_slice(v.as_bytes());
|
|
||||||
header
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn format_parameter(&self, name: &str, value: &str) -> String {
|
|
||||||
let legal_value = match *self {
|
|
||||||
PercentEncoding::PathSegment => {
|
|
||||||
percent_encoding::utf8_percent_encode(value, PATH_SEGMENT_ENCODE_SET)
|
|
||||||
.to_string()
|
|
||||||
},
|
|
||||||
PercentEncoding::AttrChar => {
|
|
||||||
percent_encoding::utf8_percent_encode(value, AttrCharEncodeSet)
|
|
||||||
.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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -469,7 +346,7 @@ mod tests {
|
|||||||
"key3",
|
"key3",
|
||||||
Part::text("value3").file_name("filename"),
|
Part::text("value3").file_name("filename"),
|
||||||
);
|
);
|
||||||
form.boundary = "boundary".to_string();
|
form.inner.boundary = "boundary".to_string();
|
||||||
let length = form.compute_length();
|
let length = form.compute_length();
|
||||||
let expected = "--boundary\r\n\
|
let expected = "--boundary\r\n\
|
||||||
Content-Disposition: form-data; name=\"reader1\"\r\n\r\n\
|
Content-Disposition: form-data; name=\"reader1\"\r\n\r\n\
|
||||||
@@ -511,7 +388,7 @@ mod tests {
|
|||||||
"key3",
|
"key3",
|
||||||
Part::text("value3").file_name("filename"),
|
Part::text("value3").file_name("filename"),
|
||||||
);
|
);
|
||||||
form.boundary = "boundary".to_string();
|
form.inner.boundary = "boundary".to_string();
|
||||||
let length = form.compute_length();
|
let length = form.compute_length();
|
||||||
let expected = "--boundary\r\n\
|
let expected = "--boundary\r\n\
|
||||||
Content-Disposition: form-data; name=\"key1\"\r\n\r\n\
|
Content-Disposition: form-data; name=\"key1\"\r\n\r\n\
|
||||||
@@ -538,9 +415,9 @@ mod tests {
|
|||||||
fn read_to_end_with_header() {
|
fn read_to_end_with_header() {
|
||||||
let mut output = Vec::new();
|
let mut output = Vec::new();
|
||||||
let mut part = Part::text("value2").mime(::mime::IMAGE_BMP);
|
let mut part = Part::text("value2").mime(::mime::IMAGE_BMP);
|
||||||
part.headers_mut().insert("Hdr3", "/a/b/c".parse().unwrap());
|
part.meta.headers.insert("Hdr3", "/a/b/c".parse().unwrap());
|
||||||
let mut form = Form::new().part("key2", part);
|
let mut form = Form::new().part("key2", part);
|
||||||
form.boundary = "boundary".to_string();
|
form.inner.boundary = "boundary".to_string();
|
||||||
let expected = "--boundary\r\n\
|
let expected = "--boundary\r\n\
|
||||||
Content-Disposition: form-data; name=\"key2\"\r\n\
|
Content-Disposition: form-data; name=\"key2\"\r\n\
|
||||||
Content-Type: image/bmp\r\n\
|
Content-Type: image/bmp\r\n\
|
||||||
@@ -557,20 +434,4 @@ mod tests {
|
|||||||
println!("START EXPECTED\n{}\nEND EXPECTED", expected);
|
println!("START EXPECTED\n{}\nEND EXPECTED", expected);
|
||||||
assert_eq!(::std::str::from_utf8(&output).unwrap(), expected);
|
assert_eq!(::std::str::from_utf8(&output).unwrap(), expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn header_percent_encoding() {
|
|
||||||
let name = "start%'\"\r\nßend";
|
|
||||||
let field = Part::text("");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
PercentEncoding::PathSegment.encode_headers(name, &field),
|
|
||||||
&b"Content-Disposition: form-data; name*=utf-8''start%25'%22%0D%0A%C3%9Fend"[..]
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
PercentEncoding::AttrChar.encode_headers(name, &field),
|
|
||||||
&b"Content-Disposition: form-data; name*=utf-8''start%25%27%22%0D%0A%C3%9Fend"[..]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user