Skip to content

Commit

Permalink
add configuration to multipart::Form to choose percent-encoding format
Browse files Browse the repository at this point in the history
Sets the default back to `path-segment`, since the "fix" breaks anyone
using reqwest from before.

Closes #363
  • Loading branch information
seanmonstar committed Oct 26, 2018
1 parent e1a67f3 commit 70b68c2
Showing 1 changed file with 72 additions and 37 deletions.
109 changes: 72 additions & 37 deletions src/multipart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ use std::io::{self, Cursor, Read};
use std::path::Path;

use mime_guess::{self, Mime};
use url::percent_encoding;
use url::percent_encoding::EncodeSet;
use url::percent_encoding::{self, EncodeSet, PATH_SEGMENT_ENCODE_SET};
use uuid::Uuid;
use http::HeaderMap;

Expand All @@ -18,6 +17,12 @@ pub struct Form {
boundary: String,
fields: Vec<(Cow<'static, str>, Part)>,
headers: Vec<Vec<u8>>,
percent_encoding: PercentEncoding,
}

enum PercentEncoding {
PathSegment,
AttrChar,
}

impl Form {
Expand All @@ -27,6 +32,7 @@ impl Form {
boundary: format!("{}", Uuid::new_v4().to_simple()),
fields: Vec::new(),
headers: Vec::new(),
percent_encoding: PercentEncoding::PathSegment,
}
}

Expand Down Expand Up @@ -84,6 +90,18 @@ impl Form {
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 {
Reader::new(self)
}
Expand All @@ -98,7 +116,7 @@ impl Form {
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 = 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
Expand Down Expand Up @@ -277,7 +295,7 @@ impl Reader {
let mut h = if self.form.headers.len() > 0 {
self.form.headers.remove(0)
} else {
header(&name, &field)
self.form.percent_encoding.encode_headers(&name, &field)
};
h.extend_from_slice(b"\r\n\r\n");
h
Expand Down Expand Up @@ -350,41 +368,51 @@ impl EncodeSet for AttrCharEncodeSet {

}

fn header(name: &str, field: &Part) -> Vec<u8> {
let s = 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(),
},
);
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
})
}
impl PercentEncoding {
fn encode_headers(&self, name: &str, field: &Part) -> 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(name: &str, value: &str) -> String {
let legal_value =
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)
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)]
mod tests {
use super::*;
Expand Down Expand Up @@ -507,8 +535,15 @@ mod tests {
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%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"[..]
);
}
}

0 comments on commit 70b68c2

Please sign in to comment.