Skip to main content

sofie_storage/
disposition.rs

1use sofie_core::encoding::percent;
2
3use crate::Disposition;
4
5/// RFC 6266: an ASCII `filename` for old clients and the UTF-8 `filename*` (RFC 5987) beside it.
6/// Quotes, backslashes and characters that mislead a reader are dropped from both.
7#[must_use]
8pub fn content_disposition(disposition: Disposition, filename: Option<&str>) -> String {
9    let mut value = disposition.as_str().to_owned();
10    let Some(filename) = filename.map(shown).filter(|name| !name.is_empty()) else {
11        return value;
12    };
13    value.push_str("; filename=\"");
14    value.extend(
15        filename
16            .chars()
17            .map(|character| if character.is_ascii() { character } else { '_' }),
18    );
19    value.push_str("\"; filename*=UTF-8''");
20    percent::push_encoded(&mut value, &filename, is_attribute_character);
21    value
22}
23
24/// RFC 5987's `attr-char`: what `filename*` may carry unescaped.
25fn is_attribute_character(byte: u8) -> bool {
26    byte.is_ascii_alphanumeric() || b"!#$&+-.^_`|~".contains(&byte)
27}
28
29fn shown(filename: &str) -> String {
30    filename
31        .chars()
32        .filter(|character| {
33            !sofie_core::misleads_display(*character) && !matches!(character, '"' | '\\')
34        })
35        .collect()
36}
37
38#[cfg(test)]
39mod tests {
40    use super::*;
41
42    #[test]
43    fn a_name_is_given_as_ascii_and_as_utf_8() {
44        assert_eq!(
45            content_disposition(Disposition::Attachment, None),
46            "attachment"
47        );
48        assert_eq!(
49            content_disposition(Disposition::Attachment, Some("Fatura Şubat.pdf")),
50            "attachment; filename=\"Fatura _ubat.pdf\"; filename*=UTF-8''Fatura%20%C5%9Eubat.pdf"
51        );
52        assert_eq!(
53            content_disposition(Disposition::Inline, Some("a\"b\r\n;c%.png")),
54            "inline; filename=\"ab;c%.png\"; filename*=UTF-8''ab%3Bc%25.png"
55        );
56    }
57}