ugi 0.2.1

Runtime-agnostic Rust request client with HTTP/1.1, HTTP/2, HTTP/3, H2C, WebSocket, SSE, and gRPC support
Documentation
//! Response body decoding — gzip, deflate, brotli, and zstd decompression.
//!
//! This module provides the `CompressionMode` enum and functions for decoding
//! response bodies based on their `Content-Encoding` header.

use flate2::read::{DeflateDecoder, GzDecoder};
use std::io::Read;

use crate::body::Body;
use crate::error::{Error, ErrorKind, Result};
use crate::header::HeaderMap;

/// Controls how the client handles response body compression.
///
/// - `Auto` (default): automatically sends `Accept-Encoding` and decodes
///   compressed responses transparently.
/// - `Manual`: does not add `Accept-Encoding` and does not decode; the caller
///   receives raw (possibly compressed) bytes.
/// - `Disabled`: explicitly requests no encoding by omitting `Accept-Encoding`.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum CompressionMode {
    /// Automatically negotiate and decode compressed responses (default).
    #[default]
    Auto,
    /// Skip auto-negotiation and decoding; caller handles raw bytes.
    Manual,
    /// Explicitly disable compression by not advertising supported encodings.
    Disabled,
}

impl CompressionMode {
    pub(crate) fn should_add_accept_encoding(self) -> bool {
        matches!(self, Self::Auto)
    }

    pub(crate) fn should_auto_decode(self) -> bool {
        matches!(self, Self::Auto)
    }
}

/// The default `Accept-Encoding` value advertised by auto-compression mode.
pub(crate) const DEFAULT_ACCEPT_ENCODING: &str = {
    #[cfg(all(feature = "brotli", feature = "zstd"))]
    {
        "gzip, deflate, br, zstd"
    }
    #[cfg(all(feature = "brotli", not(feature = "zstd")))]
    {
        "gzip, deflate, br"
    }
    #[cfg(all(not(feature = "brotli"), feature = "zstd"))]
    {
        "gzip, deflate, zstd"
    }
    #[cfg(all(not(feature = "brotli"), not(feature = "zstd")))]
    {
        "gzip, deflate"
    }
};

/// Decode a response body according to its `Content-Encoding` header.
///
/// Strips `content-encoding` and `content-length` from `headers` after
/// successful decoding so that callers see the decompressed length.
/// Unknown encodings are passed through unchanged.
pub(crate) fn decode_response_body(headers: &mut HeaderMap, body: Vec<u8>) -> Result<Body> {
    let Some(content_encoding) = headers.get("content-encoding") else {
        return Ok(Body::from(body));
    };

    let decoded = match content_encoding {
        "gzip" => decode_gzip(&body)?,
        "deflate" => decode_deflate(&body)?,
        #[cfg(feature = "brotli")]
        "br" => decode_brotli(&body)?,
        #[cfg(feature = "zstd")]
        "zstd" => decode_zstd(&body)?,
        _ => return Ok(Body::from(body)),
    };

    headers.remove("content-encoding");
    headers.remove("content-length");
    Ok(Body::from(decoded))
}

/// Conditionally decode the response body based on the client's compression mode.
///
/// If `compression_mode` has auto-decoding enabled, delegates to
/// [`decode_response_body`].  Otherwise returns the body unchanged.
pub(crate) fn maybe_decode_response_body(
    headers: &mut HeaderMap,
    body: Vec<u8>,
    compression_mode: CompressionMode,
) -> Result<Body> {
    if compression_mode.should_auto_decode() {
        decode_response_body(headers, body)
    } else {
        Ok(Body::from(body))
    }
}

fn decode_gzip(body: &[u8]) -> Result<Vec<u8>> {
    let mut decoder = GzDecoder::new(body);
    let mut decoded = Vec::new();
    decoder
        .read_to_end(&mut decoded)
        .map_err(|err| Error::with_source(ErrorKind::Decode, "failed to decode gzip body", err))?;
    Ok(decoded)
}

fn decode_deflate(body: &[u8]) -> Result<Vec<u8>> {
    let mut decoder = DeflateDecoder::new(body);
    let mut decoded = Vec::new();
    decoder.read_to_end(&mut decoded).map_err(|err| {
        Error::with_source(ErrorKind::Decode, "failed to decode deflate body", err)
    })?;
    Ok(decoded)
}

#[cfg(feature = "brotli")]
fn decode_brotli(body: &[u8]) -> Result<Vec<u8>> {
    let mut decoder = brotli::Decompressor::new(body, 4096);
    let mut decoded = Vec::new();
    decoder.read_to_end(&mut decoded).map_err(|err| {
        Error::with_source(ErrorKind::Decode, "failed to decode brotli body", err)
    })?;
    Ok(decoded)
}

#[cfg(feature = "zstd")]
fn decode_zstd(body: &[u8]) -> Result<Vec<u8>> {
    let mut decoder = zstd::stream::read::Decoder::new(body).map_err(|err| {
        Error::with_source(ErrorKind::Decode, "failed to initialize zstd decoder", err)
    })?;
    let mut decoded = Vec::new();
    decoder
        .read_to_end(&mut decoded)
        .map_err(|err| Error::with_source(ErrorKind::Decode, "failed to decode zstd body", err))?;
    Ok(decoded)
}