tuwunel_core/utils/
content_disposition.rs1use std::borrow::Cow;
2
3use ruma::http_headers::{ContentDisposition, ContentDispositionType};
4
5use crate::debug_info;
6
7const ALLOWED_INLINE_CONTENT_TYPES: [&str; 26] = [
9 "application/json",
11 "application/ld+json",
12 "audio/aac",
13 "audio/flac",
14 "audio/mp4",
15 "audio/mpeg",
16 "audio/ogg",
17 "audio/wav",
18 "audio/wave",
19 "audio/webm",
20 "audio/x-flac",
21 "audio/x-pn-wav",
22 "audio/x-wav",
23 "image/apng",
24 "image/avif",
25 "image/gif",
26 "image/jpeg",
27 "image/png",
28 "image/webp",
29 "text/css",
30 "text/csv",
31 "text/plain",
32 "video/mp4",
33 "video/ogg",
34 "video/quicktime",
35 "video/webm",
36];
37
38#[must_use]
42pub fn content_disposition_type(content_type: Option<&str>) -> ContentDispositionType {
43 let Some(content_type) = content_type else {
44 debug_info!("No Content-Type was given, assuming attachment for Content-Disposition");
45 return ContentDispositionType::Attachment;
46 };
47
48 debug_assert!(
49 ALLOWED_INLINE_CONTENT_TYPES.is_sorted(),
50 "ALLOWED_INLINE_CONTENT_TYPES is not sorted"
51 );
52
53 let content_type: Cow<'_, str> = content_type
54 .split(';')
55 .next()
56 .unwrap_or(content_type)
57 .to_ascii_lowercase()
58 .into();
59
60 if ALLOWED_INLINE_CONTENT_TYPES
61 .binary_search(&content_type.as_ref())
62 .is_ok()
63 {
64 ContentDispositionType::Inline
65 } else {
66 ContentDispositionType::Attachment
67 }
68}
69
70#[tracing::instrument(level = "debug")]
73pub fn sanitise_filename(filename: &str) -> String {
74 sanitize_filename::sanitize_with_options(filename, sanitize_filename::Options {
75 truncate: false,
76 ..Default::default()
77 })
78}
79
80pub fn make_content_disposition(
89 content_disposition: Option<&ContentDisposition>,
90 content_type: Option<&str>,
91 filename: Option<&str>,
92) -> ContentDisposition {
93 ContentDisposition::new(content_disposition_type(content_type)).with_filename(
94 filename
95 .or_else(|| {
96 content_disposition
97 .and_then(|content_disposition| content_disposition.filename.as_deref())
98 })
99 .map(sanitise_filename),
100 )
101}
102
103#[cfg(test)]
104mod tests {
105 #[test]
106 fn string_sanitisation() {
107 const SAMPLE: &str = "🏳️⚧️this\\r\\n įs \r\\n ä \\r\nstrïng 🥴that\n\r \
108 ../../../../../../../may be\r\n malicious🏳️⚧️";
109 const SANITISED: &str = "🏳️⚧️thisrn įs n ä rstrïng 🥴that ..............may be malicious🏳️⚧️";
110
111 let options = sanitize_filename::Options {
112 windows: true,
113 truncate: true,
114 replacement: "",
115 };
116
117 println!("{SAMPLE}");
119 println!("{}", sanitize_filename::sanitize_with_options(SAMPLE, options.clone()));
120 println!("{SAMPLE:?}");
121 println!("{:?}", sanitize_filename::sanitize_with_options(SAMPLE, options.clone()));
122
123 assert_eq!(SANITISED, sanitize_filename::sanitize_with_options(SAMPLE, options.clone()));
124 }
125
126 #[test]
127 fn empty_sanitisation() {
128 use crate::utils::string::EMPTY;
129
130 let result =
131 sanitize_filename::sanitize_with_options(EMPTY, sanitize_filename::Options {
132 windows: true,
133 truncate: true,
134 replacement: "",
135 });
136
137 assert_eq!(EMPTY, result);
138 }
139}