From 0a3960b0317b030cf3d3345f2d1abb83c1aebb91 Mon Sep 17 00:00:00 2001 From: Jeb Rosen Date: Sat, 27 Apr 2019 08:41:49 -0700 Subject: [PATCH] Clean up 'compression' module and documentation. --- contrib/lib/Cargo.toml | 2 +- contrib/lib/src/compression/fairing.rs | 123 ++++++------------ contrib/lib/src/compression/mod.rs | 101 ++++++++------ contrib/lib/src/compression/responder.rs | 55 ++++---- contrib/lib/src/lib.rs | 11 +- .../{compressed.rs => compress_responder.rs} | 25 ++-- ...{compression.rs => compression_fairing.rs} | 31 ++--- scripts/test.sh | 2 + 8 files changed, 150 insertions(+), 200 deletions(-) rename contrib/lib/tests/{compressed.rs => compress_responder.rs} (92%) rename contrib/lib/tests/{compression.rs => compression_fairing.rs} (92%) diff --git a/contrib/lib/Cargo.toml b/contrib/lib/Cargo.toml index d8db934f..c52b2b20 100644 --- a/contrib/lib/Cargo.toml +++ b/contrib/lib/Cargo.toml @@ -80,7 +80,7 @@ r2d2-memcache = { version = "0.3", optional = true } time = { version = "0.1.40", optional = true } # Compression dependencies -brotli = { version = "2.5", optional = true } +brotli = { version = "3.3", optional = true } flate2 = { version = "1.0", optional = true } [target.'cfg(debug_assertions)'.dependencies] diff --git a/contrib/lib/src/compression/fairing.rs b/contrib/lib/src/compression/fairing.rs index 0303e7e3..f2d1d700 100644 --- a/contrib/lib/src/compression/fairing.rs +++ b/contrib/lib/src/compression/fairing.rs @@ -1,22 +1,15 @@ -//! Automatic response compression. -//! -//! See the [`Compression`](compression::fairing::Compression) type for further -//! details. - use rocket::config::{ConfigError, Value}; use rocket::fairing::{Fairing, Info, Kind}; use rocket::http::MediaType; use rocket::Rocket; use rocket::{Request, Response}; -crate use super::CompressionUtils; - -crate struct Context { - crate exclusions: Vec, +struct Context { + exclusions: Vec, } -impl Context { - crate fn new() -> Context { +impl Default for Context { + fn default() -> Context { Context { exclusions: vec![ MediaType::parse_flexible("application/gzip").unwrap(), @@ -28,56 +21,30 @@ impl Context { ], } } - crate fn with_exclusions(excls: Vec) -> Context { - Context { exclusions: excls } - } } -/// The Compression type implements brotli and gzip compression for responses in -/// accordance with the Accept-Encoding header. If accepted, brotli compression -/// is preferred over gzip. +/// Compresses all responses with Brotli or Gzip compression. /// -/// In the brotli compression mode (using the -/// [rust-brotli](https://github.com/dropbox/rust-brotli) crate), quality is set -/// to 2 in order to achieve fast compression with a compression ratio similar -/// to gzip. When appropriate, brotli's text and font compression modes are -/// used. +/// Compression is done in the same manner as the [`Compress`](super::Compress) +/// responder. /// -/// In the gzip compression mode (using the -/// [flate2](https://github.com/alexcrichton/flate2-rs) crate), quality is set -/// to the default (9) in order to have good compression ratio. +/// By default, the fairing does not compress responses with a `Content-Type` +/// matching any of the following: /// -/// This fairing does not compress responses that already have a -/// `Content-Encoding` header. -/// -/// This fairing ignores the responses with a `Content-Type` matching any of -/// the following default types: -/// -/// - application/gzip -/// - application/brotli -/// - image/* -/// - video/* -/// - application/wasm -/// - application/octet-stream +/// - `application/gzip` +/// - `application/zip` +/// - `image/*` +/// - `video/*` +/// - `application/wasm` +/// - `application/octet-stream` /// /// The excluded types can be changed changing the `compress.exclude` Rocket /// configuration property. /// /// # Usage /// -/// To use, add the `brotli_compression` feature, the `gzip_compression` -/// feature, or the `compression` feature (to enable both algorithms) to the -/// `rocket_contrib` dependencies section of your `Cargo.toml`: -/// -/// ```toml,ignore -/// [dependencies.rocket_contrib] -/// version = "*" -/// default-features = false -/// features = ["compression"] -/// ``` -/// -/// Then, ensure that the compression [fairing](/rocket/fairing/) is attached to -/// your Rocket application: +/// Attach the compression [fairing](/rocket/fairing/) to your Rocket +/// application: /// /// ```rust /// extern crate rocket; @@ -117,7 +84,7 @@ impl Compression { /// } /// ``` pub fn fairing() -> Compression { - Compression { 0: () } + Compression(()) } } @@ -130,47 +97,31 @@ impl Fairing for Compression { } fn on_attach(&self, rocket: Rocket) -> Result { - let mut ctxt = Context::new(); + let mut ctxt = Context::default(); + match rocket.config().get_table("compress").and_then(|t| { - t.get("exclude") - .ok_or(ConfigError::Missing(String::from("exclude"))) + t.get("exclude").ok_or_else(|| ConfigError::Missing(String::from("exclude"))) }) { Ok(excls) => match excls.as_array() { Some(excls) => { - let mut error = false; - let mut exclusions_vec = Vec::with_capacity(excls.len()); - for e in excls { - match e { - Value::String(s) => match MediaType::parse_flexible(s) { - Some(media_type) => exclusions_vec.push(media_type), - None => { - error = true; - warn_!( - "Exclusions must be valid content types, using default compression exclusions '{:?}'", - ctxt.exclusions - ); - break; - } - }, - _ => { - error = true; - warn_!( - "Exclusions must be strings, using default compression exclusions '{:?}'", - ctxt.exclusions - ); - break; + ctxt.exclusions = excls.iter().flat_map(|ex| { + if let Value::String(s) = ex { + let mt = MediaType::parse_flexible(s); + if mt.is_none() { + warn_!("Ignoring invalid media type '{:?}'", s); } + mt + } else { + warn_!("Ignoring non-string media type '{:?}'", ex); + None } - } - if !error { - ctxt = Context::with_exclusions(exclusions_vec); - } + }).collect(); } None => { warn_!( - "Exclusions must be an array of strings, using default compression exclusions '{:?}'", - ctxt.exclusions - ); + "Exclusions is not an array; using default compression exclusions '{:?}'", + ctxt.exclusions + ); } }, Err(ConfigError::Missing(_)) => { /* ignore missing */ } @@ -187,6 +138,10 @@ impl Fairing for Compression { } fn on_response(&self, request: &Request, response: &mut Response) { - CompressionUtils::compress_response(request, response, true); + let context = request + .guard::<::rocket::State>() + .expect("Compression Context registered in on_attach"); + + super::CompressionUtils::compress_response(request, response, &context.exclusions); } } diff --git a/contrib/lib/src/compression/mod.rs b/contrib/lib/src/compression/mod.rs index 22d52f5e..8e6a7e69 100644 --- a/contrib/lib/src/compression/mod.rs +++ b/contrib/lib/src/compression/mod.rs @@ -1,30 +1,49 @@ -//! `Compression` fairing and `Compressed` responder to automatically and -//! on demand respectively compressing responses. +//! Gzip and Brotli response compression. +//! +//! See the [`Compression`](compression::Compression) and +//! [`Compress`](compression::Compress) types for further details. +//! +//! # Enabling +//! +//! This module is only available when one of the `brotli_compression`, +//! `gzip_compression`, or `compression` features is enabled. Enable +//! one of these in `Cargo.toml` as follows: +//! +//! ```toml +//! [dependencies.rocket_contrib] +//! version = "0.4.0" +//! default-features = false +//! features = ["compression"] +//! ``` +#[cfg(feature="brotli_compression")] extern crate brotli; +#[cfg(feature="gzip_compression")] extern crate flate2; + mod fairing; mod responder; pub use self::fairing::Compression; -pub use self::responder::Compressed; +pub use self::responder::Compress; -crate use self::fairing::Context; -use rocket::http::hyper::header::{ContentEncoding, Encoding}; -use rocket::{Request, Response}; use std::io::Read; +use rocket::http::MediaType; +use rocket::http::hyper::header::{ContentEncoding, Encoding}; +use rocket::{Request, Response}; + #[cfg(feature = "brotli_compression")] -use brotli::enc::backward_references::BrotliEncoderMode; +use self::brotli::enc::backward_references::BrotliEncoderMode; #[cfg(feature = "gzip_compression")] -use flate2::read::GzEncoder; +use self::flate2::read::GzEncoder; -crate struct CompressionUtils; +struct CompressionUtils; impl CompressionUtils { fn accepts_encoding(request: &Request, encoding: &str) -> bool { request .headers() .get("Accept-Encoding") - .flat_map(|accept| accept.split(",")) + .flat_map(|accept| accept.split(',')) .map(|accept| accept.trim()) .any(|accept| accept == encoding) } @@ -44,10 +63,10 @@ impl CompressionUtils { fn skip_encoding( content_type: &Option, - context: &rocket::State, + exclusions: &[MediaType], ) -> bool { match content_type { - Some(content_type) => context.exclusions.iter().any(|exc_media_type| { + Some(content_type) => exclusions.iter().any(|exc_media_type| { if exc_media_type.sub() == "*" { *exc_media_type.top() == *content_type.top() } else { @@ -58,53 +77,53 @@ impl CompressionUtils { } } - fn compress_response(request: &Request, response: &mut Response, respect_excludes: bool) { + fn compress_response(request: &Request, response: &mut Response, exclusions: &[MediaType]) { if CompressionUtils::already_encoded(response) { return; } let content_type = response.content_type(); - if respect_excludes { - let context = request - .guard::<::rocket::State>() - .expect("Compression Context registered in on_attach"); - - if CompressionUtils::skip_encoding(&content_type, &context) { - return; - } + if CompressionUtils::skip_encoding(&content_type, exclusions) { + return; } // Compression is done when the request accepts brotli or gzip encoding // and the corresponding feature is enabled if cfg!(feature = "brotli_compression") && CompressionUtils::accepts_encoding(request, "br") { - if let Some(plain) = response.take_body() { - let content_type_top = content_type.as_ref().map(|ct| ct.top()); - let mut params = brotli::enc::BrotliEncoderInitParams(); - params.quality = 2; - if content_type_top == Some("text".into()) { - params.mode = BrotliEncoderMode::BROTLI_MODE_TEXT; - } else if content_type_top == Some("font".into()) { - params.mode = BrotliEncoderMode::BROTLI_MODE_FONT; + #[cfg(feature = "brotli_compression")] + { + if let Some(plain) = response.take_body() { + let content_type_top = content_type.as_ref().map(|ct| ct.top()); + let mut params = brotli::enc::BrotliEncoderInitParams(); + params.quality = 2; + if content_type_top == Some("text".into()) { + params.mode = BrotliEncoderMode::BROTLI_MODE_TEXT; + } else if content_type_top == Some("font".into()) { + params.mode = BrotliEncoderMode::BROTLI_MODE_FONT; + } + + let compressor = + brotli::CompressorReader::with_params(plain.into_inner(), 4096, ¶ms); + + CompressionUtils::set_body_and_encoding( + response, + compressor, + Encoding::EncodingExt("br".into()), + ); } - - let compressor = - brotli::CompressorReader::with_params(plain.into_inner(), 4096, ¶ms); - - CompressionUtils::set_body_and_encoding( - response, - compressor, - Encoding::EncodingExt("br".into()), - ); } } else if cfg!(feature = "gzip_compression") && CompressionUtils::accepts_encoding(request, "gzip") { - if let Some(plain) = response.take_body() { - let compressor = GzEncoder::new(plain.into_inner(), flate2::Compression::default()); + #[cfg(feature = "gzip_compression")] + { + if let Some(plain) = response.take_body() { + let compressor = GzEncoder::new(plain.into_inner(), flate2::Compression::default()); - CompressionUtils::set_body_and_encoding(response, compressor, Encoding::Gzip); + CompressionUtils::set_body_and_encoding(response, compressor, Encoding::Gzip); + } } } } diff --git a/contrib/lib/src/compression/responder.rs b/contrib/lib/src/compression/responder.rs index 3d8295a8..8130b594 100644 --- a/contrib/lib/src/compression/responder.rs +++ b/contrib/lib/src/compression/responder.rs @@ -1,56 +1,47 @@ -//! Response on demand compression. -//! -//! See the [`Compression`](compression::responder::Compressed) type for -//! further details. - use rocket::response::{self, Responder, Response}; use rocket::Request; -crate use super::CompressionUtils; +use super::CompressionUtils; -/// Compress a `Responder` response ignoring the compression exclusions. +/// Compresses responses with Brotli or Gzip compression. /// -/// Delegates the remainder of the response to the wrapped `Responder`. +/// The `Compress` type implements brotli and gzip compression for responses in +/// accordance with the `Accept-Encoding` header. If accepted, brotli +/// compression is preferred over gzip. +/// +/// In the brotli compression mode (using the +/// [rust-brotli](https://github.com/dropbox/rust-brotli) crate), quality is set +/// to 2 in order to achieve fast compression with a compression ratio similar +/// to gzip. When appropriate, brotli's text and font compression modes are +/// used. +/// +/// In the gzip compression mode (using the +/// [flate2](https://github.com/alexcrichton/flate2-rs) crate), quality is set +/// to the default (9) in order to have good compression ratio. +/// +/// Responses that already have a `Content-Encoding` header are not compressed. /// /// # Usage /// -/// To use, add the `brotli_compression` feature, the `gzip_compression` -/// feature, or the `compression` feature (to enable both algorithms) to the -/// `rocket_contrib` dependencies section of your `Cargo.toml`: -/// -/// ```toml,ignore -/// [dependencies.rocket_contrib] -/// version = "*" -/// default-features = false -/// features = ["compression"] -/// ``` -/// -/// Then, compress the desired response wrapping a `Responder` inside -/// `Compressed`: +/// Compress responses by wrapping a `Responder` inside `Compress`: /// /// ```rust -/// use rocket_contrib::compression::Compressed; +/// use rocket_contrib::compression::Compress; /// /// # #[allow(unused_variables)] -/// let response = Compressed("Hi."); +/// let response = Compress("Hi."); /// ``` #[derive(Debug)] -pub struct Compressed(pub R); +pub struct Compress(pub R); -impl<'r, R: Responder<'r>> Compressed { - pub fn new(response: R) -> Compressed { - Compressed { 0: response } - } -} - -impl<'r, R: Responder<'r>> Responder<'r> for Compressed { +impl<'r, R: Responder<'r>> Responder<'r> for Compress { #[inline(always)] fn respond_to(self, request: &Request) -> response::Result<'r> { let mut response = Response::build() .merge(self.0.respond_to(request)?) .finalize(); - CompressionUtils::compress_response(request, &mut response, false); + CompressionUtils::compress_response(request, &mut response, &[]); Ok(response) } } diff --git a/contrib/lib/src/lib.rs b/contrib/lib/src/lib.rs index 0e0bb425..94a16754 100644 --- a/contrib/lib/src/lib.rs +++ b/contrib/lib/src/lib.rs @@ -25,6 +25,7 @@ //! * [uuid](uuid) - UUID (de)serialization //! * [${database}_pool](databases) - Database Configuration and Pooling //! * [helmet](helmet) - Fairing for Security and Privacy Headers +//! * [compression](compression) - Response compression //! //! The recommend way to include features from this crate via Cargo in your //! project is by adding a `[dependencies.rocket_contrib]` section to your @@ -51,15 +52,7 @@ #[cfg(feature="uuid")] pub mod uuid; #[cfg(feature="databases")] pub mod databases; #[cfg(feature = "helmet")] pub mod helmet; +#[cfg(any(feature="brotli_compression", feature="gzip_compression"))] pub mod compression; #[cfg(feature="databases")] extern crate rocket_contrib_codegen; #[cfg(feature="databases")] #[doc(hidden)] pub use rocket_contrib_codegen::*; - -#[cfg(any(feature="brotli_compression", feature="gzip_compression"))] -pub mod compression; - -#[cfg(feature="brotli_compression")] -extern crate brotli; - -#[cfg(feature="gzip_compression")] -extern crate flate2; diff --git a/contrib/lib/tests/compressed.rs b/contrib/lib/tests/compress_responder.rs similarity index 92% rename from contrib/lib/tests/compressed.rs rename to contrib/lib/tests/compress_responder.rs index 376c9edd..14d36948 100644 --- a/contrib/lib/tests/compressed.rs +++ b/contrib/lib/tests/compress_responder.rs @@ -5,7 +5,7 @@ extern crate rocket; extern crate rocket_contrib; #[cfg(all(feature = "brotli_compression", feature = "gzip_compression"))] -mod compressed_tests { +mod compress_responder_tests { extern crate brotli; extern crate flate2; @@ -14,8 +14,7 @@ mod compressed_tests { use rocket::http::{ContentType, Header}; use rocket::local::Client; use rocket::response::{Content, Response}; - use rocket::routes; - use rocket_contrib::compression::Compressed; + use rocket_contrib::compression::Compress; use std::io::Cursor; use std::io::Read; @@ -26,29 +25,29 @@ mod compressed_tests { in order to have to read more than one buffer when gzipping. こんにちは!"; #[get("/")] - pub fn index() -> Compressed { - Compressed::new(String::from(HELLO)) + pub fn index() -> Compress { + Compress(String::from(HELLO)) } #[get("/font")] - pub fn font() -> Compressed> { - Compressed::new(Content(ContentType::WOFF, HELLO)) + pub fn font() -> Compress> { + Compress(Content(ContentType::WOFF, HELLO)) } #[get("/image")] - pub fn image() -> Compressed> { - Compressed::new(Content(ContentType::PNG, HELLO)) + pub fn image() -> Compress> { + Compress(Content(ContentType::PNG, HELLO)) } #[get("/already_encoded")] - pub fn already_encoded() -> Compressed> { + pub fn already_encoded() -> Compress> { let mut encoder = GzEncoder::new( Cursor::new(String::from(HELLO)), flate2::Compression::default(), ); let mut encoded = Vec::new(); encoder.read_to_end(&mut encoded).unwrap(); - Compressed::new( + Compress( Response::build() .header(ContentEncoding(vec![Encoding::Gzip])) .sized_body(Cursor::new(encoded)) @@ -57,8 +56,8 @@ mod compressed_tests { } #[get("/identity")] - pub fn identity() -> Compressed> { - Compressed::new( + pub fn identity() -> Compress> { + Compress( Response::build() .header(ContentEncoding(vec![Encoding::Identity])) .sized_body(Cursor::new(String::from(HELLO))) diff --git a/contrib/lib/tests/compression.rs b/contrib/lib/tests/compression_fairing.rs similarity index 92% rename from contrib/lib/tests/compression.rs rename to contrib/lib/tests/compression_fairing.rs index eb5f7caa..7cae7ceb 100644 --- a/contrib/lib/tests/compression.rs +++ b/contrib/lib/tests/compression_fairing.rs @@ -5,7 +5,7 @@ extern crate rocket; extern crate rocket_contrib; #[cfg(all(feature = "brotli_compression", feature = "gzip_compression"))] -mod compression_tests { +mod compression_fairing_tests { extern crate brotli; extern crate flate2; @@ -14,8 +14,8 @@ mod compression_tests { use rocket::http::Status; use rocket::http::{ContentType, Header}; use rocket::local::Client; - use rocket::response::Response; - use rocket::routes; + use rocket::response::{Content, Response}; + use rocket_contrib::compression::Compression; use std::io::Cursor; use std::io::Read; @@ -31,27 +31,18 @@ mod compression_tests { } #[get("/font")] - pub fn font() -> Response<'static> { - Response::build() - .header(ContentType::WOFF) - .sized_body(Cursor::new(String::from(HELLO))) - .finalize() + pub fn font() -> Content<&'static str> { + Content(ContentType::WOFF, HELLO) } #[get("/image")] - pub fn image() -> Response<'static> { - Response::build() - .header(ContentType::PNG) - .sized_body(Cursor::new(String::from(HELLO))) - .finalize() + pub fn image() -> Content<&'static str> { + Content(ContentType::PNG, HELLO) } #[get("/tar")] - pub fn tar() -> Response<'static> { - Response::build() - .header(ContentType::TAR) - .sized_body(Cursor::new(String::from(HELLO))) - .finalize() + pub fn tar() -> Content<&'static str> { + Content(ContentType::TAR, HELLO) } #[get("/already_encoded")] @@ -82,7 +73,7 @@ mod compression_tests { "/", routes![index, font, image, tar, already_encoded, identity], ) - .attach(rocket_contrib::compression::Compression::fairing()) + .attach(Compression::fairing()) } fn rocket_tar_exception() -> rocket::Rocket { @@ -94,7 +85,7 @@ mod compression_tests { rocket::custom(config) .mount("/", routes![image, tar]) - .attach(rocket_contrib::compression::Compression::fairing()) + .attach(Compression::fairing()) } #[test] diff --git a/scripts/test.sh b/scripts/test.sh index 94df21a9..88f16b96 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -79,6 +79,8 @@ if [ "$1" = "--contrib" ]; then redis_pool mongodb_pool memcache_pool + brotli_compression + gzip_compression ) pushd "${CONTRIB_LIB_ROOT}" > /dev/null 2>&1