From a234fa85937ad274dfeaf4482d3cfae35ddd08fc Mon Sep 17 00:00:00 2001 From: Jose Quintana Date: Wed, 29 May 2024 06:15:29 +0200 Subject: [PATCH] fix: incorrect `Content-Encoding` for pre-compressed zstd files --- src/compression_static.rs | 10 ++--- src/fs/meta.rs | 3 +- src/static_files.rs | 23 +++++++--- tests/compression_static.rs | 70 ++++++++++++++++++++++++++++++ tests/dir_listing.rs | 2 +- tests/fixtures/public/main.js.zst | Bin 0 -> 162 bytes 6 files changed, 94 insertions(+), 14 deletions(-) create mode 100644 tests/fixtures/public/main.js.zst diff --git a/src/compression_static.rs b/src/compression_static.rs index 88c6799a..6dd47a04 100644 --- a/src/compression_static.rs +++ b/src/compression_static.rs @@ -19,13 +19,13 @@ use crate::headers_ext::ContentCoding; use crate::Error; /// It defines the pre-compressed file variant metadata of a particular file path. -pub struct CompressedFileVariant<'a> { +pub struct CompressedFileVariant { /// Current file path. pub file_path: PathBuf, /// The metadata of the current file. pub metadata: Metadata, - /// The file extension. - pub extension: &'a str, + /// The content encoding based on the file extension. + pub encoding: ContentCoding, } /// Initializes static compression. @@ -57,7 +57,7 @@ pub(crate) fn post_process( pub async fn precompressed_variant<'a>( file_path: &Path, headers: &'a HeaderMap, -) -> Option> { +) -> Option { tracing::trace!( "preparing pre-compressed file variant path of {}", file_path.display() @@ -119,7 +119,7 @@ pub async fn precompressed_variant<'a>( return Some(CompressedFileVariant { file_path, metadata, - extension: if comp_ext == "gz" { "gzip" } else { comp_ext }, + encoding, }); } diff --git a/src/fs/meta.rs b/src/fs/meta.rs index 44a1eb09..8a1a782c 100644 --- a/src/fs/meta.rs +++ b/src/fs/meta.rs @@ -10,6 +10,7 @@ use http::StatusCode; use std::fs::Metadata; use std::path::{Path, PathBuf}; +use crate::headers_ext::ContentCoding; use crate::Result; /// It defines a composed file metadata structure containing the current file @@ -24,7 +25,7 @@ pub(crate) struct FileMetadata<'a> { // If either `file_path` or `precompressed_variant` is a directory. pub is_dir: bool, // The precompressed file variant for the current `file_path`. - pub precompressed_variant: Option<(PathBuf, &'a str)>, + pub precompressed_variant: Option<(PathBuf, ContentCoding)>, } /// Try to find the file system metadata for the given file path or return a `Not Found` error. diff --git a/src/static_files.rs b/src/static_files.rs index f5e32434..6dccc373 100644 --- a/src/static_files.rs +++ b/src/static_files.rs @@ -182,13 +182,22 @@ pub async fn handle<'a>(opts: &HandleOpts<'a>) -> Result val, + Err(err) => { + tracing::error!( + "unable to parse header value from content encoding: {:?}", + err + ); + return Err(StatusCode::INTERNAL_SERVER_ERROR); + } + }; + resp.headers_mut().insert(CONTENT_ENCODING, encoding); return Ok(StaticFileResponse { resp, @@ -245,7 +254,7 @@ async fn get_composed_file_metadata<'a>( file_path, metadata: p.metadata, is_dir: false, - precompressed_variant: Some((p.file_path, p.extension)), + precompressed_variant: Some((p.file_path, p.encoding)), }); } } @@ -293,7 +302,7 @@ async fn get_composed_file_metadata<'a>( file_path, metadata: p.metadata, is_dir: false, - precompressed_variant: Some((p.file_path, p.extension)), + precompressed_variant: Some((p.file_path, p.encoding)), }); } } @@ -324,7 +333,7 @@ async fn get_composed_file_metadata<'a>( file_path, metadata: p.metadata, is_dir: false, - precompressed_variant: Some((p.file_path, p.extension)), + precompressed_variant: Some((p.file_path, p.encoding)), }); } } @@ -362,7 +371,7 @@ async fn get_composed_file_metadata<'a>( file_path, metadata: p.metadata, is_dir: false, - precompressed_variant: Some((p.file_path, p.extension)), + precompressed_variant: Some((p.file_path, p.encoding)), }); } } diff --git a/tests/compression_static.rs b/tests/compression_static.rs index e044c06d..fff1c7c6 100644 --- a/tests/compression_static.rs +++ b/tests/compression_static.rs @@ -258,4 +258,74 @@ mod tests { .await .expect("unexpected error response on `handle` function"); } + + #[tokio::test] + async fn compression_static_zstd_file_exists() { + let mut headers = HeaderMap::new(); + headers.insert( + http::header::ACCEPT_ENCODING, + "gzip, deflate, br, zstd".parse().unwrap(), + ); + + let mainjs_zst_path = PathBuf::from("tests/fixtures/public/main.js.zst"); + let mainjs_zst_path_public = public_dir().join("main.js.zst"); + std::fs::copy(&mainjs_zst_path, &mainjs_zst_path_public) + .expect("unexpected error copying fixture file"); + + let result = static_files::handle(&HandleOpts { + method: &Method::GET, + headers: &headers, + base_path: &public_dir(), + uri_path: "main.js", + uri_query: None, + #[cfg(feature = "directory-listing")] + dir_listing: false, + #[cfg(feature = "directory-listing")] + dir_listing_order: 6, + #[cfg(feature = "directory-listing")] + dir_listing_format: &DirListFmt::Html, + redirect_trailing_slash: true, + #[cfg(any( + feature = "compression", + feature = "compression-deflate", + feature = "compression-gzip", + feature = "compression-deflate", + feature = "compression-brotli", + feature = "compression-zstd" + ))] + compression_static: true, + ignore_hidden_files: false, + index_files: &[], + }) + .await + .expect("unexpected error response on `handle` function"); + let mut res = result.resp; + + let mainjs_zst_buf = + std::fs::read(&mainjs_zst_path).expect("unexpected error when reading index.html.gz"); + let mainjs_zst_buf = Bytes::from(mainjs_zst_buf); + + std::fs::remove_file(mainjs_zst_path_public).unwrap(); + + let headers = res.headers(); + + assert_eq!(res.status(), 200); + assert!(!headers.contains_key("content-length")); + assert_eq!(headers["content-encoding"], "zstd"); + assert_eq!(headers["accept-ranges"], "bytes"); + assert!(!headers["last-modified"].is_empty()); + assert_eq!( + &headers["content-type"], "application/javascript", + "content-type is not html" + ); + + let body = hyper::body::to_bytes(res.body_mut()) + .await + .expect("unexpected bytes error during `body` conversion"); + + assert_eq!( + body, mainjs_zst_buf, + "body and mainjs_zst_buf are not equal in length" + ); + } } diff --git a/tests/dir_listing.rs b/tests/dir_listing.rs index 01edfd9b..fe97d5e8 100644 --- a/tests/dir_listing.rs +++ b/tests/dir_listing.rs @@ -266,7 +266,7 @@ mod tests { if method == Method::GET { let entries: Vec = serde_json::from_str(body_str).unwrap(); - assert_eq!(entries.len(), 4); + assert_eq!(entries.len(), 5); let first_entry = entries.first().unwrap(); assert_eq!(first_entry.name, "spécial-directöry.net"); diff --git a/tests/fixtures/public/main.js.zst b/tests/fixtures/public/main.js.zst new file mode 100644 index 0000000000000000000000000000000000000000..bd865e879eaa15be766ad74a2e79fb6dc7c3b7c9 GIT binary patch literal 162 zcmV;T0A2qmwJ-eySgiyABFGyZU^UhOi1O0?Z9m&6lqCopS@#Q(7_~ZgDb4^W}69tnCKMFi4CK&guy0+_P;Jmo2>x-9>;ZTewA(ZqhDnT zQO0*Mbud=PVq