[pbs-devel] [PATCH proxmox 2/2] http: Teach client how to speak deflate

Maximiliano Sandoval m.sandoval at proxmox.com
Tue Mar 26 16:28:18 CET 2024


The Backup Server can speak deflate so we implement that.

Note that the spec [1] allows the server to encode the content multiple
times with different algorithms.

[1] https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding

Suggested-by: Lukas Wagner <l.wagner at proxmox.com>
Signed-off-by: Maximiliano Sandoval <m.sandoval at proxmox.com>
---
 proxmox-http/src/client/simple.rs | 98 ++++++++++++++++++++++++-------
 1 file changed, 78 insertions(+), 20 deletions(-)

diff --git a/proxmox-http/src/client/simple.rs b/proxmox-http/src/client/simple.rs
index b33154be..c3afa8d0 100644
--- a/proxmox-http/src/client/simple.rs
+++ b/proxmox-http/src/client/simple.rs
@@ -4,7 +4,7 @@ use std::io::Read;
 #[cfg(all(feature = "client-trait", feature = "proxmox-async"))]
 use std::str::FromStr;
 
-use flate2::read::GzDecoder;
+use flate2::read::{DeflateDecoder, GzDecoder};
 
 use futures::*;
 #[cfg(all(feature = "client-trait", feature = "proxmox-async"))]
@@ -76,7 +76,7 @@ impl Client {
 
         request.headers_mut().insert(
             hyper::header::ACCEPT_ENCODING,
-            HeaderValue::from_static("gzip"),
+            HeaderValue::from_static("gzip, deflate"),
         );
         request
             .headers_mut()
@@ -149,22 +149,24 @@ impl Client {
         match response {
             Ok(res) => {
                 let (mut parts, body) = res.into_parts();
-                let is_gzip_encoded = parts
-                    .headers
-                    .remove(&hyper::header::CONTENT_ENCODING)
-                    .is_some_and(|h| h == "gzip");
-
-                let buf = hyper::body::to_bytes(body).await?;
-                let new_body = if is_gzip_encoded {
-                    let mut gz = GzDecoder::new(&buf[..]);
-                    let mut s = String::new();
-                    gz.read_to_string(&mut s)?;
-                    s
-                } else {
-                    String::from_utf8(buf.to_vec())
-                        .map_err(|err| format_err!("Error converting HTTP result data: {}", err))?
+                let mut buf = hyper::body::to_bytes(body).await?.to_vec();
+                let content_encoding = parts.headers.remove(&hyper::header::CONTENT_ENCODING);
+
+                if let Some(content_encoding) = content_encoding {
+                    let encodings = content_encoding.to_str()?;
+                    for encoding in encodings.rsplit([',', ' ']) {
+                        buf = match encoding {
+                            "" => buf,  // "a, b" splits into ["a", "", "b"].
+                            "gzip" => decode_gzip(&buf[..])?,
+                            "deflate" => decode_deflate(&buf[..])?,
+                            other => anyhow::bail!("Unknown format: {other}"),
+                        }
+                    }
                 };
 
+                let new_body = String::from_utf8(buf)
+                    .map_err(|err| format_err!("Error converting HTTP result data: {}", err))?;
+
                 Ok(Response::from_parts(parts, new_body))
             }
             Err(err) => Err(err),
@@ -267,6 +269,10 @@ impl crate::HttpClient<String, String> for Client {
 mod test {
     use super::*;
 
+    use flate2::write::{DeflateEncoder, GzEncoder};
+    use flate2::Compression;
+    use std::io::Write;
+
     const BODY: &str = "hello world";
 
     #[tokio::test]
@@ -288,14 +294,66 @@ mod test {
         assert_eq!(Client::response_body_string(response).await.unwrap(), BODY);
     }
 
-    fn encode_gzip(bytes: &[u8]) -> Result<Vec<u8>, std::io::Error> {
-        use flate2::write::GzEncoder;
-        use flate2::Compression;
-        use std::io::Write;
+    #[tokio::test]
+    async fn test_parse_response_deflate() {
+        let encoded = encode_deflate(BODY.as_bytes()).unwrap();
+        let body = Body::from(encoded);
+
+        let response = Response::builder()
+            .header(hyper::header::CONTENT_ENCODING, "deflate")
+            .body(body)
+            .unwrap();
+        assert_eq!(Client::response_body_string(response).await.unwrap(), BODY);
+    }
+
+    #[tokio::test]
+    async fn test_parse_response_deflate_gzip() {
+        let deflate_encoded = encode_deflate(BODY.as_bytes()).unwrap();
+        let gzip_encoded = encode_gzip(&deflate_encoded).unwrap();
+        let body = Body::from(gzip_encoded);
+
+        let response = Response::builder()
+            .header(hyper::header::CONTENT_ENCODING, "deflate, gzip")
+            .body(body)
+            .unwrap();
+        assert_eq!(Client::response_body_string(response).await.unwrap(), BODY);
 
+        let gzip_encoded = encode_gzip(BODY.as_bytes()).unwrap();
+        let deflate_encoded = encode_deflate(&gzip_encoded).unwrap();
+        let body = Body::from(deflate_encoded);
+
+        let response = Response::builder()
+            .header(hyper::header::CONTENT_ENCODING, "gzip, deflate")
+            .body(body)
+            .unwrap();
+        assert_eq!(Client::response_body_string(response).await.unwrap(), BODY);
+    }
+
+    fn encode_deflate(bytes: &[u8]) -> Result<Vec<u8>, std::io::Error> {
+        let mut e = DeflateEncoder::new(Vec::new(), Compression::default());
+        e.write_all(bytes).unwrap();
+
+        e.finish()
+    }
+
+    fn encode_gzip(bytes: &[u8]) -> Result<Vec<u8>, std::io::Error> {
         let mut e = GzEncoder::new(Vec::new(), Compression::default());
         e.write_all(bytes).unwrap();
 
         e.finish()
     }
 }
+
+fn decode_gzip(buf: &[u8]) -> Result<Vec<u8>, std::io::Error> {
+    let mut dec = GzDecoder::new(buf);
+    let mut v = Vec::new();
+    dec.read_to_end(&mut v)?;
+    Ok(v)
+}
+
+fn decode_deflate(buf: &[u8]) -> Result<Vec<u8>, std::io::Error> {
+    let mut dec = DeflateDecoder::new(buf);
+    let mut v = Vec::new();
+    dec.read_to_end(&mut v)?;
+    Ok(v)
+}
-- 
2.39.2





More information about the pbs-devel mailing list