diff --git a/CHANGELOG.md b/CHANGELOG.md index 80a70444..3ca46910 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ ``` SQLPage now keeps the variable value, producing `https://api.example.com/john.doe` as expected. +- Word documents stored as `BLOB`, `BYTEA`, or `VARBINARY` in database columns are now detected correctly as `.docx`. Before, `.docx` files were parsed as `application/zip`; now they report as `application/vnd.openxmlformats-officedocument.wordprocessingml.document`. - Errors from a failed migration now point at the file and the SQL that failed. Before a reversible migration could be reported with the contents of its `.down.sql` half, and any migration with a multi-word name was reported as `0001_add new users.sql` rather than `0001_add_new_users.sql`. - A request that resolves to a directory now returns a 404 page. Directory names that contain a dot are routed to the static file handler, which used to fail with a server error instead. - A `content_security_policy` that does not contain `'nonce-{NONCE}'` is now sent as written, instead of being silently dropped and leaving the response with no `Content-Security-Policy` header at all. Setting the option to the empty string still disables the header, as documented. diff --git a/src/webserver/database/blob_to_data_url.rs b/src/webserver/database/blob_to_data_url.rs index c451e2ba..cd2905ed 100644 --- a/src/webserver/database/blob_to_data_url.rs +++ b/src/webserver/database/blob_to_data_url.rs @@ -31,7 +31,7 @@ pub fn detect_mime_type(bytes: &[u8]) -> &'static str { // Check for Office document types in ZIP central directory if bytes.len() >= 50 { let central_dir = &bytes[30..bytes.len().min(50)]; - if central_dir.windows(6).any(|w| w == b"word/") { + if central_dir.windows(5).any(|w| w == b"word/") { return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; } if central_dir.windows(3).any(|w| w == b"xl/") { @@ -189,6 +189,34 @@ mod tests { ); } + fn zip_starting_with(first_entry_name: &[u8]) -> Vec { + let mut blob = b"PK\x03\x04".to_vec(); + blob.resize(30, 0); + blob.extend_from_slice(first_entry_name); + blob.resize(50, 0); + blob + } + + #[test] + fn test_detect_office_documents() { + assert_eq!( + detect_mime_type(&zip_starting_with(b"word/document.xml")), + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + ); + assert_eq!( + detect_mime_type(&zip_starting_with(b"xl/workbook.xml")), + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + ); + assert_eq!( + detect_mime_type(&zip_starting_with(b"ppt/presentation.xml")), + "application/vnd.openxmlformats-officedocument.presentationml.presentation" + ); + assert_eq!( + detect_mime_type(&zip_starting_with(b"other/thing.txt")), + "application/zip" + ); + } + #[test] fn decodes_base64_and_percent_encoded_data_urls() { assert_eq!(