Skip to content

Commit 1073881

Browse files
feat: add zstd support (#1866)
Closes #1463
1 parent 1af8945 commit 1073881

File tree

8 files changed

+364
-20
lines changed

8 files changed

+364
-20
lines changed

.github/workflows/ci.yml

+6-4
Original file line numberDiff line numberDiff line change
@@ -103,23 +103,23 @@ jobs:
103103
- name: windows / stable-x86_64-msvc
104104
os: windows-latest
105105
target: x86_64-pc-windows-msvc
106-
features: "--features blocking,gzip,brotli,deflate,json,multipart,stream"
106+
features: "--features blocking,gzip,brotli,zstd,deflate,json,multipart,stream"
107107
- name: windows / stable-i686-msvc
108108
os: windows-latest
109109
target: i686-pc-windows-msvc
110-
features: "--features blocking,gzip,brotli,deflate,json,multipart,stream"
110+
features: "--features blocking,gzip,brotli,zstd,deflate,json,multipart,stream"
111111
- name: windows / stable-x86_64-gnu
112112
os: windows-latest
113113
rust: stable-x86_64-pc-windows-gnu
114114
target: x86_64-pc-windows-gnu
115-
features: "--features blocking,gzip,brotli,deflate,json,multipart,stream"
115+
features: "--features blocking,gzip,brotli,zstd,deflate,json,multipart,stream"
116116
package_name: mingw-w64-x86_64-gcc
117117
mingw64_path: "C:\\msys64\\mingw64\\bin"
118118
- name: windows / stable-i686-gnu
119119
os: windows-latest
120120
rust: stable-i686-pc-windows-gnu
121121
target: i686-pc-windows-gnu
122-
features: "--features blocking,gzip,brotli,deflate,json,multipart,stream"
122+
features: "--features blocking,gzip,brotli,zstd,deflate,json,multipart,stream"
123123
package_name: mingw-w64-i686-gcc
124124
mingw64_path: "C:\\msys64\\mingw32\\bin"
125125

@@ -145,6 +145,8 @@ jobs:
145145
features: "--features gzip,stream"
146146
- name: "feat.: brotli"
147147
features: "--features brotli,stream"
148+
- name: "feat.: zstd"
149+
features: "--features zstd,stream"
148150
- name: "feat.: deflate"
149151
features: "--features deflate,stream"
150152
- name: "feat.: json"

Cargo.toml

+8
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ gzip = ["dep:async-compression", "async-compression?/gzip", "dep:tokio-util"]
5555

5656
brotli = ["dep:async-compression", "async-compression?/brotli", "dep:tokio-util"]
5757

58+
zstd = ["dep:async-compression", "async-compression?/zstd", "dep:tokio-util"]
59+
5860
deflate = ["dep:async-compression", "async-compression?/zlib", "dep:tokio-util"]
5961

6062
json = ["dep:serde_json"]
@@ -167,6 +169,7 @@ hyper-util = { version = "0.1", features = ["http1", "http2", "client", "client-
167169
serde = { version = "1.0", features = ["derive"] }
168170
libflate = "1.0"
169171
brotli_crate = { package = "brotli", version = "3.3.0" }
172+
zstd_crate = { package = "zstd", version = "0.13" }
170173
doc-comment = "0.3"
171174
tokio = { version = "1.0", default-features = false, features = ["macros", "rt-multi-thread"] }
172175
futures-util = { version = "0.3.0", default-features = false, features = ["std", "alloc"] }
@@ -258,6 +261,11 @@ name = "brotli"
258261
path = "tests/brotli.rs"
259262
required-features = ["brotli", "stream"]
260263

264+
[[test]]
265+
name = "zstd"
266+
path = "tests/zstd.rs"
267+
required-features = ["zstd", "stream"]
268+
261269
[[test]]
262270
name = "deflate"
263271
path = "tests/deflate.rs"

src/async_impl/client.rs

+40
Original file line numberDiff line numberDiff line change
@@ -911,6 +911,29 @@ impl ClientBuilder {
911911
self
912912
}
913913

914+
/// Enable auto zstd decompression by checking the `Content-Encoding` response header.
915+
///
916+
/// If auto zstd decompression is turned on:
917+
///
918+
/// - When sending a request and if the request's headers do not already contain
919+
/// an `Accept-Encoding` **and** `Range` values, the `Accept-Encoding` header is set to `zstd`.
920+
/// The request body is **not** automatically compressed.
921+
/// - When receiving a response, if its headers contain a `Content-Encoding` value of
922+
/// `zstd`, both `Content-Encoding` and `Content-Length` are removed from the
923+
/// headers' set. The response body is automatically decompressed.
924+
///
925+
/// If the `zstd` feature is turned on, the default option is enabled.
926+
///
927+
/// # Optional
928+
///
929+
/// This requires the optional `zstd` feature to be enabled
930+
#[cfg(feature = "zstd")]
931+
#[cfg_attr(docsrs, doc(cfg(feature = "zstd")))]
932+
pub fn zstd(mut self, enable: bool) -> ClientBuilder {
933+
self.config.accepts.zstd = enable;
934+
self
935+
}
936+
914937
/// Enable auto deflate decompression by checking the `Content-Encoding` response header.
915938
///
916939
/// If auto deflate decompression is turned on:
@@ -968,6 +991,23 @@ impl ClientBuilder {
968991
}
969992
}
970993

994+
/// Disable auto response body zstd decompression.
995+
///
996+
/// This method exists even if the optional `zstd` feature is not enabled.
997+
/// This can be used to ensure a `Client` doesn't use zstd decompression
998+
/// even if another dependency were to enable the optional `zstd` feature.
999+
pub fn no_zstd(self) -> ClientBuilder {
1000+
#[cfg(feature = "zstd")]
1001+
{
1002+
self.zstd(false)
1003+
}
1004+
1005+
#[cfg(not(feature = "zstd"))]
1006+
{
1007+
self
1008+
}
1009+
}
1010+
9711011
/// Disable auto response body deflate decompression.
9721012
///
9731013
/// This method exists even if the optional `deflate` feature is not enabled.

src/async_impl/decoder.rs

+128-16
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ use async_compression::tokio::bufread::GzipDecoder;
99
#[cfg(feature = "brotli")]
1010
use async_compression::tokio::bufread::BrotliDecoder;
1111

12+
#[cfg(feature = "zstd")]
13+
use async_compression::tokio::bufread::ZstdDecoder;
14+
1215
#[cfg(feature = "deflate")]
1316
use async_compression::tokio::bufread::ZlibDecoder;
1417

@@ -19,9 +22,19 @@ use http::HeaderMap;
1922
use hyper::body::Body as HttpBody;
2023
use hyper::body::Frame;
2124

22-
#[cfg(any(feature = "gzip", feature = "brotli", feature = "deflate"))]
25+
#[cfg(any(
26+
feature = "gzip",
27+
feature = "brotli",
28+
feature = "zstd",
29+
feature = "deflate"
30+
))]
2331
use tokio_util::codec::{BytesCodec, FramedRead};
24-
#[cfg(any(feature = "gzip", feature = "brotli", feature = "deflate"))]
32+
#[cfg(any(
33+
feature = "gzip",
34+
feature = "brotli",
35+
feature = "zstd",
36+
feature = "deflate"
37+
))]
2538
use tokio_util::io::StreamReader;
2639

2740
use super::body::ResponseBody;
@@ -33,6 +46,8 @@ pub(super) struct Accepts {
3346
pub(super) gzip: bool,
3447
#[cfg(feature = "brotli")]
3548
pub(super) brotli: bool,
49+
#[cfg(feature = "zstd")]
50+
pub(super) zstd: bool,
3651
#[cfg(feature = "deflate")]
3752
pub(super) deflate: bool,
3853
}
@@ -44,6 +59,8 @@ impl Accepts {
4459
gzip: false,
4560
#[cfg(feature = "brotli")]
4661
brotli: false,
62+
#[cfg(feature = "zstd")]
63+
zstd: false,
4764
#[cfg(feature = "deflate")]
4865
deflate: false,
4966
}
@@ -59,7 +76,12 @@ pub(crate) struct Decoder {
5976

6077
type PeekableIoStream = Peekable<IoStream>;
6178

62-
#[cfg(any(feature = "gzip", feature = "brotli", feature = "deflate"))]
79+
#[cfg(any(
80+
feature = "gzip",
81+
feature = "zstd",
82+
feature = "brotli",
83+
feature = "deflate"
84+
))]
6385
type PeekableIoStreamReader = StreamReader<PeekableIoStream, Bytes>;
6486

6587
enum Inner {
@@ -74,12 +96,21 @@ enum Inner {
7496
#[cfg(feature = "brotli")]
7597
Brotli(Pin<Box<FramedRead<BrotliDecoder<PeekableIoStreamReader>, BytesCodec>>>),
7698

99+
/// A `Zstd` decoder will uncompress the zstd compressed response content before returning it.
100+
#[cfg(feature = "zstd")]
101+
Zstd(Pin<Box<FramedRead<ZstdDecoder<PeekableIoStreamReader>, BytesCodec>>>),
102+
77103
/// A `Deflate` decoder will uncompress the deflated response content before returning it.
78104
#[cfg(feature = "deflate")]
79105
Deflate(Pin<Box<FramedRead<ZlibDecoder<PeekableIoStreamReader>, BytesCodec>>>),
80106

81107
/// A decoder that doesn't have a value yet.
82-
#[cfg(any(feature = "brotli", feature = "gzip", feature = "deflate"))]
108+
#[cfg(any(
109+
feature = "brotli",
110+
feature = "zstd",
111+
feature = "gzip",
112+
feature = "deflate"
113+
))]
83114
Pending(Pin<Box<Pending>>),
84115
}
85116

@@ -93,6 +124,8 @@ enum DecoderType {
93124
Gzip,
94125
#[cfg(feature = "brotli")]
95126
Brotli,
127+
#[cfg(feature = "zstd")]
128+
Zstd,
96129
#[cfg(feature = "deflate")]
97130
Deflate,
98131
}
@@ -155,6 +188,21 @@ impl Decoder {
155188
}
156189
}
157190

191+
/// A zstd decoder.
192+
///
193+
/// This decoder will buffer and decompress chunks that are zstd compressed.
194+
#[cfg(feature = "zstd")]
195+
fn zstd(body: ResponseBody) -> Decoder {
196+
use futures_util::StreamExt;
197+
198+
Decoder {
199+
inner: Inner::Pending(Box::pin(Pending(
200+
IoStream(body).peekable(),
201+
DecoderType::Zstd,
202+
))),
203+
}
204+
}
205+
158206
/// A deflate decoder.
159207
///
160208
/// This decoder will buffer and decompress chunks that are deflated.
@@ -170,7 +218,12 @@ impl Decoder {
170218
}
171219
}
172220

173-
#[cfg(any(feature = "brotli", feature = "gzip", feature = "deflate"))]
221+
#[cfg(any(
222+
feature = "brotli",
223+
feature = "zstd",
224+
feature = "gzip",
225+
feature = "deflate"
226+
))]
174227
fn detect_encoding(headers: &mut HeaderMap, encoding_str: &str) -> bool {
175228
use http::header::{CONTENT_ENCODING, CONTENT_LENGTH, TRANSFER_ENCODING};
176229
use log::warn;
@@ -225,6 +278,13 @@ impl Decoder {
225278
}
226279
}
227280

281+
#[cfg(feature = "zstd")]
282+
{
283+
if _accepts.zstd && Decoder::detect_encoding(_headers, "zstd") {
284+
return Decoder::zstd(body);
285+
}
286+
}
287+
228288
#[cfg(feature = "deflate")]
229289
{
230290
if _accepts.deflate && Decoder::detect_encoding(_headers, "deflate") {
@@ -245,7 +305,12 @@ impl HttpBody for Decoder {
245305
cx: &mut Context,
246306
) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
247307
match self.inner {
248-
#[cfg(any(feature = "brotli", feature = "gzip", feature = "deflate"))]
308+
#[cfg(any(
309+
feature = "brotli",
310+
feature = "zstd",
311+
feature = "gzip",
312+
feature = "deflate"
313+
))]
249314
Inner::Pending(ref mut future) => match Pin::new(future).poll(cx) {
250315
Poll::Ready(Ok(inner)) => {
251316
self.inner = inner;
@@ -277,6 +342,14 @@ impl HttpBody for Decoder {
277342
None => Poll::Ready(None),
278343
}
279344
}
345+
#[cfg(feature = "zstd")]
346+
Inner::Zstd(ref mut decoder) => {
347+
match futures_core::ready!(Pin::new(decoder).poll_next(cx)) {
348+
Some(Ok(bytes)) => Poll::Ready(Some(Ok(Frame::data(bytes.freeze())))),
349+
Some(Err(err)) => Poll::Ready(Some(Err(crate::error::decode_io(err)))),
350+
None => Poll::Ready(None),
351+
}
352+
}
280353
#[cfg(feature = "deflate")]
281354
Inner::Deflate(ref mut decoder) => {
282355
match futures_core::ready!(Pin::new(decoder).poll_next(cx)) {
@@ -292,7 +365,12 @@ impl HttpBody for Decoder {
292365
match self.inner {
293366
Inner::PlainText(ref body) => HttpBody::size_hint(body),
294367
// the rest are "unknown", so default
295-
#[cfg(any(feature = "brotli", feature = "gzip", feature = "deflate"))]
368+
#[cfg(any(
369+
feature = "brotli",
370+
feature = "zstd",
371+
feature = "gzip",
372+
feature = "deflate"
373+
))]
296374
_ => http_body::SizeHint::default(),
297375
}
298376
}
@@ -332,6 +410,11 @@ impl Future for Pending {
332410
BrotliDecoder::new(StreamReader::new(_body)),
333411
BytesCodec::new(),
334412
))))),
413+
#[cfg(feature = "zstd")]
414+
DecoderType::Zstd => Poll::Ready(Ok(Inner::Zstd(Box::pin(FramedRead::new(
415+
ZstdDecoder::new(StreamReader::new(_body)),
416+
BytesCodec::new(),
417+
))))),
335418
#[cfg(feature = "gzip")]
336419
DecoderType::Gzip => Poll::Ready(Ok(Inner::Gzip(Box::pin(FramedRead::new(
337420
GzipDecoder::new(StreamReader::new(_body)),
@@ -381,22 +464,37 @@ impl Accepts {
381464
gzip: false,
382465
#[cfg(feature = "brotli")]
383466
brotli: false,
467+
#[cfg(feature = "zstd")]
468+
zstd: false,
384469
#[cfg(feature = "deflate")]
385470
deflate: false,
386471
}
387472
}
388473
*/
389474

390475
pub(super) fn as_str(&self) -> Option<&'static str> {
391-
match (self.is_gzip(), self.is_brotli(), self.is_deflate()) {
392-
(true, true, true) => Some("gzip, br, deflate"),
393-
(true, true, false) => Some("gzip, br"),
394-
(true, false, true) => Some("gzip, deflate"),
395-
(false, true, true) => Some("br, deflate"),
396-
(true, false, false) => Some("gzip"),
397-
(false, true, false) => Some("br"),
398-
(false, false, true) => Some("deflate"),
399-
(false, false, false) => None,
476+
match (
477+
self.is_gzip(),
478+
self.is_brotli(),
479+
self.is_zstd(),
480+
self.is_deflate(),
481+
) {
482+
(true, true, true, true) => Some("gzip, br, zstd, deflate"),
483+
(true, true, false, true) => Some("gzip, br, deflate"),
484+
(true, true, true, false) => Some("gzip, br, zstd"),
485+
(true, true, false, false) => Some("gzip, br"),
486+
(true, false, true, true) => Some("gzip, zstd, deflate"),
487+
(true, false, false, true) => Some("gzip, zstd, deflate"),
488+
(false, true, true, true) => Some("br, zstd, deflate"),
489+
(false, true, false, true) => Some("br, zstd, deflate"),
490+
(true, false, true, false) => Some("gzip, zstd"),
491+
(true, false, false, false) => Some("gzip"),
492+
(false, true, true, false) => Some("br, zstd"),
493+
(false, true, false, false) => Some("br"),
494+
(false, false, true, true) => Some("zstd, deflate"),
495+
(false, false, true, false) => Some("zstd"),
496+
(false, false, false, true) => Some("deflate"),
497+
(false, false, false, false) => None,
400498
}
401499
}
402500

@@ -424,6 +522,18 @@ impl Accepts {
424522
}
425523
}
426524

525+
fn is_zstd(&self) -> bool {
526+
#[cfg(feature = "zstd")]
527+
{
528+
self.zstd
529+
}
530+
531+
#[cfg(not(feature = "zstd"))]
532+
{
533+
false
534+
}
535+
}
536+
427537
fn is_deflate(&self) -> bool {
428538
#[cfg(feature = "deflate")]
429539
{
@@ -444,6 +554,8 @@ impl Default for Accepts {
444554
gzip: true,
445555
#[cfg(feature = "brotli")]
446556
brotli: true,
557+
#[cfg(feature = "zstd")]
558+
zstd: true,
447559
#[cfg(feature = "deflate")]
448560
deflate: true,
449561
}

0 commit comments

Comments
 (0)