http_fs/headers/
cd.rs

1//! A `Content-Disposition` header, defined in [RFC6266](https://tools.ietf.org/html/rfc6266).
2///
3/// The Content-Disposition response header field is used to convey
4/// additional information about how to process the response payload, and
5/// also can be used to attach additional metadata, such as the filename
6/// to use when saving the response payload locally.
7use percent_encoding::utf8_percent_encode;
8use crate::utils::HEADER_VALUE_ENCODE_SET;
9
10use std::fmt;
11use std::borrow::Cow;
12
13///Describes possible `Content-Disposition` types
14///
15/// `FormData` is not included as it is not supposed to be used
16#[derive(PartialEq, Debug)]
17pub enum DispositionType {
18    ///Tells that content should be displayed inside web page.
19    Inline,
20    ///Tells that content should be downloaded.
21    Attachment,
22}
23
24#[derive(Debug)]
25///Filename parameter of `Content-Disposition`
26pub enum Filename {
27    ///Regular `filename`
28    Name(Option<String>),
29    ///Extended `filename*`
30    ///
31    ///Charset is always UTF-8, because whatelse you need?
32    ///
33    ///Values:
34    ///1. Optional language tag.
35    ///2. Correctly percent encoded string
36    Extended(Option<String>, String)
37}
38
39impl Filename {
40    ///Returns default `Filename` with empty name field.
41    pub fn new() -> Self {
42        Filename::Name(None)
43    }
44
45    ///Creates file name.
46    pub fn with_name(name: String) -> Self {
47        Filename::Name(Some(name))
48    }
49
50    ///Creates file name, and checks whether it should be encoded.
51    ///
52    ///Note that actual encoding would happen only when header is written.
53    ///The value itself would remain unchanged in the `Filename`.
54    pub fn with_encoded_name(name: Cow<str>) -> Self {
55        match name.is_ascii() {
56            true => Self::with_name(name.into_owned()),
57            false => match utf8_percent_encode(&name, HEADER_VALUE_ENCODE_SET).into() {
58                std::borrow::Cow::Owned(encoded) => Self::with_extended(None, encoded),
59                std::borrow::Cow::Borrowed(maybe_encoded) => match maybe_encoded == name {
60                    true => Self::with_extended(None, maybe_encoded.to_owned()),
61                    false => Self::with_name(name.into_owned()),
62                }
63            }
64        }
65    }
66
67    ///Creates extended file name.
68    pub fn with_extended(lang: Option<String>, name: String) -> Self {
69        Filename::Extended(lang, name)
70    }
71
72    #[inline]
73    ///Returns whether filename is of extended type.
74    pub fn is_extended(&self) -> bool {
75        match self {
76            Filename::Extended(_, _) => true,
77            _ => false
78        }
79    }
80}
81
82#[derive(Debug)]
83/// A `Content-Disposition` header, defined in [RFC6266](https://tools.ietf.org/html/rfc6266).
84///
85/// The Content-Disposition response header field is used to convey
86/// additional information about how to process the response payload, and
87/// also can be used to attach additional metadata, such as the filename
88/// to use when saving the response payload locally.
89///
90/// `FormData` is not included as it is not supposed to be used
91pub enum ContentDisposition {
92    ///Tells that content should be displayed inside web page.
93    Inline,
94    ///Tells that content should be downloaded.
95    Attachment(Filename),
96}
97
98impl fmt::Display for ContentDisposition {
99    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
100        match self {
101            ContentDisposition::Inline => write!(f, "inline"),
102            ContentDisposition::Attachment(file) => match file {
103                Filename::Name(Some(name)) => write!(f, "attachment; filename=\"{}\"", name),
104                Filename::Name(None) => write!(f, "attachment"),
105                Filename::Extended(lang, value) => {
106                    write!(f, "attachment; filename*=utf-8'{}'{}",
107                           lang.as_ref().map(|lang| lang.as_str()).unwrap_or(""),
108                           value)
109                },
110            },
111        }
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::{ContentDisposition, Filename};
118
119    #[test]
120    fn parse_file_name_extended_ascii() {
121        const INPUT: &'static str = "rori.mp4";
122        let file_name = Filename::with_encoded_name(INPUT.into());
123        assert!(!file_name.is_extended());
124    }
125
126    #[test]
127    fn parse_file_name_extended_non_ascii() {
128        const INPUT: &'static str = "ロリへんたい.mp4";
129        let file_name = Filename::with_encoded_name(INPUT.into());
130        assert!(file_name.is_extended());
131    }
132
133    #[test]
134    fn verify_content_disposition_display() {
135        let cd = ContentDisposition::Inline;
136        let cd = format!("{}", cd);
137        assert_eq!(cd, "inline");
138
139        let cd = ContentDisposition::Attachment(Filename::new());
140        let cd = format!("{}", cd);
141        assert_eq!(cd, "attachment");
142
143        let cd = ContentDisposition::Attachment(Filename::with_name("lolka".to_string()));
144        let cd = format!("{}", cd);
145        assert_eq!(cd, "attachment; filename=\"lolka\"");
146
147        let cd = ContentDisposition::Attachment(Filename::with_encoded_name("lolka".into()));
148        let cd = format!("{}", cd);
149        assert_eq!(cd, "attachment; filename=\"lolka\"");
150
151        let cd = ContentDisposition::Attachment(Filename::with_encoded_name("ロリ".into()));
152        let cd = format!("{}", cd);
153        assert_eq!(cd, "attachment; filename*=utf-8\'\'%E3%83%AD%E3%83%AA");
154    }
155}