Skip to content

Commit

Permalink
decode responses in zstd format
Browse files Browse the repository at this point in the history
  • Loading branch information
zuisong committed Apr 13, 2024
1 parent 0c335ac commit f74da51
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 13 deletions.
42 changes: 42 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ termcolor = "1.1.2"
time = "0.3.16"
unicode-width = "0.1.9"
url = "2.2.2"
zstd = { version = "0.13.1", default-features = false }

[dependencies.reqwest]
version = "0.12.3"
Expand Down
16 changes: 15 additions & 1 deletion src/decoder.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
use std::io::{self, Read};
use std::io::{self, BufReader, Read};
use std::str::FromStr;

use brotli::Decompressor as BrotliDecoder;
use flate2::read::{GzDecoder, ZlibDecoder};
use reqwest::header::{HeaderMap, CONTENT_ENCODING, CONTENT_LENGTH, TRANSFER_ENCODING};
use zstd::Decoder as ZstdDecoder;

#[derive(Debug)]
pub enum CompressionType {
Gzip,
Deflate,
Brotli,
Zstd,
}

impl FromStr for CompressionType {
Expand All @@ -19,6 +21,7 @@ impl FromStr for CompressionType {
"gzip" => Ok(CompressionType::Gzip),
"deflate" => Ok(CompressionType::Deflate),
"br" => Ok(CompressionType::Brotli),
"zstd" => Ok(CompressionType::Zstd),
_ => Err(anyhow::anyhow!("unknown compression type")),
}
}
Expand Down Expand Up @@ -88,6 +91,7 @@ enum Decoder<R: Read> {
Gzip(GzDecoder<InnerReader<R>>),
Deflate(ZlibDecoder<InnerReader<R>>),
Brotli(BrotliDecoder<InnerReader<R>>),
Zstd(ZstdDecoder<'static, BufReader<InnerReader<R>>>),
}

impl<R: Read> Read for Decoder<R> {
Expand Down Expand Up @@ -121,6 +125,15 @@ impl<R: Read> Read for Decoder<R> {
format!("error decoding brotli response body: {}", e),
)),
},
Decoder::Zstd(decoder) => match decoder.read(buf) {
Ok(n) => Ok(n),
Err(e) if decoder.get_ref().get_ref().has_errored => Err(e),
Err(_) if !decoder.get_ref().get_ref().has_read_data => Ok(0),
Err(e) => Err(io::Error::new(
e.kind(),
format!("error decoding zstd response body: {}", e),
)),
},
}
}
}
Expand All @@ -135,6 +148,7 @@ pub fn decompress(
Some(CompressionType::Deflate) => Decoder::Deflate(ZlibDecoder::new(reader)),
Some(CompressionType::Brotli) => Decoder::Brotli(BrotliDecoder::new(reader, 4096)),
None => Decoder::PlainText(reader),
Some(CompressionType::Zstd) => Decoder::Zstd(ZstdDecoder::new(reader).unwrap()),
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ fn run(args: Cli) -> Result<i32> {
.request(method, url.clone())
.header(
ACCEPT_ENCODING,
HeaderValue::from_static("gzip, deflate, br"),
HeaderValue::from_static("gzip, deflate, br, zstd"),
)
.header(USER_AGENT, get_user_agent());

Expand Down
47 changes: 36 additions & 11 deletions tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,7 @@ fn verbose() {
.stdout(indoc! {r#"
POST / HTTP/1.1
Accept: application/json, */*;q=0.5
Accept-Encoding: gzip, deflate, br
Accept-Encoding: gzip, deflate, br, zstd
Connection: keep-alive
Content-Length: 9
Content-Type: application/json
Expand Down Expand Up @@ -940,7 +940,7 @@ fn digest_auth_with_redirection() {
.stdout(indoc! {r#"
GET /login_page HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Encoding: gzip, deflate, br, zstd
Connection: keep-alive
Host: http.mock
User-Agent: xh/0.0.0 (test mode)
Expand All @@ -954,7 +954,7 @@ fn digest_auth_with_redirection() {
GET /login_page HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Encoding: gzip, deflate, br, zstd
Authorization: Digest username="ahmed", realm="[email protected]", nonce="e5051361f053723a807674177fc7022f", uri="/login_page", qop=auth, nc=00000001, cnonce="f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ", response="894fd5ee1dcc702df7e4a6abed37fd56", opaque="9dcf562038f1ec1c8d02f218ef0e7a4b", algorithm=MD5
Connection: keep-alive
Host: http.mock
Expand All @@ -969,7 +969,7 @@ fn digest_auth_with_redirection() {
GET /admin_page HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Encoding: gzip, deflate, br, zstd
Connection: keep-alive
Host: http.mock
User-Agent: xh/0.0.0 (test mode)
Expand Down Expand Up @@ -2015,7 +2015,7 @@ fn can_unset_default_headers() {
.stdout(indoc! {r#"
GET / HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Encoding: gzip, deflate, br, zstd
Connection: keep-alive
Host: http.mock
Expand All @@ -2030,7 +2030,7 @@ fn can_unset_headers() {
.stdout(indoc! {r#"
GET / HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Encoding: gzip, deflate, br, zstd
Connection: keep-alive
Hello: world
Host: http.mock
Expand All @@ -2047,7 +2047,7 @@ fn can_set_unset_header() {
.stdout(indoc! {r#"
GET / HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Encoding: gzip, deflate, br, zstd
Connection: keep-alive
Hello: world
Host: http.mock
Expand Down Expand Up @@ -2780,7 +2780,7 @@ fn print_intermediate_requests_and_responses() {
.stdout(indoc! {r#"
GET /first_page HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Encoding: gzip, deflate, br, zstd
Connection: keep-alive
Host: http.mock
User-Agent: xh/0.0.0 (test mode)
Expand All @@ -2794,7 +2794,7 @@ fn print_intermediate_requests_and_responses() {
GET /second_page HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Encoding: gzip, deflate, br, zstd
Connection: keep-alive
Host: http.mock
User-Agent: xh/0.0.0 (test mode)
Expand Down Expand Up @@ -2835,7 +2835,7 @@ fn history_print() {
.stdout(indoc! {r#"
GET /first_page HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Encoding: gzip, deflate, br, zstd
Connection: keep-alive
Host: http.mock
User-Agent: xh/0.0.0 (test mode)
Expand All @@ -2847,7 +2847,7 @@ fn history_print() {
GET /second_page HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Encoding: gzip, deflate, br, zstd
Connection: keep-alive
Host: http.mock
User-Agent: xh/0.0.0 (test mode)
Expand Down Expand Up @@ -3378,6 +3378,31 @@ fn brotli() {
"#});
}

#[test]
fn zstd() {
let server = server::http(|_req| async move {
let compressed_bytes = fs::read("./tests/fixtures/responses/hello_world.zst").unwrap();
hyper::Response::builder()
.header("date", "N/A")
.header("content-encoding", "zstd")
.body(compressed_bytes.into())
.unwrap()
});

get_command()
.arg(server.base_url())
.assert()
.stdout(indoc! {r#"
HTTP/1.1 200 OK
Content-Encoding: zstd
Content-Length: 25
Date: N/A
Hello world
"#});
}

#[test]
fn empty_response_with_content_encoding() {
let server = server::http(|_req| async move {
Expand Down
3 changes: 3 additions & 0 deletions tests/fixtures/responses/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,7 @@ $ pigz -z hello_world # hello_world.zz

$ echo "Hello world" > hello_world
$ brotli hello_world # hello_world.br

$ echo "Hello world" > hello_world
$ zstd hello_world # hello_world.zst
```
Binary file added tests/fixtures/responses/hello_world.zst
Binary file not shown.

0 comments on commit f74da51

Please sign in to comment.