add configuration to multipart::Form to choose percent-encoding format

Sets the default back to `path-segment`, since the "fix" breaks anyone
using reqwest from before.

Closes #363
This commit is contained in:
Sean McArthur
2018-10-26 13:57:45 -07:00
parent e1a67f32aa
commit 70b68c2b0a

View File

@@ -6,8 +6,7 @@ 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; use url::percent_encoding::{self, EncodeSet, PATH_SEGMENT_ENCODE_SET};
use url::percent_encoding::EncodeSet;
use uuid::Uuid; use uuid::Uuid;
use http::HeaderMap; use http::HeaderMap;
@@ -18,6 +17,12 @@ pub struct Form {
boundary: String, boundary: String,
fields: Vec<(Cow<'static, str>, Part)>, fields: Vec<(Cow<'static, str>, Part)>,
headers: Vec<Vec<u8>>, headers: Vec<Vec<u8>>,
percent_encoding: PercentEncoding,
}
enum PercentEncoding {
PathSegment,
AttrChar,
} }
impl Form { impl Form {
@@ -27,6 +32,7 @@ impl Form {
boundary: format!("{}", Uuid::new_v4().to_simple()), boundary: format!("{}", Uuid::new_v4().to_simple()),
fields: Vec::new(), fields: Vec::new(),
headers: Vec::new(), headers: Vec::new(),
percent_encoding: PercentEncoding::PathSegment,
} }
} }
@@ -84,6 +90,18 @@ impl Form {
self self
} }
/// Configure this `Form` to percent-encode using the `path-segment` rules.
pub fn percent_encode_path_segment(mut self) -> Form {
self.percent_encoding = PercentEncoding::PathSegment;
self
}
/// Configure this `Form` to percent-encode using the `attr-char` rules.
pub fn percent_encode_attr_chars(mut self) -> Form {
self.percent_encoding = PercentEncoding::AttrChar;
self
}
pub(crate) fn reader(self) -> Reader { pub(crate) fn reader(self) -> Reader {
Reader::new(self) Reader::new(self)
} }
@@ -98,7 +116,7 @@ impl Form {
Some(value_length) => { Some(value_length) => {
// We are constructing the header just to get its length. To not have to // 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. // construct it again when the request is sent we cache these headers.
let header = header(name, field); let header = self.percent_encoding.encode_headers(name, field);
let header_length = header.len(); let header_length = header.len();
self.headers.push(header); self.headers.push(header);
// The additions mimick the format string out of which the field is constructed // The additions mimick the format string out of which the field is constructed
@@ -277,7 +295,7 @@ impl Reader {
let mut h = if self.form.headers.len() > 0 { let mut h = if self.form.headers.len() > 0 {
self.form.headers.remove(0) self.form.headers.remove(0)
} else { } else {
header(&name, &field) self.form.percent_encoding.encode_headers(&name, &field)
}; };
h.extend_from_slice(b"\r\n\r\n"); h.extend_from_slice(b"\r\n\r\n");
h h
@@ -350,41 +368,51 @@ impl EncodeSet for AttrCharEncodeSet {
} }
fn header(name: &str, field: &Part) -> Vec<u8> { impl PercentEncoding {
let s = format!( fn encode_headers(&self, name: &str, field: &Part) -> Vec<u8> {
"Content-Disposition: form-data; {}{}{}", let s = format!(
format_parameter("name", name), "Content-Disposition: form-data; {}{}{}",
match field.file_name { self.format_parameter("name", name),
Some(ref file_name) => format!("; {}", format_parameter("filename", file_name)), match field.file_name {
None => String::new(), 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), match field.mime {
None => "".to_string(), 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"); field.headers.iter().fold(s.into_bytes(), |mut header, (k,v)| {
header.extend_from_slice(k.as_str().as_bytes()); header.extend_from_slice(b"\r\n");
header.extend_from_slice(b": "); header.extend_from_slice(k.as_str().as_bytes());
header.extend_from_slice(v.as_bytes()); header.extend_from_slice(b": ");
header header.extend_from_slice(v.as_bytes());
}) header
} })
}
fn format_parameter(name: &str, value: &str) -> String { fn format_parameter(&self, name: &str, value: &str) -> String {
let legal_value = let legal_value = match *self {
percent_encoding::utf8_percent_encode(value, AttrCharEncodeSet) PercentEncoding::PathSegment => {
.to_string(); percent_encoding::utf8_percent_encode(value, PATH_SEGMENT_ENCODE_SET)
if value.len() == legal_value.len() { .to_string()
// nothing has been percent encoded },
format!("{}=\"{}\"", name, value) PercentEncoding::AttrChar => {
} else { percent_encoding::utf8_percent_encode(value, AttrCharEncodeSet)
// something has been percent encoded .to_string()
format!("{}*=utf-8''{}", name, legal_value) },
};
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::*;
@@ -507,8 +535,15 @@ mod tests {
fn header_percent_encoding() { fn header_percent_encoding() {
let name = "start%'\"\r\nßend"; let name = "start%'\"\r\nßend";
let field = Part::text(""); let field = Part::text("");
let expected = "Content-Disposition: form-data; name*=utf-8''start%25%27%22%0D%0A%C3%9Fend";
assert_eq!(header(name, &field), expected.as_bytes()); 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"[..]
);
} }
} }