diff --git a/rustfs/src/server/compress.rs b/rustfs/src/server/compress.rs index f43c68ffb..2e54e3199 100644 --- a/rustfs/src/server/compress.rs +++ b/rustfs/src/server/compress.rs @@ -213,13 +213,21 @@ impl HttpCompressionConfig { /// Extract filename from Content-Disposition header /// Format: attachment; filename="example.txt" or attachment; filename=example.txt pub(crate) fn extract_filename_from_content_disposition(header_value: &str) -> Option { - // Look for filename= or filename*= parameter - let lower = header_value.to_lowercase(); + // Use ASCII lowercasing so byte offsets remain valid UTF-8 boundaries in header_value. + let lower = header_value.to_ascii_lowercase(); + let mut search_from = 0; + + // Try to find a semicolon-delimited filename="..." or filename=... parameter. + while let Some(rel_idx) = lower[search_from..].find("filename=") { + let idx = search_from + rel_idx; + let prefix = header_value[..idx].trim_end(); + if !prefix.is_empty() && !prefix.ends_with(';') { + search_from = idx + "filename=".len(); + continue; + } - // Try to find filename="..." or filename=... - if let Some(idx) = lower.find("filename=") { let start = idx + "filename=".len(); - let rest = &header_value[start..]; + let rest = header_value[start..].trim_start(); // Check if it's quoted if let Some(stripped) = rest.strip_prefix('"') { @@ -232,6 +240,8 @@ impl HttpCompressionConfig { let end = rest.find(';').unwrap_or(rest.len()); return Some(rest[..end].trim().to_string()); } + + search_from = start; } None @@ -646,6 +656,24 @@ mod tests { Some("test.json".to_string()) ); + // Multi-byte Unicode characters whose UTF-8 byte length changes under to_lowercase() + assert_eq!( + HttpCompressionConfig::extract_filename_from_content_disposition(r#"attachment; title="İ"; filename="report.txt""#), + Some("report.txt".to_string()) + ); + assert_eq!( + HttpCompressionConfig::extract_filename_from_content_disposition(r#"attachment; unit="K"; FILENAME="metrics.csv""#), + Some("metrics.csv".to_string()) + ); + + // Preceding parameter whose name ends with "filename" + assert_eq!( + HttpCompressionConfig::extract_filename_from_content_disposition( + r#"attachment; x-filename="decoy.txt"; filename="bundle.zip""# + ), + Some("bundle.zip".to_string()) + ); + // No filename assert_eq!(HttpCompressionConfig::extract_filename_from_content_disposition("inline"), None); }