continuwuity/src/core/utils/content_disposition.rs
strawberry bf10ff65a4 media: ignore Content-Type params, use binary_search
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-05 17:28:51 -04:00

139 lines
3.6 KiB
Rust
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use crate::debug_info;
const ATTACHMENT: &str = "attachment";
const INLINE: &str = "inline";
/// as defined by MSC2702
const ALLOWED_INLINE_CONTENT_TYPES: [&str; 26] = [
// keep sorted
"application/json",
"application/ld+json",
"audio/aac",
"audio/flac",
"audio/mp4",
"audio/mpeg",
"audio/ogg",
"audio/wav",
"audio/wave",
"audio/webm",
"audio/x-flac",
"audio/x-pn-wav",
"audio/x-wav",
"image/apng",
"image/avif",
"image/gif",
"image/jpeg",
"image/png",
"image/webp",
"text/css",
"text/csv",
"text/plain",
"video/mp4",
"video/ogg",
"video/quicktime",
"video/webm",
];
/// Returns a Content-Disposition of `attachment` or `inline`, depending on the
/// Content-Type against MSC2702 list of safe inline Content-Types
/// (`ALLOWED_INLINE_CONTENT_TYPES`)
#[must_use]
pub fn content_disposition_type(content_type: &Option<String>) -> &'static str {
let Some(content_type) = content_type else {
debug_info!("No Content-Type was given, assuming attachment for Content-Disposition");
return ATTACHMENT;
};
// is_sorted is unstable
/* debug_assert!(ALLOWED_INLINE_CONTENT_TYPES.is_sorted(),
* "ALLOWED_INLINE_CONTENT_TYPES is not sorted"); */
let content_type = content_type
.split(';')
.next()
.unwrap_or(content_type)
.to_ascii_lowercase();
if ALLOWED_INLINE_CONTENT_TYPES
.binary_search(&content_type.as_str())
.is_ok()
{
INLINE
} else {
ATTACHMENT
}
}
/// sanitises the file name for the Content-Disposition using
/// `sanitize_filename` crate
#[tracing::instrument]
pub fn sanitise_filename(filename: String) -> String {
let options = sanitize_filename::Options {
truncate: false,
..Default::default()
};
sanitize_filename::sanitize_with_options(filename, options)
}
/// creates the final Content-Disposition based on whether the filename exists
/// or not, or if a requested filename was specified (media download with
/// filename)
///
/// if filename exists:
/// `Content-Disposition: attachment/inline; filename=filename.ext`
///
/// else: `Content-Disposition: attachment/inline`
pub fn make_content_disposition(
content_type: &Option<String>, content_disposition: Option<String>, req_filename: Option<String>,
) -> String {
let filename: String;
if let Some(req_filename) = req_filename {
filename = sanitise_filename(req_filename);
} else {
filename = content_disposition.map_or_else(String::new, |content_disposition| {
let (_, filename) = content_disposition
.split_once("filename=")
.unwrap_or(("", ""));
if filename.is_empty() {
String::new()
} else {
sanitise_filename(filename.to_owned())
}
});
};
if !filename.is_empty() {
// Content-Disposition: attachment/inline; filename=filename.ext
format!("{}; filename={}", content_disposition_type(content_type), filename)
} else {
// Content-Disposition: attachment/inline
String::from(content_disposition_type(content_type))
}
}
#[cfg(test)]
mod tests {
#[test]
fn string_sanitisation() {
const SAMPLE: &str =
"🏳this\\r\\n įs \r\\n ä \\r\nstrïng 🥴that\n\r ../../../../../../../may be\r\n malicious🏳";
const SANITISED: &str = "🏳thisrn įs n ä rstrïng 🥴that ..............may be malicious🏳";
let options = sanitize_filename::Options {
windows: true,
truncate: true,
replacement: "",
};
// cargo test -- --nocapture
println!("{SAMPLE}");
println!("{}", sanitize_filename::sanitize_with_options(SAMPLE, options.clone()));
println!("{SAMPLE:?}");
println!("{:?}", sanitize_filename::sanitize_with_options(SAMPLE, options.clone()));
assert_eq!(SANITISED, sanitize_filename::sanitize_with_options(SAMPLE, options.clone()));
}
}