fn sanitize_filename(name: &str) -> String {
let name = name.trim();
let name: String = name.chars().filter(|c| *c != '\0').collect();
let name = name.rsplit(['/', '\\']).next().unwrap_or(&name);
let name = name.trim_start_matches('.');
let name: String = name.split_whitespace().collect::<Vec<_>>().join(" ");
let mut result = String::new();
let mut byte_count = 0;
for ch in name.chars() {
let char_bytes = ch.len_utf8();
if byte_count + char_bytes > 255 {
break;
}
result.push(ch);
byte_count += char_bytes;
}
result
}
pub(crate) fn extract_filename(content_disposition: &str) -> Option<String> {
let mut fallback_filename: Option<String> = None;
for part in content_disposition.split(';') {
let part = part.trim();
if let Some(rest) = part.strip_prefix("filename*=") {
let rest = rest.trim_matches('"');
let mut it = rest.splitn(3, '\'');
let _charset = it.next();
let _lang = it.next();
if let Some(value) = it.next() {
return Some(sanitize_filename(value));
}
continue;
}
if let Some(filename) = part.strip_prefix("filename=") {
if fallback_filename.is_none() {
fallback_filename = Some(sanitize_filename(filename.trim_matches('"')));
}
}
}
fallback_filename
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extract_filename_utf8_star() {
let raw = "attachment; filename=\"upload_all.rs\"; filename*=UTF-8''upload_all.rs";
assert_eq!(extract_filename(raw).as_deref(), Some("upload_all.rs"));
}
#[test]
fn extract_filename_star_missing_charset() {
let raw = "attachment; filename*=''missing_utf8.txt";
assert_eq!(extract_filename(raw).as_deref(), Some("missing_utf8.txt"));
}
#[test]
fn extract_filename_star_malformed() {
let raw = "attachment; filename*=UTF-8";
assert_eq!(extract_filename(raw).as_deref(), None);
}
#[test]
fn extract_filename_quoted() {
let raw = "attachment; filename=\"simple.txt\"";
assert_eq!(extract_filename(raw).as_deref(), Some("simple.txt"));
}
#[test]
fn extract_filename_unquoted() {
let raw = "attachment; filename=simple.txt";
assert_eq!(extract_filename(raw).as_deref(), Some("simple.txt"));
}
#[test]
fn extract_filename_multiple_parts() {
let raw = "attachment; charset=utf-8; filename*=UTF-8''complex%20name.txt; other=value";
assert_eq!(extract_filename(raw).as_deref(), Some("complex%20name.txt"));
}
#[test]
fn extract_filename_empty() {
assert_eq!(extract_filename("").as_deref(), None);
}
#[test]
fn sanitize_removes_path_traversal() {
let raw = r#"attachment; filename="../../../etc/passwd""#;
let result = extract_filename(raw);
assert_eq!(result.as_deref(), Some("passwd"));
}
#[test]
fn sanitize_removes_path_separators() {
let raw = r#"attachment; filename="foo/bar/baz.txt""#;
let result = extract_filename(raw);
assert_eq!(result.as_deref(), Some("baz.txt"));
}
#[test]
fn sanitize_removes_backslash() {
let raw = r#"attachment; filename="foo\bar\baz.txt""#;
let result = extract_filename(raw);
assert_eq!(result.as_deref(), Some("baz.txt"));
}
#[test]
fn sanitize_removes_null_bytes() {
let raw = "attachment; filename=\"foo\0bar.txt\"";
let result = extract_filename(raw);
assert_eq!(result.as_deref(), Some("foobar.txt"));
}
#[test]
fn sanitize_limits_length() {
let long_name = "a".repeat(300);
let raw = format!("attachment; filename=\"{long_name}\"");
let result = extract_filename(&raw);
let name = result.unwrap();
assert!(name.len() <= 255);
}
#[test]
fn sanitize_removes_leading_dots() {
let raw = r#"attachment; filename=".hidden""#;
let result = extract_filename(raw);
assert_eq!(result.as_deref(), Some("hidden"));
}
}