Rocket

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

commit 0a3960b0317b030cf3d3345f2d1abb83c1aebb91
parent 6a55aa72536b5f46f5a16e7a3487c0ee8b8ddeaa
Author: Jeb Rosen <jeb@jebrosen.com>
Date:   Sat, 27 Apr 2019 08:41:49 -0700

Clean up 'compression' module and documentation.

Diffstat:
Mcontrib/lib/Cargo.toml | 2+-
Mcontrib/lib/src/compression/fairing.rs | 123+++++++++++++++++++++++++------------------------------------------------------
Mcontrib/lib/src/compression/mod.rs | 99+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Mcontrib/lib/src/compression/responder.rs | 53++++++++++++++++++++++-------------------------------
Mcontrib/lib/src/lib.rs | 11++---------
Acontrib/lib/tests/compress_responder.rs | 243+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dcontrib/lib/tests/compressed.rs | 244-------------------------------------------------------------------------------
Dcontrib/lib/tests/compression.rs | 307-------------------------------------------------------------------------------
Acontrib/lib/tests/compression_fairing.rs | 298+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mscripts/test.sh | 2++
10 files changed, 666 insertions(+), 716 deletions(-)

diff --git 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 @@ -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<MediaType>, +struct Context { + exclusions: Vec<MediaType>, } -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<MediaType>) -> 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. -/// -/// 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. +/// Compresses all responses with Brotli or Gzip compression. /// -/// This fairing does not compress responses that already have a -/// `Content-Encoding` header. +/// Compression is done in the same manner as the [`Compress`](super::Compress) +/// responder. /// -/// This fairing ignores the responses with a `Content-Type` matching any of -/// the following default types: +/// By default, the fairing does not compress responses with a `Content-Type` +/// matching any of the following: /// -/// - 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<Rocket, Rocket> { - 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<Context>>() + .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 @@ -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; + +use std::io::Read; -crate use self::fairing::Context; +use rocket::http::MediaType; use rocket::http::hyper::header::{ContentEncoding, Encoding}; use rocket::{Request, Response}; -use std::io::Read; #[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<rocket::http::ContentType>, - context: &rocket::State<Context>, + 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<Context>>() - .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, &params); + + CompressionUtils::set_body_and_encoding( + response, + compressor, + Encoding::EncodingExt("br".into()), + ); } - - let compressor = - brotli::CompressorReader::with_params(plain.into_inner(), 4096, &params); - - 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 @@ -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. /// -/// # Usage +/// 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. /// -/// 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`: +/// 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. /// -/// ```toml,ignore -/// [dependencies.rocket_contrib] -/// version = "*" -/// default-features = false -/// features = ["compression"] -/// ``` +/// Responses that already have a `Content-Encoding` header are not compressed. +/// +/// # Usage /// -/// 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<R>(pub R); - -impl<'r, R: Responder<'r>> Compressed<R> { - pub fn new(response: R) -> Compressed<R> { - Compressed { 0: response } - } -} +pub struct Compress<R>(pub R); -impl<'r, R: Responder<'r>> Responder<'r> for Compressed<R> { +impl<'r, R: Responder<'r>> Responder<'r> for Compress<R> { #[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 @@ -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/compress_responder.rs b/contrib/lib/tests/compress_responder.rs @@ -0,0 +1,243 @@ +#![feature(decl_macro, proc_macro_hygiene)] + +#[macro_use] +extern crate rocket; +extern crate rocket_contrib; + +#[cfg(all(feature = "brotli_compression", feature = "gzip_compression"))] +mod compress_responder_tests { + extern crate brotli; + extern crate flate2; + + use rocket::http::hyper::header::{ContentEncoding, Encoding}; + use rocket::http::Status; + use rocket::http::{ContentType, Header}; + use rocket::local::Client; + use rocket::response::{Content, Response}; + use rocket_contrib::compression::Compress; + + use std::io::Cursor; + use std::io::Read; + + use self::flate2::read::{GzDecoder, GzEncoder}; + + const HELLO: &str = r"This is a message to hello with more than 100 bytes \ + in order to have to read more than one buffer when gzipping. こんにちは!"; + + #[get("/")] + pub fn index() -> Compress<String> { + Compress(String::from(HELLO)) + } + + #[get("/font")] + pub fn font() -> Compress<Content<&'static str>> { + Compress(Content(ContentType::WOFF, HELLO)) + } + + #[get("/image")] + pub fn image() -> Compress<Content<&'static str>> { + Compress(Content(ContentType::PNG, HELLO)) + } + + #[get("/already_encoded")] + pub fn already_encoded() -> Compress<Response<'static>> { + 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(); + Compress( + Response::build() + .header(ContentEncoding(vec![Encoding::Gzip])) + .sized_body(Cursor::new(encoded)) + .finalize(), + ) + } + + #[get("/identity")] + pub fn identity() -> Compress<Response<'static>> { + Compress( + Response::build() + .header(ContentEncoding(vec![Encoding::Identity])) + .sized_body(Cursor::new(String::from(HELLO))) + .finalize(), + ) + } + + fn rocket() -> rocket::Rocket { + rocket::ignite().mount("/", routes![index, font, image, already_encoded, identity]) + } + + #[test] + fn test_prioritizes_brotli() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let mut response = client + .get("/") + .header(Header::new("Accept-Encoding", "deflate, gzip, br")) + .dispatch(); + assert_eq!(response.status(), Status::Ok); + assert!(response + .headers() + .get("Content-Encoding") + .any(|x| x == "br")); + let mut body_plain = Cursor::new(Vec::<u8>::new()); + brotli::BrotliDecompress( + &mut Cursor::new(response.body_bytes().unwrap()), + &mut body_plain, + ) + .expect("decompress response"); + assert_eq!( + String::from_utf8(body_plain.get_mut().to_vec()).unwrap(), + String::from(HELLO) + ); + } + + #[test] + fn test_br_font() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let mut response = client + .get("/font") + .header(Header::new("Accept-Encoding", "deflate, gzip, br")) + .dispatch(); + assert_eq!(response.status(), Status::Ok); + assert!(response + .headers() + .get("Content-Encoding") + .any(|x| x == "br")); + let mut body_plain = Cursor::new(Vec::<u8>::new()); + brotli::BrotliDecompress( + &mut Cursor::new(response.body_bytes().unwrap()), + &mut body_plain, + ) + .expect("decompress response"); + assert_eq!( + String::from_utf8(body_plain.get_mut().to_vec()).unwrap(), + String::from(HELLO) + ); + } + + #[test] + fn test_fallback_gzip() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let mut response = client + .get("/") + .header(Header::new("Accept-Encoding", "deflate, gzip")) + .dispatch(); + assert_eq!(response.status(), Status::Ok); + assert!(!response + .headers() + .get("Content-Encoding") + .any(|x| x == "br")); + assert!(response + .headers() + .get("Content-Encoding") + .any(|x| x == "gzip")); + let mut s = String::new(); + GzDecoder::new(&response.body_bytes().unwrap()[..]) + .read_to_string(&mut s) + .expect("decompress response"); + assert_eq!(s, String::from(HELLO)); + } + + #[test] + fn test_does_not_recompress() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let mut response = client + .get("/already_encoded") + .header(Header::new("Accept-Encoding", "deflate, gzip, br")) + .dispatch(); + assert_eq!(response.status(), Status::Ok); + assert!(!response + .headers() + .get("Content-Encoding") + .any(|x| x == "br")); + assert!(response + .headers() + .get("Content-Encoding") + .any(|x| x == "gzip")); + let mut s = String::new(); + GzDecoder::new(&response.body_bytes().unwrap()[..]) + .read_to_string(&mut s) + .expect("decompress response"); + assert_eq!(s, String::from(HELLO)); + } + + #[test] + fn test_does_not_compress_explicit_identity() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let mut response = client + .get("/identity") + .header(Header::new("Accept-Encoding", "deflate, gzip, br")) + .dispatch(); + assert_eq!(response.status(), Status::Ok); + assert!(!response + .headers() + .get("Content-Encoding") + .any(|x| x != "identity")); + assert_eq!( + String::from_utf8(response.body_bytes().unwrap()).unwrap(), + String::from(HELLO) + ); + } + + #[test] + fn test_ignore_exceptions() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let mut response = client + .get("/image") + .header(Header::new("Accept-Encoding", "deflate, gzip, br")) + .dispatch(); + assert_eq!(response.status(), Status::Ok); + assert!(response + .headers() + .get("Content-Encoding") + .any(|x| x == "br")); + let mut body_plain = Cursor::new(Vec::<u8>::new()); + brotli::BrotliDecompress( + &mut Cursor::new(response.body_bytes().unwrap()), + &mut body_plain, + ) + .expect("decompress response"); + assert_eq!( + String::from_utf8(body_plain.get_mut().to_vec()).unwrap(), + String::from(HELLO) + ); + } + + #[test] + fn test_ignores_unimplemented_encodings() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let mut response = client + .get("/") + .header(Header::new("Accept-Encoding", "deflate")) + .dispatch(); + assert_eq!(response.status(), Status::Ok); + assert!(!response + .headers() + .get("Content-Encoding") + .any(|x| x != "identity")); + assert_eq!( + String::from_utf8(response.body_bytes().unwrap()).unwrap(), + String::from(HELLO) + ); + } + + #[test] + fn test_respects_identity_only() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let mut response = client + .get("/") + .header(Header::new("Accept-Encoding", "identity")) + .dispatch(); + assert_eq!(response.status(), Status::Ok); + assert!(!response + .headers() + .get("Content-Encoding") + .any(|x| x != "identity")); + assert_eq!( + String::from_utf8(response.body_bytes().unwrap()).unwrap(), + String::from(HELLO) + ); + } +} diff --git a/contrib/lib/tests/compressed.rs b/contrib/lib/tests/compressed.rs @@ -1,244 +0,0 @@ -#![feature(decl_macro, proc_macro_hygiene)] - -#[macro_use] -extern crate rocket; -extern crate rocket_contrib; - -#[cfg(all(feature = "brotli_compression", feature = "gzip_compression"))] -mod compressed_tests { - extern crate brotli; - extern crate flate2; - - use rocket::http::hyper::header::{ContentEncoding, Encoding}; - use rocket::http::Status; - use rocket::http::{ContentType, Header}; - use rocket::local::Client; - use rocket::response::{Content, Response}; - use rocket::routes; - use rocket_contrib::compression::Compressed; - - use std::io::Cursor; - use std::io::Read; - - use self::flate2::read::{GzDecoder, GzEncoder}; - - const HELLO: &str = r"This is a message to hello with more than 100 bytes \ - in order to have to read more than one buffer when gzipping. こんにちは!"; - - #[get("/")] - pub fn index() -> Compressed<String> { - Compressed::new(String::from(HELLO)) - } - - #[get("/font")] - pub fn font() -> Compressed<Content<&'static str>> { - Compressed::new(Content(ContentType::WOFF, HELLO)) - } - - #[get("/image")] - pub fn image() -> Compressed<Content<&'static str>> { - Compressed::new(Content(ContentType::PNG, HELLO)) - } - - #[get("/already_encoded")] - pub fn already_encoded() -> Compressed<Response<'static>> { - 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( - Response::build() - .header(ContentEncoding(vec![Encoding::Gzip])) - .sized_body(Cursor::new(encoded)) - .finalize(), - ) - } - - #[get("/identity")] - pub fn identity() -> Compressed<Response<'static>> { - Compressed::new( - Response::build() - .header(ContentEncoding(vec![Encoding::Identity])) - .sized_body(Cursor::new(String::from(HELLO))) - .finalize(), - ) - } - - fn rocket() -> rocket::Rocket { - rocket::ignite().mount("/", routes![index, font, image, already_encoded, identity]) - } - - #[test] - fn test_prioritizes_brotli() { - let client = Client::new(rocket()).expect("valid rocket instance"); - let mut response = client - .get("/") - .header(Header::new("Accept-Encoding", "deflate, gzip, br")) - .dispatch(); - assert_eq!(response.status(), Status::Ok); - assert!(response - .headers() - .get("Content-Encoding") - .any(|x| x == "br")); - let mut body_plain = Cursor::new(Vec::<u8>::new()); - brotli::BrotliDecompress( - &mut Cursor::new(response.body_bytes().unwrap()), - &mut body_plain, - ) - .expect("decompress response"); - assert_eq!( - String::from_utf8(body_plain.get_mut().to_vec()).unwrap(), - String::from(HELLO) - ); - } - - #[test] - fn test_br_font() { - let client = Client::new(rocket()).expect("valid rocket instance"); - let mut response = client - .get("/font") - .header(Header::new("Accept-Encoding", "deflate, gzip, br")) - .dispatch(); - assert_eq!(response.status(), Status::Ok); - assert!(response - .headers() - .get("Content-Encoding") - .any(|x| x == "br")); - let mut body_plain = Cursor::new(Vec::<u8>::new()); - brotli::BrotliDecompress( - &mut Cursor::new(response.body_bytes().unwrap()), - &mut body_plain, - ) - .expect("decompress response"); - assert_eq!( - String::from_utf8(body_plain.get_mut().to_vec()).unwrap(), - String::from(HELLO) - ); - } - - #[test] - fn test_fallback_gzip() { - let client = Client::new(rocket()).expect("valid rocket instance"); - let mut response = client - .get("/") - .header(Header::new("Accept-Encoding", "deflate, gzip")) - .dispatch(); - assert_eq!(response.status(), Status::Ok); - assert!(!response - .headers() - .get("Content-Encoding") - .any(|x| x == "br")); - assert!(response - .headers() - .get("Content-Encoding") - .any(|x| x == "gzip")); - let mut s = String::new(); - GzDecoder::new(&response.body_bytes().unwrap()[..]) - .read_to_string(&mut s) - .expect("decompress response"); - assert_eq!(s, String::from(HELLO)); - } - - #[test] - fn test_does_not_recompress() { - let client = Client::new(rocket()).expect("valid rocket instance"); - let mut response = client - .get("/already_encoded") - .header(Header::new("Accept-Encoding", "deflate, gzip, br")) - .dispatch(); - assert_eq!(response.status(), Status::Ok); - assert!(!response - .headers() - .get("Content-Encoding") - .any(|x| x == "br")); - assert!(response - .headers() - .get("Content-Encoding") - .any(|x| x == "gzip")); - let mut s = String::new(); - GzDecoder::new(&response.body_bytes().unwrap()[..]) - .read_to_string(&mut s) - .expect("decompress response"); - assert_eq!(s, String::from(HELLO)); - } - - #[test] - fn test_does_not_compress_explicit_identity() { - let client = Client::new(rocket()).expect("valid rocket instance"); - let mut response = client - .get("/identity") - .header(Header::new("Accept-Encoding", "deflate, gzip, br")) - .dispatch(); - assert_eq!(response.status(), Status::Ok); - assert!(!response - .headers() - .get("Content-Encoding") - .any(|x| x != "identity")); - assert_eq!( - String::from_utf8(response.body_bytes().unwrap()).unwrap(), - String::from(HELLO) - ); - } - - #[test] - fn test_ignore_exceptions() { - let client = Client::new(rocket()).expect("valid rocket instance"); - let mut response = client - .get("/image") - .header(Header::new("Accept-Encoding", "deflate, gzip, br")) - .dispatch(); - assert_eq!(response.status(), Status::Ok); - assert!(response - .headers() - .get("Content-Encoding") - .any(|x| x == "br")); - let mut body_plain = Cursor::new(Vec::<u8>::new()); - brotli::BrotliDecompress( - &mut Cursor::new(response.body_bytes().unwrap()), - &mut body_plain, - ) - .expect("decompress response"); - assert_eq!( - String::from_utf8(body_plain.get_mut().to_vec()).unwrap(), - String::from(HELLO) - ); - } - - #[test] - fn test_ignores_unimplemented_encodings() { - let client = Client::new(rocket()).expect("valid rocket instance"); - let mut response = client - .get("/") - .header(Header::new("Accept-Encoding", "deflate")) - .dispatch(); - assert_eq!(response.status(), Status::Ok); - assert!(!response - .headers() - .get("Content-Encoding") - .any(|x| x != "identity")); - assert_eq!( - String::from_utf8(response.body_bytes().unwrap()).unwrap(), - String::from(HELLO) - ); - } - - #[test] - fn test_respects_identity_only() { - let client = Client::new(rocket()).expect("valid rocket instance"); - let mut response = client - .get("/") - .header(Header::new("Accept-Encoding", "identity")) - .dispatch(); - assert_eq!(response.status(), Status::Ok); - assert!(!response - .headers() - .get("Content-Encoding") - .any(|x| x != "identity")); - assert_eq!( - String::from_utf8(response.body_bytes().unwrap()).unwrap(), - String::from(HELLO) - ); - } -} diff --git a/contrib/lib/tests/compression.rs b/contrib/lib/tests/compression.rs @@ -1,307 +0,0 @@ -#![feature(decl_macro, proc_macro_hygiene)] - -#[macro_use] -extern crate rocket; -extern crate rocket_contrib; - -#[cfg(all(feature = "brotli_compression", feature = "gzip_compression"))] -mod compression_tests { - extern crate brotli; - extern crate flate2; - - use rocket::config::{Config, Environment}; - use rocket::http::hyper::header::{ContentEncoding, Encoding}; - use rocket::http::Status; - use rocket::http::{ContentType, Header}; - use rocket::local::Client; - use rocket::response::Response; - use rocket::routes; - - use std::io::Cursor; - use std::io::Read; - - use self::flate2::read::{GzDecoder, GzEncoder}; - - const HELLO: &str = r"This is a message to hello with more than 100 bytes \ - in order to have to read more than one buffer when gzipping. こんにちは!"; - - #[get("/")] - pub fn index() -> String { - String::from(HELLO) - } - - #[get("/font")] - pub fn font() -> Response<'static> { - Response::build() - .header(ContentType::WOFF) - .sized_body(Cursor::new(String::from(HELLO))) - .finalize() - } - - #[get("/image")] - pub fn image() -> Response<'static> { - Response::build() - .header(ContentType::PNG) - .sized_body(Cursor::new(String::from(HELLO))) - .finalize() - } - - #[get("/tar")] - pub fn tar() -> Response<'static> { - Response::build() - .header(ContentType::TAR) - .sized_body(Cursor::new(String::from(HELLO))) - .finalize() - } - - #[get("/already_encoded")] - pub fn already_encoded() -> Response<'static> { - 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(); - Response::build() - .header(ContentEncoding(vec![Encoding::Gzip])) - .sized_body(Cursor::new(encoded)) - .finalize() - } - - #[get("/identity")] - pub fn identity() -> Response<'static> { - Response::build() - .header(ContentEncoding(vec![Encoding::Identity])) - .sized_body(Cursor::new(String::from(HELLO))) - .finalize() - } - - fn rocket() -> rocket::Rocket { - rocket::ignite() - .mount( - "/", - routes![index, font, image, tar, already_encoded, identity], - ) - .attach(rocket_contrib::compression::Compression::fairing()) - } - - fn rocket_tar_exception() -> rocket::Rocket { - let mut table = std::collections::BTreeMap::new(); - table.insert("exclude".to_string(), vec!["application/x-tar"]); - let config = Config::build(Environment::Development) - .extra("compress", table) - .expect("valid configuration"); - - rocket::custom(config) - .mount("/", routes![image, tar]) - .attach(rocket_contrib::compression::Compression::fairing()) - } - - #[test] - fn test_prioritizes_brotli() { - let client = Client::new(rocket()).expect("valid rocket instance"); - let mut response = client - .get("/") - .header(Header::new("Accept-Encoding", "deflate, gzip, br")) - .dispatch(); - assert_eq!(response.status(), Status::Ok); - assert!(response - .headers() - .get("Content-Encoding") - .any(|x| x == "br")); - let mut body_plain = Cursor::new(Vec::<u8>::new()); - brotli::BrotliDecompress( - &mut Cursor::new(response.body_bytes().unwrap()), - &mut body_plain, - ) - .expect("decompress response"); - assert_eq!( - String::from_utf8(body_plain.get_mut().to_vec()).unwrap(), - String::from(HELLO) - ); - } - - #[test] - fn test_br_font() { - let client = Client::new(rocket()).expect("valid rocket instance"); - let mut response = client - .get("/font") - .header(Header::new("Accept-Encoding", "deflate, gzip, br")) - .dispatch(); - assert_eq!(response.status(), Status::Ok); - assert!(response - .headers() - .get("Content-Encoding") - .any(|x| x == "br")); - let mut body_plain = Cursor::new(Vec::<u8>::new()); - brotli::BrotliDecompress( - &mut Cursor::new(response.body_bytes().unwrap()), - &mut body_plain, - ) - .expect("decompress response"); - assert_eq!( - String::from_utf8(body_plain.get_mut().to_vec()).unwrap(), - String::from(HELLO) - ); - } - - #[test] - fn test_fallback_gzip() { - let client = Client::new(rocket()).expect("valid rocket instance"); - let mut response = client - .get("/") - .header(Header::new("Accept-Encoding", "deflate, gzip")) - .dispatch(); - assert_eq!(response.status(), Status::Ok); - assert!(!response - .headers() - .get("Content-Encoding") - .any(|x| x == "br")); - assert!(response - .headers() - .get("Content-Encoding") - .any(|x| x == "gzip")); - let mut s = String::new(); - GzDecoder::new(&response.body_bytes().unwrap()[..]) - .read_to_string(&mut s) - .expect("decompress response"); - assert_eq!(s, String::from(HELLO)); - } - - #[test] - fn test_does_not_recompress() { - let client = Client::new(rocket()).expect("valid rocket instance"); - let mut response = client - .get("/already_encoded") - .header(Header::new("Accept-Encoding", "deflate, gzip, br")) - .dispatch(); - assert_eq!(response.status(), Status::Ok); - assert!(!response - .headers() - .get("Content-Encoding") - .any(|x| x == "br")); - assert!(response - .headers() - .get("Content-Encoding") - .any(|x| x == "gzip")); - let mut s = String::new(); - GzDecoder::new(&response.body_bytes().unwrap()[..]) - .read_to_string(&mut s) - .expect("decompress response"); - assert_eq!(s, String::from(HELLO)); - } - - #[test] - fn test_does_not_compress_explicit_identity() { - let client = Client::new(rocket()).expect("valid rocket instance"); - let mut response = client - .get("/identity") - .header(Header::new("Accept-Encoding", "deflate, gzip, br")) - .dispatch(); - assert_eq!(response.status(), Status::Ok); - assert!(!response - .headers() - .get("Content-Encoding") - .any(|x| x != "identity")); - assert_eq!( - String::from_utf8(response.body_bytes().unwrap()).unwrap(), - String::from(HELLO) - ); - } - - #[test] - fn test_does_not_compress_image() { - let client = Client::new(rocket()).expect("valid rocket instance"); - let mut response = client - .get("/image") - .header(Header::new("Accept-Encoding", "deflate, gzip, br")) - .dispatch(); - assert_eq!(response.status(), Status::Ok); - assert!(!response - .headers() - .get("Content-Encoding") - .any(|x| x != "identity")); - assert_eq!( - String::from_utf8(response.body_bytes().unwrap()).unwrap(), - String::from(HELLO) - ); - } - - #[test] - fn test_ignores_unimplemented_encodings() { - let client = Client::new(rocket()).expect("valid rocket instance"); - let mut response = client - .get("/") - .header(Header::new("Accept-Encoding", "deflate")) - .dispatch(); - assert_eq!(response.status(), Status::Ok); - assert!(!response - .headers() - .get("Content-Encoding") - .any(|x| x != "identity")); - assert_eq!( - String::from_utf8(response.body_bytes().unwrap()).unwrap(), - String::from(HELLO) - ); - } - - #[test] - fn test_respects_identity_only() { - let client = Client::new(rocket()).expect("valid rocket instance"); - let mut response = client - .get("/") - .header(Header::new("Accept-Encoding", "identity")) - .dispatch(); - assert_eq!(response.status(), Status::Ok); - assert!(!response - .headers() - .get("Content-Encoding") - .any(|x| x != "identity")); - assert_eq!( - String::from_utf8(response.body_bytes().unwrap()).unwrap(), - String::from(HELLO) - ); - } - - #[test] - fn test_does_not_compress_custom_exception() { - let client = Client::new(rocket_tar_exception()).expect("valid rocket instance"); - let mut response = client - .get("/tar") - .header(Header::new("Accept-Encoding", "deflate, gzip, br")) - .dispatch(); - assert_eq!(response.status(), Status::Ok); - assert!(!response - .headers() - .get("Content-Encoding") - .any(|x| x != "identity")); - assert_eq!( - String::from_utf8(response.body_bytes().unwrap()).unwrap(), - String::from(HELLO) - ); - } - - #[test] - fn test_compress_custom_removed_exception() { - let client = Client::new(rocket_tar_exception()).expect("valid rocket instance"); - let mut response = client - .get("/image") - .header(Header::new("Accept-Encoding", "deflate, gzip, br")) - .dispatch(); - assert_eq!(response.status(), Status::Ok); - assert!(response - .headers() - .get("Content-Encoding") - .any(|x| x == "br")); - let mut body_plain = Cursor::new(Vec::<u8>::new()); - brotli::BrotliDecompress( - &mut Cursor::new(response.body_bytes().unwrap()), - &mut body_plain, - ) - .expect("decompress response"); - assert_eq!( - String::from_utf8(body_plain.get_mut().to_vec()).unwrap(), - String::from(HELLO) - ); - } -} diff --git a/contrib/lib/tests/compression_fairing.rs b/contrib/lib/tests/compression_fairing.rs @@ -0,0 +1,298 @@ +#![feature(decl_macro, proc_macro_hygiene)] + +#[macro_use] +extern crate rocket; +extern crate rocket_contrib; + +#[cfg(all(feature = "brotli_compression", feature = "gzip_compression"))] +mod compression_fairing_tests { + extern crate brotli; + extern crate flate2; + + use rocket::config::{Config, Environment}; + use rocket::http::hyper::header::{ContentEncoding, Encoding}; + use rocket::http::Status; + use rocket::http::{ContentType, Header}; + use rocket::local::Client; + use rocket::response::{Content, Response}; + use rocket_contrib::compression::Compression; + + use std::io::Cursor; + use std::io::Read; + + use self::flate2::read::{GzDecoder, GzEncoder}; + + const HELLO: &str = r"This is a message to hello with more than 100 bytes \ + in order to have to read more than one buffer when gzipping. こんにちは!"; + + #[get("/")] + pub fn index() -> String { + String::from(HELLO) + } + + #[get("/font")] + pub fn font() -> Content<&'static str> { + Content(ContentType::WOFF, HELLO) + } + + #[get("/image")] + pub fn image() -> Content<&'static str> { + Content(ContentType::PNG, HELLO) + } + + #[get("/tar")] + pub fn tar() -> Content<&'static str> { + Content(ContentType::TAR, HELLO) + } + + #[get("/already_encoded")] + pub fn already_encoded() -> Response<'static> { + 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(); + Response::build() + .header(ContentEncoding(vec![Encoding::Gzip])) + .sized_body(Cursor::new(encoded)) + .finalize() + } + + #[get("/identity")] + pub fn identity() -> Response<'static> { + Response::build() + .header(ContentEncoding(vec![Encoding::Identity])) + .sized_body(Cursor::new(String::from(HELLO))) + .finalize() + } + + fn rocket() -> rocket::Rocket { + rocket::ignite() + .mount( + "/", + routes![index, font, image, tar, already_encoded, identity], + ) + .attach(Compression::fairing()) + } + + fn rocket_tar_exception() -> rocket::Rocket { + let mut table = std::collections::BTreeMap::new(); + table.insert("exclude".to_string(), vec!["application/x-tar"]); + let config = Config::build(Environment::Development) + .extra("compress", table) + .expect("valid configuration"); + + rocket::custom(config) + .mount("/", routes![image, tar]) + .attach(Compression::fairing()) + } + + #[test] + fn test_prioritizes_brotli() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let mut response = client + .get("/") + .header(Header::new("Accept-Encoding", "deflate, gzip, br")) + .dispatch(); + assert_eq!(response.status(), Status::Ok); + assert!(response + .headers() + .get("Content-Encoding") + .any(|x| x == "br")); + let mut body_plain = Cursor::new(Vec::<u8>::new()); + brotli::BrotliDecompress( + &mut Cursor::new(response.body_bytes().unwrap()), + &mut body_plain, + ) + .expect("decompress response"); + assert_eq!( + String::from_utf8(body_plain.get_mut().to_vec()).unwrap(), + String::from(HELLO) + ); + } + + #[test] + fn test_br_font() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let mut response = client + .get("/font") + .header(Header::new("Accept-Encoding", "deflate, gzip, br")) + .dispatch(); + assert_eq!(response.status(), Status::Ok); + assert!(response + .headers() + .get("Content-Encoding") + .any(|x| x == "br")); + let mut body_plain = Cursor::new(Vec::<u8>::new()); + brotli::BrotliDecompress( + &mut Cursor::new(response.body_bytes().unwrap()), + &mut body_plain, + ) + .expect("decompress response"); + assert_eq!( + String::from_utf8(body_plain.get_mut().to_vec()).unwrap(), + String::from(HELLO) + ); + } + + #[test] + fn test_fallback_gzip() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let mut response = client + .get("/") + .header(Header::new("Accept-Encoding", "deflate, gzip")) + .dispatch(); + assert_eq!(response.status(), Status::Ok); + assert!(!response + .headers() + .get("Content-Encoding") + .any(|x| x == "br")); + assert!(response + .headers() + .get("Content-Encoding") + .any(|x| x == "gzip")); + let mut s = String::new(); + GzDecoder::new(&response.body_bytes().unwrap()[..]) + .read_to_string(&mut s) + .expect("decompress response"); + assert_eq!(s, String::from(HELLO)); + } + + #[test] + fn test_does_not_recompress() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let mut response = client + .get("/already_encoded") + .header(Header::new("Accept-Encoding", "deflate, gzip, br")) + .dispatch(); + assert_eq!(response.status(), Status::Ok); + assert!(!response + .headers() + .get("Content-Encoding") + .any(|x| x == "br")); + assert!(response + .headers() + .get("Content-Encoding") + .any(|x| x == "gzip")); + let mut s = String::new(); + GzDecoder::new(&response.body_bytes().unwrap()[..]) + .read_to_string(&mut s) + .expect("decompress response"); + assert_eq!(s, String::from(HELLO)); + } + + #[test] + fn test_does_not_compress_explicit_identity() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let mut response = client + .get("/identity") + .header(Header::new("Accept-Encoding", "deflate, gzip, br")) + .dispatch(); + assert_eq!(response.status(), Status::Ok); + assert!(!response + .headers() + .get("Content-Encoding") + .any(|x| x != "identity")); + assert_eq!( + String::from_utf8(response.body_bytes().unwrap()).unwrap(), + String::from(HELLO) + ); + } + + #[test] + fn test_does_not_compress_image() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let mut response = client + .get("/image") + .header(Header::new("Accept-Encoding", "deflate, gzip, br")) + .dispatch(); + assert_eq!(response.status(), Status::Ok); + assert!(!response + .headers() + .get("Content-Encoding") + .any(|x| x != "identity")); + assert_eq!( + String::from_utf8(response.body_bytes().unwrap()).unwrap(), + String::from(HELLO) + ); + } + + #[test] + fn test_ignores_unimplemented_encodings() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let mut response = client + .get("/") + .header(Header::new("Accept-Encoding", "deflate")) + .dispatch(); + assert_eq!(response.status(), Status::Ok); + assert!(!response + .headers() + .get("Content-Encoding") + .any(|x| x != "identity")); + assert_eq!( + String::from_utf8(response.body_bytes().unwrap()).unwrap(), + String::from(HELLO) + ); + } + + #[test] + fn test_respects_identity_only() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let mut response = client + .get("/") + .header(Header::new("Accept-Encoding", "identity")) + .dispatch(); + assert_eq!(response.status(), Status::Ok); + assert!(!response + .headers() + .get("Content-Encoding") + .any(|x| x != "identity")); + assert_eq!( + String::from_utf8(response.body_bytes().unwrap()).unwrap(), + String::from(HELLO) + ); + } + + #[test] + fn test_does_not_compress_custom_exception() { + let client = Client::new(rocket_tar_exception()).expect("valid rocket instance"); + let mut response = client + .get("/tar") + .header(Header::new("Accept-Encoding", "deflate, gzip, br")) + .dispatch(); + assert_eq!(response.status(), Status::Ok); + assert!(!response + .headers() + .get("Content-Encoding") + .any(|x| x != "identity")); + assert_eq!( + String::from_utf8(response.body_bytes().unwrap()).unwrap(), + String::from(HELLO) + ); + } + + #[test] + fn test_compress_custom_removed_exception() { + let client = Client::new(rocket_tar_exception()).expect("valid rocket instance"); + let mut response = client + .get("/image") + .header(Header::new("Accept-Encoding", "deflate, gzip, br")) + .dispatch(); + assert_eq!(response.status(), Status::Ok); + assert!(response + .headers() + .get("Content-Encoding") + .any(|x| x == "br")); + let mut body_plain = Cursor::new(Vec::<u8>::new()); + brotli::BrotliDecompress( + &mut Cursor::new(response.body_bytes().unwrap()), + &mut body_plain, + ) + .expect("decompress response"); + assert_eq!( + String::from_utf8(body_plain.get_mut().to_vec()).unwrap(), + String::from(HELLO) + ); + } +} diff --git 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