1use std::fmt::{self, Write};
14
15use once_cell::sync::Lazy;
16#[cfg(feature = "unicode")]
17use regex::Regex;
18#[cfg(not(feature = "unicode"))]
19use regex_lite::Regex;
20
21use super::{ExtendedValue, Header, TryIntoHeaderValue, Writer};
22use crate::http::header;
23
24fn split_once(haystack: &str, needle: char) -> (&str, &str) {
26    haystack.find(needle).map_or_else(
27        || (haystack, ""),
28        |sc| {
29            let (first, last) = haystack.split_at(sc);
30            (first, last.split_at(1).1)
31        },
32    )
33}
34
35fn split_once_and_trim(haystack: &str, needle: char) -> (&str, &str) {
38    let (first, last) = split_once(haystack, needle);
39    (first.trim_end(), last.trim_start())
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
44pub enum DispositionType {
45    Inline,
47
48    Attachment,
51
52    FormData,
56
57    Ext(String),
59}
60
61impl<'a> From<&'a str> for DispositionType {
62    fn from(origin: &'a str) -> DispositionType {
63        if origin.eq_ignore_ascii_case("inline") {
64            DispositionType::Inline
65        } else if origin.eq_ignore_ascii_case("attachment") {
66            DispositionType::Attachment
67        } else if origin.eq_ignore_ascii_case("form-data") {
68            DispositionType::FormData
69        } else {
70            DispositionType::Ext(origin.to_owned())
71        }
72    }
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
86#[allow(clippy::large_enum_variant)]
87pub enum DispositionParam {
88    Name(String),
91
92    Filename(String),
99
100    FilenameExt(ExtendedValue),
103
104    Unknown(String, String),
110
111    UnknownExt(String, ExtendedValue),
118}
119
120impl DispositionParam {
121    #[inline]
123    pub fn is_name(&self) -> bool {
124        self.as_name().is_some()
125    }
126
127    #[inline]
129    pub fn is_filename(&self) -> bool {
130        self.as_filename().is_some()
131    }
132
133    #[inline]
135    pub fn is_filename_ext(&self) -> bool {
136        self.as_filename_ext().is_some()
137    }
138
139    #[inline]
141    pub fn is_unknown<T: AsRef<str>>(&self, name: T) -> bool {
143        self.as_unknown(name).is_some()
144    }
145
146    #[inline]
149    pub fn is_unknown_ext<T: AsRef<str>>(&self, name: T) -> bool {
150        self.as_unknown_ext(name).is_some()
151    }
152
153    #[inline]
155    pub fn as_name(&self) -> Option<&str> {
156        match self {
157            DispositionParam::Name(name) => Some(name.as_str()),
158            _ => None,
159        }
160    }
161
162    #[inline]
164    pub fn as_filename(&self) -> Option<&str> {
165        match self {
166            DispositionParam::Filename(filename) => Some(filename.as_str()),
167            _ => None,
168        }
169    }
170
171    #[inline]
173    pub fn as_filename_ext(&self) -> Option<&ExtendedValue> {
174        match self {
175            DispositionParam::FilenameExt(value) => Some(value),
176            _ => None,
177        }
178    }
179
180    #[inline]
183    pub fn as_unknown<T: AsRef<str>>(&self, name: T) -> Option<&str> {
184        match self {
185            DispositionParam::Unknown(ref ext_name, ref value)
186                if ext_name.eq_ignore_ascii_case(name.as_ref()) =>
187            {
188                Some(value.as_str())
189            }
190            _ => None,
191        }
192    }
193
194    #[inline]
197    pub fn as_unknown_ext<T: AsRef<str>>(&self, name: T) -> Option<&ExtendedValue> {
198        match self {
199            DispositionParam::UnknownExt(ref ext_name, ref value)
200                if ext_name.eq_ignore_ascii_case(name.as_ref()) =>
201            {
202                Some(value)
203            }
204            _ => None,
205        }
206    }
207}
208
209#[derive(Debug, Clone, PartialEq, Eq)]
314pub struct ContentDisposition {
315    pub disposition: DispositionType,
317
318    pub parameters: Vec<DispositionParam>,
320}
321
322impl ContentDisposition {
323    pub fn attachment(filename: impl Into<String>) -> Self {
335        Self {
336            disposition: DispositionType::Attachment,
337            parameters: vec![DispositionParam::Filename(filename.into())],
338        }
339    }
340
341    pub fn from_raw(hv: &header::HeaderValue) -> Result<Self, crate::error::ParseError> {
343        let hv = String::from_utf8(hv.as_bytes().to_vec())
346            .map_err(|_| crate::error::ParseError::Header)?;
347
348        let (disp_type, mut left) = split_once_and_trim(hv.as_str().trim(), ';');
349        if disp_type.is_empty() {
350            return Err(crate::error::ParseError::Header);
351        }
352
353        let mut cd = ContentDisposition {
354            disposition: disp_type.into(),
355            parameters: Vec::new(),
356        };
357
358        while !left.is_empty() {
359            let (param_name, new_left) = split_once_and_trim(left, '=');
360            if param_name.is_empty() || param_name == "*" || new_left.is_empty() {
361                return Err(crate::error::ParseError::Header);
362            }
363            left = new_left;
364            if let Some(param_name) = param_name.strip_suffix('*') {
365                let (ext_value, new_left) = split_once_and_trim(left, ';');
367                left = new_left;
368                let ext_value = header::parse_extended_value(ext_value)?;
369
370                let param = if param_name.eq_ignore_ascii_case("filename") {
371                    DispositionParam::FilenameExt(ext_value)
372                } else {
373                    DispositionParam::UnknownExt(param_name.to_owned(), ext_value)
374                };
375                cd.parameters.push(param);
376            } else {
377                let value = if left.starts_with('\"') {
379                    let mut escaping = false;
381                    let mut quoted_string = vec![];
382                    let mut end = None;
383                    for (i, &c) in left.as_bytes().iter().skip(1).enumerate() {
385                        if escaping {
386                            escaping = false;
387                            quoted_string.push(c);
388                        } else if c == 0x5c {
389                            escaping = true;
391                        } else if c == 0x22 {
392                            end = Some(i + 1); break;
395                        } else {
396                            quoted_string.push(c);
397                        }
398                    }
399                    left = &left[end.ok_or(crate::error::ParseError::Header)? + 1..];
400                    left = split_once(left, ';').1.trim_start();
401                    String::from_utf8(quoted_string)
403                        .map_err(|_| crate::error::ParseError::Header)?
404                } else {
405                    let (token, new_left) = split_once_and_trim(left, ';');
407                    left = new_left;
408                    if token.is_empty() {
409                        return Err(crate::error::ParseError::Header);
411                    }
412                    token.to_owned()
413                };
414
415                let param = if param_name.eq_ignore_ascii_case("name") {
416                    DispositionParam::Name(value)
417                } else if param_name.eq_ignore_ascii_case("filename") {
418                    DispositionParam::Filename(value)
420                } else {
421                    DispositionParam::Unknown(param_name.to_owned(), value)
422                };
423                cd.parameters.push(param);
424            }
425        }
426
427        Ok(cd)
428    }
429
430    pub fn is_inline(&self) -> bool {
432        matches!(self.disposition, DispositionType::Inline)
433    }
434
435    pub fn is_attachment(&self) -> bool {
437        matches!(self.disposition, DispositionType::Attachment)
438    }
439
440    pub fn is_form_data(&self) -> bool {
442        matches!(self.disposition, DispositionType::FormData)
443    }
444
445    pub fn is_ext(&self, disp_type: impl AsRef<str>) -> bool {
447        matches!(
448            self.disposition,
449            DispositionType::Ext(ref t) if t.eq_ignore_ascii_case(disp_type.as_ref())
450        )
451    }
452
453    pub fn get_name(&self) -> Option<&str> {
455        self.parameters.iter().find_map(DispositionParam::as_name)
456    }
457
458    pub fn get_filename(&self) -> Option<&str> {
460        self.parameters
461            .iter()
462            .find_map(DispositionParam::as_filename)
463    }
464
465    pub fn get_filename_ext(&self) -> Option<&ExtendedValue> {
467        self.parameters
468            .iter()
469            .find_map(DispositionParam::as_filename_ext)
470    }
471
472    pub fn get_unknown(&self, name: impl AsRef<str>) -> Option<&str> {
474        let name = name.as_ref();
475        self.parameters.iter().find_map(|p| p.as_unknown(name))
476    }
477
478    pub fn get_unknown_ext(&self, name: impl AsRef<str>) -> Option<&ExtendedValue> {
480        let name = name.as_ref();
481        self.parameters.iter().find_map(|p| p.as_unknown_ext(name))
482    }
483}
484
485impl TryIntoHeaderValue for ContentDisposition {
486    type Error = header::InvalidHeaderValue;
487
488    fn try_into_value(self) -> Result<header::HeaderValue, Self::Error> {
489        let mut writer = Writer::new();
490        let _ = write!(&mut writer, "{}", self);
491        header::HeaderValue::from_maybe_shared(writer.take())
492    }
493}
494
495impl Header for ContentDisposition {
496    fn name() -> header::HeaderName {
497        header::CONTENT_DISPOSITION
498    }
499
500    fn parse<T: crate::HttpMessage>(msg: &T) -> Result<Self, crate::error::ParseError> {
501        if let Some(h) = msg.headers().get(Self::name()) {
502            Self::from_raw(h)
503        } else {
504            Err(crate::error::ParseError::Header)
505        }
506    }
507}
508
509impl fmt::Display for DispositionType {
510    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
511        match self {
512            DispositionType::Inline => write!(f, "inline"),
513            DispositionType::Attachment => write!(f, "attachment"),
514            DispositionType::FormData => write!(f, "form-data"),
515            DispositionType::Ext(ref s) => write!(f, "{}", s),
516        }
517    }
518}
519
520impl fmt::Display for DispositionParam {
521    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
522        static RE: Lazy<Regex> =
558            Lazy::new(|| Regex::new("[\x00-\x08\x10-\x1F\x7F\"\\\\]").unwrap());
559
560        match self {
561            DispositionParam::Name(ref value) => write!(f, "name={}", value),
562
563            DispositionParam::Filename(ref value) => {
564                write!(f, "filename=\"{}\"", RE.replace_all(value, "\\$0").as_ref())
565            }
566
567            DispositionParam::Unknown(ref name, ref value) => write!(
568                f,
569                "{}=\"{}\"",
570                name,
571                &RE.replace_all(value, "\\$0").as_ref()
572            ),
573
574            DispositionParam::FilenameExt(ref ext_value) => {
575                write!(f, "filename*={}", ext_value)
576            }
577
578            DispositionParam::UnknownExt(ref name, ref ext_value) => {
579                write!(f, "{}*={}", name, ext_value)
580            }
581        }
582    }
583}
584
585impl fmt::Display for ContentDisposition {
586    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
587        write!(f, "{}", self.disposition)?;
588        self.parameters
589            .iter()
590            .try_for_each(|param| write!(f, "; {}", param))
591    }
592}
593
594#[cfg(test)]
595mod tests {
596    use super::{ContentDisposition, DispositionParam, DispositionType};
597    use crate::http::header::{Charset, ExtendedValue, HeaderValue};
598
599    #[test]
600    fn test_from_raw_basic() {
601        assert!(ContentDisposition::from_raw(&HeaderValue::from_static("")).is_err());
602
603        let a =
604            HeaderValue::from_static("form-data; dummy=3; name=upload; filename=\"sample.png\"");
605        let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
606        let b = ContentDisposition {
607            disposition: DispositionType::FormData,
608            parameters: vec![
609                DispositionParam::Unknown("dummy".to_owned(), "3".to_owned()),
610                DispositionParam::Name("upload".to_owned()),
611                DispositionParam::Filename("sample.png".to_owned()),
612            ],
613        };
614        assert_eq!(a, b);
615
616        let a = HeaderValue::from_static("attachment; filename=\"image.jpg\"");
617        let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
618        let b = ContentDisposition {
619            disposition: DispositionType::Attachment,
620            parameters: vec![DispositionParam::Filename("image.jpg".to_owned())],
621        };
622        assert_eq!(a, b);
623
624        let a = HeaderValue::from_static("inline; filename=image.jpg");
625        let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
626        let b = ContentDisposition {
627            disposition: DispositionType::Inline,
628            parameters: vec![DispositionParam::Filename("image.jpg".to_owned())],
629        };
630        assert_eq!(a, b);
631
632        let a = HeaderValue::from_static(
633            "attachment; creation-date=\"Wed, 12 Feb 1997 16:29:51 -0500\"",
634        );
635        let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
636        let b = ContentDisposition {
637            disposition: DispositionType::Attachment,
638            parameters: vec![DispositionParam::Unknown(
639                String::from("creation-date"),
640                "Wed, 12 Feb 1997 16:29:51 -0500".to_owned(),
641            )],
642        };
643        assert_eq!(a, b);
644    }
645
646    #[test]
647    fn test_from_raw_extended() {
648        let a = HeaderValue::from_static(
649            "attachment; filename*=UTF-8''%c2%a3%20and%20%e2%82%ac%20rates",
650        );
651        let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
652        let b = ContentDisposition {
653            disposition: DispositionType::Attachment,
654            parameters: vec![DispositionParam::FilenameExt(ExtendedValue {
655                charset: Charset::Ext(String::from("UTF-8")),
656                language_tag: None,
657                value: vec![
658                    0xc2, 0xa3, 0x20, b'a', b'n', b'd', 0x20, 0xe2, 0x82, 0xac, 0x20, b'r', b'a',
659                    b't', b'e', b's',
660                ],
661            })],
662        };
663        assert_eq!(a, b);
664
665        let a = HeaderValue::from_static(
666            "attachment; filename*=UTF-8''%c2%a3%20and%20%e2%82%ac%20rates",
667        );
668        let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
669        let b = ContentDisposition {
670            disposition: DispositionType::Attachment,
671            parameters: vec![DispositionParam::FilenameExt(ExtendedValue {
672                charset: Charset::Ext(String::from("UTF-8")),
673                language_tag: None,
674                value: vec![
675                    0xc2, 0xa3, 0x20, b'a', b'n', b'd', 0x20, 0xe2, 0x82, 0xac, 0x20, b'r', b'a',
676                    b't', b'e', b's',
677                ],
678            })],
679        };
680        assert_eq!(a, b);
681    }
682
683    #[test]
684    fn test_from_raw_extra_whitespace() {
685        let a = HeaderValue::from_static(
686            "form-data  ; du-mmy= 3  ; name =upload ; filename =  \"sample.png\"  ; ",
687        );
688        let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
689        let b = ContentDisposition {
690            disposition: DispositionType::FormData,
691            parameters: vec![
692                DispositionParam::Unknown("du-mmy".to_owned(), "3".to_owned()),
693                DispositionParam::Name("upload".to_owned()),
694                DispositionParam::Filename("sample.png".to_owned()),
695            ],
696        };
697        assert_eq!(a, b);
698    }
699
700    #[test]
701    fn test_from_raw_unordered() {
702        let a = HeaderValue::from_static(
703            "form-data; dummy=3; filename=\"sample.png\" ; name=upload;",
704            );
706        let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
707        let b = ContentDisposition {
708            disposition: DispositionType::FormData,
709            parameters: vec![
710                DispositionParam::Unknown("dummy".to_owned(), "3".to_owned()),
711                DispositionParam::Filename("sample.png".to_owned()),
712                DispositionParam::Name("upload".to_owned()),
713            ],
714        };
715        assert_eq!(a, b);
716
717        let a = HeaderValue::from_str(
718            "attachment; filename*=iso-8859-1''foo-%E4.html; filename=\"foo-ä.html\"",
719        )
720        .unwrap();
721        let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
722        let b = ContentDisposition {
723            disposition: DispositionType::Attachment,
724            parameters: vec![
725                DispositionParam::FilenameExt(ExtendedValue {
726                    charset: Charset::Iso_8859_1,
727                    language_tag: None,
728                    value: b"foo-\xe4.html".to_vec(),
729                }),
730                DispositionParam::Filename("foo-ä.html".to_owned()),
731            ],
732        };
733        assert_eq!(a, b);
734    }
735
736    #[test]
737    fn test_from_raw_only_disp() {
738        let a = ContentDisposition::from_raw(&HeaderValue::from_static("attachment")).unwrap();
739        let b = ContentDisposition {
740            disposition: DispositionType::Attachment,
741            parameters: vec![],
742        };
743        assert_eq!(a, b);
744
745        let a = ContentDisposition::from_raw(&HeaderValue::from_static("inline ;")).unwrap();
746        let b = ContentDisposition {
747            disposition: DispositionType::Inline,
748            parameters: vec![],
749        };
750        assert_eq!(a, b);
751
752        let a =
753            ContentDisposition::from_raw(&HeaderValue::from_static("unknown-disp-param")).unwrap();
754        let b = ContentDisposition {
755            disposition: DispositionType::Ext(String::from("unknown-disp-param")),
756            parameters: vec![],
757        };
758        assert_eq!(a, b);
759    }
760
761    #[test]
762    fn from_raw_with_mixed_case() {
763        let a = HeaderValue::from_str(
764            "InLInE; fIlenAME*=iso-8859-1''foo-%E4.html; filEName=\"foo-ä.html\"",
765        )
766        .unwrap();
767        let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
768        let b = ContentDisposition {
769            disposition: DispositionType::Inline,
770            parameters: vec![
771                DispositionParam::FilenameExt(ExtendedValue {
772                    charset: Charset::Iso_8859_1,
773                    language_tag: None,
774                    value: b"foo-\xe4.html".to_vec(),
775                }),
776                DispositionParam::Filename("foo-ä.html".to_owned()),
777            ],
778        };
779        assert_eq!(a, b);
780    }
781
782    #[test]
783    fn from_raw_with_unicode() {
784        let a = HeaderValue::from_str("form-data; name=upload; filename=\"文件.webp\"").unwrap();
793        let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
794        let b = ContentDisposition {
795            disposition: DispositionType::FormData,
796            parameters: vec![
797                DispositionParam::Name(String::from("upload")),
798                DispositionParam::Filename(String::from("文件.webp")),
799            ],
800        };
801        assert_eq!(a, b);
802
803        let a = HeaderValue::from_str(
804            "form-data; name=upload; filename=\"余固知謇謇之為患兮,忍而不能舍也.pptx\"",
805        )
806        .unwrap();
807        let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
808        let b = ContentDisposition {
809            disposition: DispositionType::FormData,
810            parameters: vec![
811                DispositionParam::Name(String::from("upload")),
812                DispositionParam::Filename(String::from("余固知謇謇之為患兮,忍而不能舍也.pptx")),
813            ],
814        };
815        assert_eq!(a, b);
816    }
817
818    #[test]
819    fn test_from_raw_escape() {
820        let a = HeaderValue::from_static(
821            "form-data; dummy=3; name=upload; filename=\"s\\amp\\\"le.png\"",
822        );
823        let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
824        let b = ContentDisposition {
825            disposition: DispositionType::FormData,
826            parameters: vec![
827                DispositionParam::Unknown("dummy".to_owned(), "3".to_owned()),
828                DispositionParam::Name("upload".to_owned()),
829                DispositionParam::Filename(
830                    ['s', 'a', 'm', 'p', '\"', 'l', 'e', '.', 'p', 'n', 'g']
831                        .iter()
832                        .collect(),
833                ),
834            ],
835        };
836        assert_eq!(a, b);
837    }
838
839    #[test]
840    fn test_from_raw_semicolon() {
841        let a = HeaderValue::from_static("form-data; filename=\"A semicolon here;.pdf\"");
842        let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
843        let b = ContentDisposition {
844            disposition: DispositionType::FormData,
845            parameters: vec![DispositionParam::Filename(String::from(
846                "A semicolon here;.pdf",
847            ))],
848        };
849        assert_eq!(a, b);
850    }
851
852    #[test]
853    fn test_from_raw_unnecessary_percent_decode() {
854        let a = HeaderValue::from_static(
865            "form-data; name=photo; filename=\"%74%65%73%74%2e%70%6e%67\"",
866        );
867        let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
868        let b = ContentDisposition {
869            disposition: DispositionType::FormData,
870            parameters: vec![
871                DispositionParam::Name("photo".to_owned()),
872                DispositionParam::Filename(String::from("%74%65%73%74%2e%70%6e%67")),
873            ],
874        };
875        assert_eq!(a, b);
876
877        let a = HeaderValue::from_static("form-data; name=photo; filename=\"%74%65%73%74.png\"");
878        let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
879        let b = ContentDisposition {
880            disposition: DispositionType::FormData,
881            parameters: vec![
882                DispositionParam::Name("photo".to_owned()),
883                DispositionParam::Filename(String::from("%74%65%73%74.png")),
884            ],
885        };
886        assert_eq!(a, b);
887    }
888
889    #[test]
890    fn test_from_raw_param_value_missing() {
891        let a = HeaderValue::from_static("form-data; name=upload ; filename=");
892        assert!(ContentDisposition::from_raw(&a).is_err());
893
894        let a = HeaderValue::from_static("attachment; dummy=; filename=invoice.pdf");
895        assert!(ContentDisposition::from_raw(&a).is_err());
896
897        let a = HeaderValue::from_static("inline; filename=  ");
898        assert!(ContentDisposition::from_raw(&a).is_err());
899
900        let a = HeaderValue::from_static("inline; filename=\"\"");
901        assert!(ContentDisposition::from_raw(&a)
902            .expect("parse cd")
903            .get_filename()
904            .expect("filename")
905            .is_empty());
906    }
907
908    #[test]
909    fn test_from_raw_param_name_missing() {
910        let a = HeaderValue::from_static("inline; =\"test.txt\"");
911        assert!(ContentDisposition::from_raw(&a).is_err());
912
913        let a = HeaderValue::from_static("inline; =diary.odt");
914        assert!(ContentDisposition::from_raw(&a).is_err());
915
916        let a = HeaderValue::from_static("inline; =");
917        assert!(ContentDisposition::from_raw(&a).is_err());
918    }
919
920    #[test]
921    fn test_display_extended() {
922        let as_string = "attachment; filename*=UTF-8'en'%C2%A3%20and%20%E2%82%AC%20rates";
923        let a = HeaderValue::from_static(as_string);
924        let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
925        let display_rendered = format!("{}", a);
926        assert_eq!(as_string, display_rendered);
927
928        let a = HeaderValue::from_static("attachment; filename=colourful.csv");
929        let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
930        let display_rendered = format!("{}", a);
931        assert_eq!(
932            "attachment; filename=\"colourful.csv\"".to_owned(),
933            display_rendered
934        );
935    }
936
937    #[test]
938    fn test_display_quote() {
939        let as_string = "form-data; name=upload; filename=\"Quote\\\"here.png\"";
940        as_string
941            .find(['\\', '\"'].iter().collect::<String>().as_str())
942            .unwrap(); let a = HeaderValue::from_static(as_string);
944        let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
945        let display_rendered = format!("{}", a);
946        assert_eq!(as_string, display_rendered);
947    }
948
949    #[test]
950    fn test_display_space_tab() {
951        let as_string = "form-data; name=upload; filename=\"Space here.png\"";
952        let a = HeaderValue::from_static(as_string);
953        let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
954        let display_rendered = format!("{}", a);
955        assert_eq!(as_string, display_rendered);
956
957        let a: ContentDisposition = ContentDisposition {
958            disposition: DispositionType::Inline,
959            parameters: vec![DispositionParam::Filename(String::from("Tab\there.png"))],
960        };
961        let display_rendered = format!("{}", a);
962        assert_eq!("inline; filename=\"Tab\x09here.png\"", display_rendered);
963    }
964
965    #[test]
966    fn test_display_control_characters() {
967        let a: ContentDisposition = ContentDisposition {
978            disposition: DispositionType::Inline,
979            parameters: vec![DispositionParam::Filename(String::from("bell\x07.png"))],
980        };
981        let display_rendered = format!("{}", a);
982        assert_eq!("inline; filename=\"bell\\\x07.png\"", display_rendered);
983    }
984
985    #[test]
986    fn test_param_methods() {
987        let param = DispositionParam::Filename(String::from("sample.txt"));
988        assert!(param.is_filename());
989        assert_eq!(param.as_filename().unwrap(), "sample.txt");
990
991        let param = DispositionParam::Unknown(String::from("foo"), String::from("bar"));
992        assert!(param.is_unknown("foo"));
993        assert_eq!(param.as_unknown("fOo"), Some("bar"));
994    }
995
996    #[test]
997    fn test_disposition_methods() {
998        let cd = ContentDisposition {
999            disposition: DispositionType::FormData,
1000            parameters: vec![
1001                DispositionParam::Unknown("dummy".to_owned(), "3".to_owned()),
1002                DispositionParam::Name("upload".to_owned()),
1003                DispositionParam::Filename("sample.png".to_owned()),
1004            ],
1005        };
1006        assert_eq!(cd.get_name(), Some("upload"));
1007        assert_eq!(cd.get_unknown("dummy"), Some("3"));
1008        assert_eq!(cd.get_filename(), Some("sample.png"));
1009        assert_eq!(cd.get_unknown_ext("dummy"), None);
1010        assert_eq!(cd.get_unknown("duMMy"), Some("3"));
1011    }
1012}