Skip to content

Commit

Permalink
feat(headers): Add Content-Disposition header
Browse files Browse the repository at this point in the history
  • Loading branch information
mikedilger committed Nov 10, 2015
1 parent 45b0b88 commit 1e6e7de
Show file tree
Hide file tree
Showing 2 changed files with 170 additions and 0 deletions.
168 changes: 168 additions & 0 deletions src/header/common/content_disposition.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// # References
//
// "The Content-Disposition Header Field" https://www.ietf.org/rfc/rfc2183.txt
// "The Content-Disposition Header Field in the Hypertext Transfer Protocol (HTTP)" https://www.ietf.org/rfc/rfc6266.txt
// "Returning Values from Forms: multipart/form-data" https://www.ietf.org/rfc/rfc2388.txt
// Browser conformance tests at: http://greenbytes.de/tech/tc2231/
// IANA assignment: http://www.iana.org/assignments/cont-disp/cont-disp.xhtml

use std::ascii::AsciiExt;
use std::fmt;

use header::{Header, HeaderFormat, parsing};

#[derive(Clone, Debug, PartialEq)]
pub enum DispositionType {
Inline,
Attachment,
Ext(String)
}

#[derive(Clone, Debug, PartialEq)]
pub enum DispositionParam {
Filename(String),
Ext(String,String)
}

/// A `Content-Disposition` header, (re)defined in [RFC6266](https://tools.ietf.org/html/rfc6266)
///
/// The Content-Disposition response header field is used to convey
/// additional information about how to process the response payload, and
/// also can be used to attach additional metadata, such as the filename
/// to use when saving the response payload locally.
///
/// # ABNF
/// ```plain
/// content-disposition = "Content-Disposition" ":"
/// disposition-type *( ";" disposition-parm )
///
/// disposition-type = "inline" | "attachment" | disp-ext-type
/// ; case-insensitive
///
/// disp-ext-type = token
///
/// disposition-parm = filename-parm | disp-ext-parm
///
/// filename-parm = "filename" "=" value
/// | "filename*" "=" ext-value
///
/// disp-ext-parm = token "=" value
/// | ext-token "=" ext-value
///
/// ext-token = <the characters in token, followed by "*">
/// ```
#[derive(Clone, Debug, PartialEq)]
pub struct ContentDisposition {
pub disposition: DispositionType,
pub parameters: Vec<DispositionParam>,
}

impl Header for ContentDisposition {
fn header_name() -> &'static str {
"Content-Disposition"
}

fn parse_header(raw: &[Vec<u8>]) -> ::Result<ContentDisposition> {
parsing::from_one_raw_str(raw).and_then(|s: String| {
let mut sections = s.split(';');
let disposition = match sections.next() {
Some(s) => s.trim().to_ascii_lowercase(),
None => return Err(::Error::Header),
};

let mut cd = ContentDisposition {
disposition: match &*disposition {
"inline" => DispositionType::Inline,
"attachment" => DispositionType::Attachment,
_ => DispositionType::Ext(disposition),
},
parameters: Vec::new(),
};

for section in sections {
let mut parts = section.split('=');

let key = if let Some(key) = parts.next() {
key.trim().to_ascii_lowercase()
} else {
return Err(::Error::Header);
};

let mut val = if let Some(val) = parts.next() {
val.trim()
} else {
return Err(::Error::Header);
};

if val.chars().next() == Some('"') && val.chars().rev().next() == Some('"') {
// Unwrap the quotation marks.
val = &val[1..val.len() - 1];
}

cd.parameters.push(
match &*key {
"filename" => DispositionParam::Filename(val.to_owned()),
_ => DispositionParam::Ext(key, val.to_owned()),
}
);
}

Ok(cd)
})
}
}


impl HeaderFormat for ContentDisposition {
#[inline]
fn fmt_header(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Display::fmt(&self, f)
}
}

impl fmt::Display for ContentDisposition {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self.disposition {
DispositionType::Inline => try!(write!(f, "inline")),
DispositionType::Attachment => try!(write!(f, "attachment")),
DispositionType::Ext(ref s) => try!(write!(f, "{}", s)),
}
for param in self.parameters.iter() {
match param {
&DispositionParam::Filename(ref v) => try!(write!(f, "; filename=\"{}\"", v)),
&DispositionParam::Ext(ref k, ref v) => try!(write!(f, "; {}=\"{}\"", k, v)),
}
}
Ok(())
}
}

#[cfg(test)]
mod tests {
use super::{ContentDisposition,DispositionType,DispositionParam};
use ::header::Header;

#[test]
fn parse_header() {
assert!(ContentDisposition::parse_header([b"".to_vec()].as_ref()).is_err());

let a = [b"form-data; dummy=3; name=upload;\r\n filename=\"sample.png\"".to_vec()];
let a: ContentDisposition = ContentDisposition::parse_header(a.as_ref()).unwrap();
let b = ContentDisposition {
disposition: DispositionType::Ext("form-data".to_owned()),
parameters: vec![ DispositionParam::Ext("dummy".to_owned(), "3".to_owned()),
DispositionParam::Ext("name".to_owned(), "upload".to_owned()),
DispositionParam::Filename("sample.png".to_owned()) ]
};
assert_eq!(a, b);

let a = [b"attachment; filename=\"image.jpg\"".to_vec()];
let a: ContentDisposition = ContentDisposition::parse_header(a.as_ref()).unwrap();
let b = ContentDisposition {
disposition: DispositionType::Attachment,
parameters: vec![ DispositionParam::Filename("image.jpg".to_owned()) ]
};
assert_eq!(a, b);

}
}
2 changes: 2 additions & 0 deletions src/header/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub use self::allow::Allow;
pub use self::authorization::{Authorization, Scheme, Basic, Bearer};
pub use self::cache_control::{CacheControl, CacheDirective};
pub use self::connection::{Connection, ConnectionOption};
pub use self::content_disposition::ContentDisposition;
pub use self::content_length::ContentLength;
pub use self::content_encoding::ContentEncoding;
pub use self::content_language::ContentLanguage;
Expand Down Expand Up @@ -369,6 +370,7 @@ mod authorization;
mod cache_control;
mod cookie;
mod connection;
mod content_disposition;
mod content_encoding;
mod content_language;
mod content_length;
Expand Down

0 comments on commit 1e6e7de

Please sign in to comment.