http-fs 0.5.0

HTTP File Service library
Documentation
//! 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::{percent_encode, PATH_SEGMENT_ENCODE_SET, EncodeSet};

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*`
    ///
    ///Values:
    ///1. Charset.
    ///2. Optional language tag.
    ///3. Raw bytes of name.
    Extended(String, Option<String>, Vec<u8>)
}

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 {
        let is_non_ascii = name.as_bytes().iter().any(|byte| PATH_SEGMENT_ENCODE_SET.contains(*byte));

        match is_non_ascii {
            false => Self::with_name(name.into_owned()),
            true => {
                let bytes = match name {
                    Cow::Borrowed(name) => name.as_bytes().into(),
                    Cow::Owned(name) => name.into_bytes(),
                };
                Filename::Extended("utf-8".to_owned(), None, bytes)
            }
        }
    }

    ///Creates extended file name.
    pub fn with_extended(charset: String, lang: Option<String>, name: Vec<u8>) -> Self {
        Filename::Extended(charset, 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(charset, lang, value) => {
                    write!(f, "attachment; filename*={}'{}'{}",
                           charset,
                           lang.as_ref().map(|lang| lang.as_str()).unwrap_or(""),
                           percent_encode(&value, PATH_SEGMENT_ENCODE_SET).to_string())
                },
            },
        }
    }
}

#[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");
    }
}