use std::collections::{BTreeMap, HashMap};
use charset::Charset;
#[derive(Debug, Clone, PartialEq)]
pub enum DispositionType {
Inline,
Attachment,
FormData,
Extension(String),
}
impl Default for DispositionType {
fn default() -> Self {
DispositionType::Inline
}
}
fn parse_disposition_type(disposition: &str) -> DispositionType {
match &disposition.to_lowercase()[..] {
"inline" => DispositionType::Inline,
"attachment" => DispositionType::Attachment,
"form-data" => DispositionType::FormData,
extension => DispositionType::Extension(extension.to_string()),
}
}
#[derive(Debug, Clone, Default)]
pub struct ParsedContentDisposition {
pub disposition: DispositionType,
pub params: BTreeMap<String, String>,
}
impl ParsedContentDisposition {
#[allow(dead_code)]
pub fn name(&self) -> Option<String> {
self.params.get("name").cloned()
}
#[allow(dead_code)]
pub fn filename_full(&self) -> Option<String> {
self.params.get("filename").cloned()
}
#[allow(dead_code)]
pub fn filename(&self) -> Option<(String, Option<String>)> {
let clone = self.params.get("filename").cloned();
match clone {
Some(c) => {
let mut arr: Vec<&str> = c.split(".").collect();
let last = arr.pop();
let first = arr.join(".");
Some(match last {
Some(l) => (first, Some(l.to_owned())),
None => (first, None),
})
}
None => None,
}
}
}
pub fn parse_content_disposition(header: &str) -> ParsedContentDisposition {
let params = parse_param_content(header);
let disposition = parse_disposition_type(¶ms.value);
ParsedContentDisposition {
disposition,
params: params.params,
}
}
struct ParamContent {
value: String,
params: BTreeMap<String, String>,
}
fn parse_param_content(content: &str) -> ParamContent {
let mut tokens = content.split(';');
let value = tokens.next().unwrap().trim();
let mut map: BTreeMap<String, String> = tokens
.filter_map(|kv| {
kv.find('=').map(|idx| {
let key = kv[0..idx].trim().to_lowercase();
let mut value = kv[idx + 1..].trim();
if value.starts_with('"') && value.ends_with('"') && value.len() > 1 {
value = &value[1..value.len() - 1];
}
(key, value.to_string())
})
})
.collect();
let decode_key_list: Vec<String> = map
.keys()
.filter_map(|k| k.strip_suffix('*'))
.map(String::from)
.filter(|k| !map.contains_key(k))
.collect();
let encodings = compute_parameter_encodings(&map, &decode_key_list);
for (k, (e, strip)) in encodings {
if let Some(charset) = Charset::for_label_no_replacement(e.as_bytes()) {
let key = format!("{}*", k);
let percent_encoded_value = map.remove(&key).unwrap();
let encoded_value = if strip {
percent_decode(percent_encoded_value.splitn(3, '\'').nth(2).unwrap_or(""))
} else {
percent_decode(&percent_encoded_value)
};
let decoded_value = charset.decode_without_bom_handling(&encoded_value).0;
map.insert(k, decoded_value.to_string());
}
}
let unwrap_key_list: Vec<String> = map
.keys()
.filter_map(|k| k.strip_suffix("*0"))
.map(String::from)
.filter(|k| !map.contains_key(k))
.collect();
for unwrap_key in unwrap_key_list {
let mut unwrapped_value = String::new();
let mut index = 0;
while let Some(wrapped_value_part) = map.remove(&format!("{}*{}", &unwrap_key, index)) {
index += 1;
unwrapped_value.push_str(&wrapped_value_part);
}
let old_value = map.insert(unwrap_key, unwrapped_value);
assert!(old_value.is_none());
}
ParamContent {
value: value.into(),
params: map,
}
}
fn compute_parameter_encodings(
map: &BTreeMap<String, String>,
decode_key_list: &Vec<String>,
) -> HashMap<String, (String, bool)> {
let mut encodings: HashMap<String, (String, bool)> = HashMap::new();
for decode_key in decode_key_list {
if let Some(unwrap_key) = decode_key.strip_suffix("*0") {
let encoding = map
.get(&format!("{}*", decode_key))
.unwrap()
.split('\'')
.next()
.unwrap_or("");
let continuation_prefix = format!("{}*", unwrap_key);
for continuation_key in decode_key_list {
if continuation_key.starts_with(&continuation_prefix) {
encodings.insert(
continuation_key.clone(),
(encoding.to_string(), continuation_key == decode_key),
);
}
}
} else if !encodings.contains_key(decode_key) {
let encoding = map
.get(&format!("{}*", decode_key))
.unwrap()
.split('\'')
.next()
.unwrap_or("")
.to_string();
let old_value = encodings.insert(decode_key.clone(), (encoding, true));
assert!(old_value.is_none());
}
}
encodings
}
fn percent_decode(encoded: &str) -> Vec<u8> {
let mut decoded = Vec::with_capacity(encoded.len());
let mut bytes = encoded.bytes();
let mut next = bytes.next();
while next.is_some() {
let b = next.unwrap();
if b != b'%' {
decoded.push(b);
next = bytes.next();
continue;
}
let top = match bytes.next() {
Some(n) if n.is_ascii_hexdigit() => n,
n => {
decoded.push(b);
next = n;
continue;
}
};
let bottom = match bytes.next() {
Some(n) if n.is_ascii_hexdigit() => n,
n => {
decoded.push(b);
decoded.push(top);
next = n;
continue;
}
};
let decoded_byte = (hex_to_nybble(top) << 4) | hex_to_nybble(bottom);
decoded.push(decoded_byte);
next = bytes.next();
}
decoded
}
fn hex_to_nybble(byte: u8) -> u8 {
match byte {
b'0'..=b'9' => byte - b'0',
b'a'..=b'f' => byte - b'a' + 10,
b'A'..=b'F' => byte - b'A' + 10,
_ => panic!("Not a hex character!"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_content_disposition() {
let dis = parse_content_disposition("inline");
assert_eq!(dis.disposition, DispositionType::Inline);
assert_eq!(dis.params.get("name"), None);
assert_eq!(dis.params.get("filename"), None);
let dis = parse_content_disposition(
" attachment; x=y; charset=\"fake\" ; x2=y2; name=\"King Joffrey.death\"",
);
assert_eq!(dis.disposition, DispositionType::Attachment);
assert_eq!(
dis.params.get("name"),
Some(&"King Joffrey.death".to_string())
);
assert_eq!(dis.params.get("filename"), None);
let dis = parse_content_disposition(" form-data; name=\"cover\"; filename=\"exif.jpg\"");
assert_eq!(dis.disposition, DispositionType::FormData);
assert_eq!(dis.name(), Some("cover".to_string()));
assert_eq!(dis.filename_full(), Some("exif.jpg".to_string()));
let f = dis.filename().unwrap();
assert_eq!(f.0, "exif".to_string());
assert_eq!(f.1, Some("jpg".to_string()));
}
}