From 2c75a6adae9db19fc194afd92f1e0c68df4e692c Mon Sep 17 00:00:00 2001 From: Ilya Averyanov Date: Fri, 3 Jan 2025 17:42:35 +0200 Subject: [PATCH] Add deflate compression support --- tests/compression/Cargo.toml | 2 +- tonic/Cargo.toml | 1 + tonic/src/client/grpc.rs | 2 +- tonic/src/codec/compression.rs | 51 +++++++++++++++++++++++++++------- tonic/src/lib.rs | 2 ++ tonic/src/response.rs | 2 +- tonic/src/server/grpc.rs | 2 +- 7 files changed, 48 insertions(+), 14 deletions(-) diff --git a/tests/compression/Cargo.toml b/tests/compression/Cargo.toml index 31ac8c60b..7371ca3ae 100644 --- a/tests/compression/Cargo.toml +++ b/tests/compression/Cargo.toml @@ -16,7 +16,7 @@ pin-project = "1.0" prost = "0.13" tokio = {version = "1.0", features = ["macros", "rt-multi-thread", "net"]} tokio-stream = "0.1" -tonic = {path = "../../tonic", features = ["gzip", "zstd"]} +tonic = {path = "../../tonic", features = ["gzip", "deflate", "zstd"]} tower = "0.5" tower-http = {version = "0.6", features = ["map-response-body", "map-request-body"]} diff --git a/tonic/Cargo.toml b/tonic/Cargo.toml index 4b716dcfe..419c7d33a 100644 --- a/tonic/Cargo.toml +++ b/tonic/Cargo.toml @@ -25,6 +25,7 @@ version = "0.13.0" [features] codegen = ["dep:async-trait"] gzip = ["dep:flate2"] +deflate = ["dep:flate2"] zstd = ["dep:zstd"] default = ["transport", "codegen", "prost"] prost = ["dep:prost"] diff --git a/tonic/src/client/grpc.rs b/tonic/src/client/grpc.rs index 484db7fca..cd8a6b7d6 100644 --- a/tonic/src/client/grpc.rs +++ b/tonic/src/client/grpc.rs @@ -404,7 +404,7 @@ impl GrpcConfig { .headers_mut() .insert(CONTENT_TYPE, GRPC_CONTENT_TYPE); - #[cfg(any(feature = "gzip", feature = "zstd"))] + #[cfg(any(feature = "gzip", feature = "deflate", feature = "zstd"))] if let Some(encoding) = self.send_compression_encodings { request.headers_mut().insert( crate::codec::compression::ENCODING_HEADER, diff --git a/tonic/src/codec/compression.rs b/tonic/src/codec/compression.rs index 810813b98..f7a970cb3 100644 --- a/tonic/src/codec/compression.rs +++ b/tonic/src/codec/compression.rs @@ -1,5 +1,7 @@ use crate::{metadata::MetadataValue, Status}; use bytes::{Buf, BufMut, BytesMut}; +#[cfg(feature = "deflate")] +use flate2::read::{DeflateDecoder, DeflateEncoder}; #[cfg(feature = "gzip")] use flate2::read::{GzDecoder, GzEncoder}; use std::fmt; @@ -14,7 +16,7 @@ pub(crate) const ACCEPT_ENCODING_HEADER: &str = "grpc-accept-encoding"; /// Represents an ordered list of compression encodings that are enabled. #[derive(Debug, Default, Clone, Copy)] pub struct EnabledCompressionEncodings { - inner: [Option; 2], + inner: [Option; 3], } impl EnabledCompressionEncodings { @@ -85,6 +87,9 @@ pub enum CompressionEncoding { #[cfg(feature = "gzip")] Gzip, #[allow(missing_docs)] + #[cfg(feature = "deflate")] + Deflate, + #[allow(missing_docs)] #[cfg(feature = "zstd")] Zstd, } @@ -93,6 +98,8 @@ impl CompressionEncoding { pub(crate) const ENCODINGS: &'static [CompressionEncoding] = &[ #[cfg(feature = "gzip")] CompressionEncoding::Gzip, + #[cfg(feature = "deflate")] + CompressionEncoding::Deflate, #[cfg(feature = "zstd")] CompressionEncoding::Zstd, ]; @@ -112,6 +119,8 @@ impl CompressionEncoding { split_by_comma(header_value_str).find_map(|value| match value { #[cfg(feature = "gzip")] "gzip" => Some(CompressionEncoding::Gzip), + #[cfg(feature = "deflate")] + "deflate" => Some(CompressionEncoding::Deflate), #[cfg(feature = "zstd")] "zstd" => Some(CompressionEncoding::Zstd), _ => None, @@ -132,6 +141,10 @@ impl CompressionEncoding { b"gzip" if enabled_encodings.is_enabled(CompressionEncoding::Gzip) => { Ok(Some(CompressionEncoding::Gzip)) } + #[cfg(feature = "deflate")] + b"deflate" if enabled_encodings.is_enabled(CompressionEncoding::Deflate) => { + Ok(Some(CompressionEncoding::Deflate)) + } #[cfg(feature = "zstd")] b"zstd" if enabled_encodings.is_enabled(CompressionEncoding::Zstd) => { Ok(Some(CompressionEncoding::Zstd)) @@ -170,6 +183,8 @@ impl CompressionEncoding { match self { #[cfg(feature = "gzip")] CompressionEncoding::Gzip => "gzip", + #[cfg(feature = "deflate")] + CompressionEncoding::Deflate => "deflate", #[cfg(feature = "zstd")] CompressionEncoding::Zstd => "zstd", } @@ -217,6 +232,15 @@ pub(crate) fn compress( ); std::io::copy(&mut gzip_encoder, &mut out_writer)?; } + #[cfg(feature = "deflate")] + CompressionEncoding::Deflate => { + let mut deflate_encoder = DeflateEncoder::new( + &decompressed_buf[0..len], + // FIXME: support customizing the compression level + flate2::Compression::new(6), + ); + std::io::copy(&mut deflate_encoder, &mut out_writer)?; + } #[cfg(feature = "zstd")] CompressionEncoding::Zstd => { let mut zstd_encoder = Encoder::new( @@ -247,7 +271,7 @@ pub(crate) fn decompress( ((estimate_decompressed_len / buffer_growth_interval) + 1) * buffer_growth_interval; out_buf.reserve(capacity); - #[cfg(any(feature = "gzip", feature = "zstd"))] + #[cfg(any(feature = "gzip", feature = "deflate", feature = "zstd"))] let mut out_writer = out_buf.writer(); match settings.encoding { @@ -256,6 +280,11 @@ pub(crate) fn decompress( let mut gzip_decoder = GzDecoder::new(&compressed_buf[0..len]); std::io::copy(&mut gzip_decoder, &mut out_writer)?; } + #[cfg(feature = "deflate")] + CompressionEncoding::Deflate => { + let mut deflate_decoder = DeflateDecoder::new(&compressed_buf[0..len]); + std::io::copy(&mut deflate_decoder, &mut out_writer)?; + } #[cfg(feature = "zstd")] CompressionEncoding::Zstd => { let mut zstd_decoder = Decoder::new(&compressed_buf[0..len])?; @@ -282,7 +311,7 @@ pub enum SingleMessageCompressionOverride { #[cfg(test)] mod tests { - #[cfg(any(feature = "gzip", feature = "zstd"))] + #[cfg(any(feature = "gzip", feature = "deflate", feature = "zstd"))] use http::HeaderValue; use super::*; @@ -300,13 +329,13 @@ mod tests { const GZIP: HeaderValue = HeaderValue::from_static("gzip,identity"); let encodings = EnabledCompressionEncodings { - inner: [Some(CompressionEncoding::Gzip), None], + inner: [Some(CompressionEncoding::Gzip), None, None], }; assert_eq!(encodings.into_accept_encoding_header_value().unwrap(), GZIP); let encodings = EnabledCompressionEncodings { - inner: [None, Some(CompressionEncoding::Gzip)], + inner: [None, None, Some(CompressionEncoding::Gzip)], }; assert_eq!(encodings.into_accept_encoding_header_value().unwrap(), GZIP); @@ -318,43 +347,45 @@ mod tests { const ZSTD: HeaderValue = HeaderValue::from_static("zstd,identity"); let encodings = EnabledCompressionEncodings { - inner: [Some(CompressionEncoding::Zstd), None], + inner: [Some(CompressionEncoding::Zstd), None, None], }; assert_eq!(encodings.into_accept_encoding_header_value().unwrap(), ZSTD); let encodings = EnabledCompressionEncodings { - inner: [None, Some(CompressionEncoding::Zstd)], + inner: [None, None, Some(CompressionEncoding::Zstd)], }; assert_eq!(encodings.into_accept_encoding_header_value().unwrap(), ZSTD); } #[test] - #[cfg(all(feature = "gzip", feature = "zstd"))] + #[cfg(all(feature = "gzip", feature = "deflate", feature = "zstd"))] fn convert_gzip_and_zstd_into_header_value() { let encodings = EnabledCompressionEncodings { inner: [ Some(CompressionEncoding::Gzip), + Some(CompressionEncoding::Deflate), Some(CompressionEncoding::Zstd), ], }; assert_eq!( encodings.into_accept_encoding_header_value().unwrap(), - HeaderValue::from_static("gzip,zstd,identity"), + HeaderValue::from_static("gzip,deflate,zstd,identity"), ); let encodings = EnabledCompressionEncodings { inner: [ Some(CompressionEncoding::Zstd), + Some(CompressionEncoding::Deflate), Some(CompressionEncoding::Gzip), ], }; assert_eq!( encodings.into_accept_encoding_header_value().unwrap(), - HeaderValue::from_static("zstd,gzip,identity"), + HeaderValue::from_static("zstd,deflate,gzip,identity"), ); } } diff --git a/tonic/src/lib.rs b/tonic/src/lib.rs index 6cdc41027..3dcbd2108 100644 --- a/tonic/src/lib.rs +++ b/tonic/src/lib.rs @@ -35,6 +35,8 @@ //! - `prost`: Enables the [`prost`] based gRPC [`Codec`] implementation. Enabled by default. //! - `gzip`: Enables compressing requests, responses, and streams. Depends on [`flate2`]. //! Not enabled by default. +//! - `deflate`: Enables compressing requests, responses, and streams. Depends on [`flate2`]. +//! Not enabled by default. //! - `zstd`: Enables compressing requests, responses, and streams. Depends on [`zstd`]. //! Not enabled by default. //! diff --git a/tonic/src/response.rs b/tonic/src/response.rs index 1089cdda4..e22e25eaf 100644 --- a/tonic/src/response.rs +++ b/tonic/src/response.rs @@ -120,7 +120,7 @@ impl Response { /// **Note**: This only has effect on responses to unary requests and responses to client to /// server streams. Response streams (server to client stream and bidirectional streams) will /// still be compressed according to the configuration of the server. - #[cfg(feature = "gzip")] + #[cfg(any(feature = "gzip", feature = "deflate", feature = "zstd"))] pub fn disable_compression(&mut self) { self.extensions_mut() .insert(crate::codec::compression::SingleMessageCompressionOverride::Disable); diff --git a/tonic/src/server/grpc.rs b/tonic/src/server/grpc.rs index 81a048222..0b057fcfb 100644 --- a/tonic/src/server/grpc.rs +++ b/tonic/src/server/grpc.rs @@ -435,7 +435,7 @@ where .headers .insert(http::header::CONTENT_TYPE, GRPC_CONTENT_TYPE); - #[cfg(any(feature = "gzip", feature = "zstd"))] + #[cfg(any(feature = "gzip", feature = "deflate", feature = "zstd"))] if let Some(encoding) = accept_encoding { // Set the content encoding parts.headers.insert(