1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
//! A `Content-Disposition` header, 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.
use percent_encoding::{utf8_percent_encode};
use crate::utils::HEADER_VALUE_ENCODE_SET;

use std::fmt;
use std::borrow::Cow;

///Describes possible `Content-Disposition` types
///
/// `FormData` is not included as it is not supposed to be used
#[derive(PartialEq, Debug)]
pub enum DispositionType {
    ///Tells that content should be displayed inside web page.
    Inline,
    ///Tells that content should be downloaded.
    Attachment,
}

#[derive(Debug)]
///Filename parameter of `Content-Disposition`
pub enum Filename {
    ///Regular `filename`
    Name(Option<String>),
    ///Extended `filename*`
    ///
    ///Charset is always UTF-8, because whatelse you need?
    ///
    ///Values:
    ///1. Optional language tag.
    ///2. Correctly percent encoded string
    Extended(Option<String>, String)
}

impl Filename {
    ///Returns default `Filename` with empty name field.
    pub fn new() -> Self {
        Filename::Name(None)
    }

    ///Creates file name.
    pub fn with_name(name: String) -> Self {
        Filename::Name(Some(name))
    }

    ///Creates file name, and checks whether it should be encoded.
    ///
    ///Note that actual encoding would happen only when header is written.
    ///The value itself would remain unchanged in the `Filename`.
    pub fn with_encoded_name(name: Cow<str>) -> Self {
        match name.is_ascii() {
            true => Self::with_name(name.into_owned()),
            false => match utf8_percent_encode(&name, HEADER_VALUE_ENCODE_SET).into() {
                std::borrow::Cow::Owned(encoded) => Self::with_extended(None, encoded),
                std::borrow::Cow::Borrowed(maybe_encoded) => match maybe_encoded == name {
                    true => Self::with_extended(None, maybe_encoded.to_owned()),
                    false => Self::with_name(name.into_owned()),
                }
            }
        }
    }

    ///Creates extended file name.
    pub fn with_extended(lang: Option<String>, name: String) -> Self {
        Filename::Extended(lang, name)
    }

    #[inline]
    ///Returns whether filename is of extended type.
    pub fn is_extended(&self) -> bool {
        match self {
            Filename::Extended(_, _) => true,
            _ => false
        }
    }
}

#[derive(Debug)]
/// A `Content-Disposition` header, 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.
///
/// `FormData` is not included as it is not supposed to be used
pub enum ContentDisposition {
    ///Tells that content should be displayed inside web page.
    Inline,
    ///Tells that content should be downloaded.
    Attachment(Filename),
}

impl fmt::Display for ContentDisposition {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            ContentDisposition::Inline => write!(f, "inline"),
            ContentDisposition::Attachment(file) => match file {
                Filename::Name(Some(name)) => write!(f, "attachment; filename=\"{}\"", name),
                Filename::Name(None) => write!(f, "attachment"),
                Filename::Extended(lang, value) => {
                    write!(f, "attachment; filename*=utf-8'{}'{}",
                           lang.as_ref().map(|lang| lang.as_str()).unwrap_or(""),
                           value)
                },
            },
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{ContentDisposition, Filename};

    #[test]
    fn parse_file_name_extended_ascii() {
        const INPUT: &'static str = "rori.mp4";
        let file_name = Filename::with_encoded_name(INPUT.into());
        assert!(!file_name.is_extended());
    }

    #[test]
    fn parse_file_name_extended_non_ascii() {
        const INPUT: &'static str = "ロリへんたい.mp4";
        let file_name = Filename::with_encoded_name(INPUT.into());
        assert!(file_name.is_extended());
    }

    #[test]
    fn verify_content_disposition_display() {
        let cd = ContentDisposition::Inline;
        let cd = format!("{}", cd);
        assert_eq!(cd, "inline");

        let cd = ContentDisposition::Attachment(Filename::new());
        let cd = format!("{}", cd);
        assert_eq!(cd, "attachment");

        let cd = ContentDisposition::Attachment(Filename::with_name("lolka".to_string()));
        let cd = format!("{}", cd);
        assert_eq!(cd, "attachment; filename=\"lolka\"");

        let cd = ContentDisposition::Attachment(Filename::with_encoded_name("lolka".into()));
        let cd = format!("{}", cd);
        assert_eq!(cd, "attachment; filename=\"lolka\"");

        let cd = ContentDisposition::Attachment(Filename::with_encoded_name("ロリ".into()));
        let cd = format!("{}", cd);
        assert_eq!(cd, "attachment; filename*=utf-8\'\'%E3%83%AD%E3%83%AA");
    }
}