Skip to main content

connectrpc/
compression.rs

1//! Pluggable compression support for ConnectRPC.
2//!
3//! This module provides a trait-based compression system that allows users to
4//! register custom compression providers. Built-in providers are available
5//! for common algorithms when the corresponding features are enabled:
6//!
7//! - `gzip` - Gzip compression via flate2 (enabled by default)
8//! - `zstd` - Zstandard compression via zstd (enabled by default)
9//!
10//! # Streaming Compression
11//!
12//! When the `streaming` feature is enabled (default), providers can also
13//! support streaming compression/decompression for handling large payloads
14//! without buffering the entire message in memory.
15//!
16//! # Example
17//!
18//! ```rust,ignore
19//! use connectrpc::compression::{CompressionRegistry, GzipProvider, ZstdProvider};
20//!
21//! // Create a registry with built-in providers
22//! let registry = CompressionRegistry::new()
23//!     .register(GzipProvider::default())
24//!     .register(ZstdProvider::default());
25//!
26//! // Or use the default registry (includes all feature-enabled providers)
27//! let registry = CompressionRegistry::default();
28//!
29//! // Use with server
30//! let server = Server::new(router).with_compression(registry);
31//! ```
32//!
33//! # Custom Providers
34//!
35//! ```rust,ignore
36//! use connectrpc::compression::CompressionProvider;
37//!
38//! struct MyCompression;
39//!
40//! impl CompressionProvider for MyCompression {
41//!     fn name(&self) -> &'static str { "my-algo" }
42//!     fn compress(&self, data: &[u8]) -> Result<Bytes, ConnectError> { ... }
43//!     fn decompressor<'a>(&self, data: &'a [u8]) -> Result<Box<dyn std::io::Read + 'a>, ConnectError> {
44//!         // Return a reader that yields decompressed bytes.
45//!         // The framework controls how much is read, so you get safe
46//!         // `decompress_with_limit` for free via `Read::take`.
47//!         ...
48//!     }
49//! }
50//!
51//! let registry = CompressionRegistry::new()
52//!     .register(MyCompression);
53//! ```
54
55use std::collections::HashMap;
56#[cfg(feature = "streaming")]
57use std::pin::Pin;
58use std::sync::Arc;
59
60use bytes::Bytes;
61#[cfg(feature = "streaming")]
62use tokio::io::AsyncBufRead;
63#[cfg(feature = "streaming")]
64use tokio::io::AsyncRead;
65
66use crate::error::ConnectError;
67
68#[cfg(any(feature = "gzip", feature = "zstd"))]
69fn malformed_compressed_payload(message: impl Into<String>) -> ConnectError {
70    ConnectError::invalid_argument(message)
71}
72
73// ============================================================================
74// Streaming Types
75// ============================================================================
76
77/// A boxed async reader for streaming compression/decompression.
78#[cfg(feature = "streaming")]
79#[cfg_attr(docsrs, doc(cfg(feature = "streaming")))]
80pub type BoxedAsyncRead = Pin<Box<dyn AsyncRead + Send>>;
81
82/// A boxed async buffered reader for streaming input.
83#[cfg(feature = "streaming")]
84#[cfg_attr(docsrs, doc(cfg(feature = "streaming")))]
85pub type BoxedAsyncBufRead = Pin<Box<dyn AsyncBufRead + Send>>;
86
87/// Trait for compression algorithm implementations.
88///
89/// Implement this trait to provide custom compression support. The only
90/// required methods are [`name`](Self::name), [`compress`](Self::compress),
91/// and [`decompressor`](Self::decompressor). The provided default for
92/// [`decompress_with_limit`](Self::decompress_with_limit) is structurally
93/// safe — the framework controls how many bytes are read from the
94/// decompressor, using [`Read::take`](std::io::Read::take) to cap output and prevent unbounded
95/// memory allocation from compression bombs.
96///
97/// All decompression goes through `decompress_with_limit` — there is no
98/// unbounded `decompress` method, by design.
99pub trait CompressionProvider: Send + Sync + 'static {
100    /// The encoding name for this algorithm.
101    ///
102    /// This should match the value used in Content-Encoding headers
103    /// (e.g., "gzip", "zstd", "br").
104    fn name(&self) -> &'static str;
105
106    /// Compress the given data.
107    fn compress(&self, data: &[u8]) -> Result<Bytes, ConnectError>;
108
109    /// Return a reader that yields decompressed bytes from `data`.
110    ///
111    /// This is the core decompression method that implementations must
112    /// provide. Most decompression libraries provide a `Read` adapter
113    /// (e.g. `flate2::read::GzDecoder`, `zstd::Decoder`,
114    /// `brotli::Decompressor`) — just wrap the input and return it.
115    ///
116    /// The framework controls the read loop, so implementations do not
117    /// need to worry about size limits. The default
118    /// [`decompress_with_limit`](Self::decompress_with_limit) uses
119    /// `Read::take(max_size + 1)` to structurally bound memory.
120    fn decompressor<'a>(&self, data: &'a [u8])
121    -> Result<Box<dyn std::io::Read + 'a>, ConnectError>;
122
123    /// Decompress the given data with a size limit.
124    ///
125    /// Returns an error if the decompressed data exceeds `max_size`.
126    /// This protects against compression bomb attacks.
127    ///
128    /// The default implementation uses `Read::take(max_size + 1)` on the
129    /// reader from [`decompressor`](Self::decompressor) to structurally
130    /// bound memory — custom providers are safe without any extra work.
131    /// Built-in providers override this for performance.
132    ///
133    /// # Error codes
134    ///
135    /// Malformed or truncated input surfaces as
136    /// [`ConnectError::invalid_argument`] — the client sent a payload that
137    /// cannot be decoded. The default implementation maps read failures to
138    /// this code, matching the built-in gzip and zstd providers; custom
139    /// overrides should follow the same convention.
140    fn decompress_with_limit(&self, data: &[u8], max_size: usize) -> Result<Bytes, ConnectError> {
141        use std::io::Read;
142        let reader = self.decompressor(data)?;
143        let capacity = initial_decompress_capacity(data.len(), 2, Some(max_size));
144        let mut buf = Vec::with_capacity(capacity);
145        reader
146            .take((max_size as u64).saturating_add(1))
147            .read_to_end(&mut buf)
148            .map_err(|e| ConnectError::invalid_argument(format!("decompression failed: {e}")))?;
149        if buf.len() > max_size {
150            return Err(ConnectError::resource_exhausted(format!(
151                "decompressed size exceeds limit {max_size}"
152            )));
153        }
154        Ok(Bytes::from(buf))
155    }
156}
157
158/// Trait for streaming compression support.
159///
160/// This trait extends [`CompressionProvider`] with streaming methods that
161/// process data incrementally without buffering the entire payload in memory.
162///
163/// Available when the `streaming` feature is enabled (default).
164#[cfg(feature = "streaming")]
165#[cfg_attr(docsrs, doc(cfg(feature = "streaming")))]
166pub trait StreamingCompressionProvider: CompressionProvider {
167    /// Create a streaming decompressor.
168    ///
169    /// Returns an `AsyncRead` that decompresses data from the input reader.
170    fn decompress_stream(&self, reader: BoxedAsyncBufRead) -> BoxedAsyncRead;
171
172    /// Create a streaming compressor.
173    ///
174    /// Returns an `AsyncRead` that compresses data from the input reader.
175    fn compress_stream(&self, reader: BoxedAsyncBufRead) -> BoxedAsyncRead;
176}
177
178/// Registry of compression providers.
179///
180/// The registry maps encoding names to their provider implementations.
181/// Use [`CompressionRegistry::default()`] to get a registry with all
182/// feature-enabled built-in providers.
183#[derive(Clone)]
184pub struct CompressionRegistry {
185    providers: Arc<HashMap<&'static str, Arc<dyn CompressionProvider>>>,
186    #[cfg(feature = "streaming")]
187    streaming_providers: Arc<HashMap<&'static str, Arc<dyn StreamingCompressionProvider>>>,
188    /// Cached, sorted, comma-joined list of supported encodings for
189    /// Accept-Encoding headers. Recomputed when providers are registered
190    /// (rather than on every request).
191    accept_encoding: Arc<str>,
192}
193
194impl std::fmt::Debug for CompressionRegistry {
195    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196        f.debug_struct("CompressionRegistry")
197            .field("providers", &self.providers.keys().collect::<Vec<_>>())
198            .finish()
199    }
200}
201
202impl CompressionRegistry {
203    /// Create an empty compression registry.
204    ///
205    /// Use [`register`](Self::register) to add providers, or use
206    /// [`default`](Self::default) to get a registry with built-in providers.
207    pub fn new() -> Self {
208        Self {
209            providers: Arc::new(HashMap::new()),
210            #[cfg(feature = "streaming")]
211            streaming_providers: Arc::new(HashMap::new()),
212            accept_encoding: Arc::from(""),
213        }
214    }
215
216    /// Recompute the cached accept-encoding string from the current provider set.
217    fn rebuild_accept_encoding(&mut self) {
218        let mut encodings: Vec<_> = self.providers.keys().copied().collect();
219        encodings.sort_unstable();
220        self.accept_encoding = Arc::from(encodings.join(", "));
221    }
222
223    /// Register a compression provider.
224    ///
225    /// Returns self for method chaining.
226    ///
227    /// # Example
228    ///
229    /// ```rust
230    /// # #[cfg(all(feature = "gzip", feature = "zstd"))] {
231    /// use connectrpc::compression::{CompressionRegistry, GzipProvider, ZstdProvider};
232    /// let registry = CompressionRegistry::new()
233    ///     .register(GzipProvider::default())
234    ///     .register(ZstdProvider::default());
235    /// assert!(registry.supports("gzip"));
236    /// assert!(registry.supports("zstd"));
237    /// # }
238    /// ```
239    #[must_use]
240    pub fn register<P: CompressionProvider>(mut self, provider: P) -> Self {
241        let providers = Arc::make_mut(&mut self.providers);
242        providers.insert(provider.name(), Arc::new(provider));
243        self.rebuild_accept_encoding();
244        self
245    }
246
247    /// Get a provider by encoding name.
248    ///
249    /// Returns `None` if no provider is registered for the given name.
250    #[must_use]
251    pub fn get(&self, name: &str) -> Option<Arc<dyn CompressionProvider>> {
252        self.providers.get(name).cloned()
253    }
254
255    /// Check if a provider is registered for the given encoding name.
256    pub fn supports(&self, name: &str) -> bool {
257        self.providers.contains_key(name)
258    }
259
260    /// List all supported encoding names.
261    pub fn supported_encodings(&self) -> Vec<&'static str> {
262        self.providers.keys().copied().collect()
263    }
264
265    /// Get a comma-separated string of supported encodings.
266    ///
267    /// Useful for Accept-Encoding headers. The string is computed once when
268    /// providers are registered and cached, so this is a cheap lookup.
269    pub fn accept_encoding_header(&self) -> &str {
270        &self.accept_encoding
271    }
272
273    /// Negotiate a response encoding based on the client's accept-encoding header.
274    ///
275    /// Returns the first encoding from the client's preference list that this
276    /// registry supports, or None if only identity is acceptable.
277    ///
278    /// Per the Connect spec, the accept-encoding header is treated as an ordered
279    /// list with most preferred first (no quality values).
280    ///
281    /// If the client omits accept-encoding, the spec says the server may assume
282    /// the client accepts the same encoding used for the request (if any), plus identity.
283    pub fn negotiate_encoding(
284        &self,
285        accept_encoding: Option<&str>,
286        request_encoding: Option<&str>,
287    ) -> Option<&'static str> {
288        // If client sent Accept-Encoding, use it
289        if let Some(accept) = accept_encoding {
290            for encoding in accept.split(',').map(|s| s.trim()) {
291                if encoding == "identity" {
292                    continue; // identity means no compression
293                }
294                if let Some((key, _)) = self.providers.get_key_value(encoding) {
295                    return Some(*key);
296                }
297            }
298            return None; // Client listed encodings but none supported
299        }
300
301        // Spec: if client omits accept-encoding, assume it accepts the request encoding
302        if let Some(req_enc) = request_encoding
303            && req_enc != "identity"
304            && let Some((key, _)) = self.providers.get_key_value(req_enc)
305        {
306            return Some(*key);
307        }
308
309        None // No compression
310    }
311
312    /// Decompress data using the specified encoding with a size limit.
313    ///
314    /// Returns an error if the encoding is not supported or if the decompressed
315    /// data exceeds `max_size`. This protects against compression bomb attacks.
316    ///
317    /// For identity encoding, returns the data without copying.
318    pub fn decompress_with_limit(
319        &self,
320        encoding: &str,
321        data: Bytes,
322        max_size: usize,
323    ) -> Result<Bytes, ConnectError> {
324        // "identity" means no compression — return data without copying
325        if encoding == "identity" {
326            if data.len() > max_size {
327                return Err(ConnectError::resource_exhausted(format!(
328                    "message size {} exceeds limit {}",
329                    data.len(),
330                    max_size
331                )));
332            }
333            return Ok(data);
334        }
335
336        let provider = self.get(encoding).ok_or_else(|| {
337            ConnectError::unimplemented(format!("unsupported compression encoding: {encoding}"))
338        })?;
339
340        // Per the Connect spec: "Servers must not attempt to decompress
341        // zero-length HTTP request content" (and the symmetric rule for
342        // clients). A zero-length body with Content-Encoding set is valid —
343        // clients may skip compressing empty payloads but still advertise
344        // the encoding. Return empty without invoking the decoder, which
345        // may reject empty input as an incomplete frame (zstd does).
346        // Checked AFTER resolving the encoding so unsupported encodings
347        // still error (conformance "unexpected-compression" test).
348        if data.is_empty() {
349            return Ok(data);
350        }
351
352        provider.decompress_with_limit(&data, max_size)
353    }
354
355    /// Compress data using the specified encoding.
356    ///
357    /// Returns an error if the encoding is not supported.
358    pub fn compress(&self, encoding: &str, data: &[u8]) -> Result<Bytes, ConnectError> {
359        // "identity" means no compression
360        if encoding == "identity" {
361            return Ok(Bytes::copy_from_slice(data));
362        }
363
364        let provider = self.get(encoding).ok_or_else(|| {
365            ConnectError::unimplemented(format!("unsupported compression encoding: {encoding}"))
366        })?;
367
368        provider.compress(data)
369    }
370
371    /// Register a streaming compression provider.
372    ///
373    /// This also registers the provider for buffered compression.
374    /// Returns self for method chaining.
375    ///
376    /// Available when the `streaming` feature is enabled.
377    #[cfg(feature = "streaming")]
378    #[cfg_attr(docsrs, doc(cfg(feature = "streaming")))]
379    #[must_use]
380    pub fn register_streaming<P: StreamingCompressionProvider>(mut self, provider: P) -> Self {
381        let name = provider.name();
382        let provider = Arc::new(provider);
383
384        // Register for both buffered and streaming
385        let providers = Arc::make_mut(&mut self.providers);
386        providers.insert(name, provider.clone());
387
388        let streaming_providers = Arc::make_mut(&mut self.streaming_providers);
389        streaming_providers.insert(name, provider);
390
391        self.rebuild_accept_encoding();
392        self
393    }
394
395    /// Get a streaming provider by encoding name.
396    ///
397    /// Returns `None` if no streaming provider is registered for the given name.
398    #[cfg(feature = "streaming")]
399    #[cfg_attr(docsrs, doc(cfg(feature = "streaming")))]
400    pub fn get_streaming(&self, name: &str) -> Option<Arc<dyn StreamingCompressionProvider>> {
401        self.streaming_providers.get(name).cloned()
402    }
403
404    /// Check if streaming compression is supported for the given encoding name.
405    #[cfg(feature = "streaming")]
406    #[cfg_attr(docsrs, doc(cfg(feature = "streaming")))]
407    pub fn supports_streaming(&self, name: &str) -> bool {
408        self.streaming_providers.contains_key(name)
409    }
410
411    /// Create a streaming decompressor for the specified encoding.
412    ///
413    /// Returns an `AsyncRead` that decompresses data from the input reader.
414    /// Returns an error if the encoding is not supported for streaming.
415    #[cfg(feature = "streaming")]
416    #[cfg_attr(docsrs, doc(cfg(feature = "streaming")))]
417    pub fn decompress_stream(
418        &self,
419        encoding: &str,
420        reader: BoxedAsyncBufRead,
421    ) -> Result<BoxedAsyncRead, ConnectError> {
422        // "identity" means no compression - just return the reader as-is
423        if encoding == "identity" {
424            return Ok(reader);
425        }
426
427        let provider = self.get_streaming(encoding).ok_or_else(|| {
428            ConnectError::unimplemented(format!(
429                "streaming decompression not supported for encoding: {encoding}"
430            ))
431        })?;
432
433        Ok(provider.decompress_stream(reader))
434    }
435
436    /// Create a streaming compressor for the specified encoding.
437    ///
438    /// Returns an `AsyncRead` that compresses data from the input reader.
439    /// Returns an error if the encoding is not supported for streaming.
440    #[cfg(feature = "streaming")]
441    #[cfg_attr(docsrs, doc(cfg(feature = "streaming")))]
442    pub fn compress_stream(
443        &self,
444        encoding: &str,
445        reader: BoxedAsyncBufRead,
446    ) -> Result<BoxedAsyncRead, ConnectError> {
447        // "identity" means no compression - just return the reader as-is
448        if encoding == "identity" {
449            return Ok(reader);
450        }
451
452        let provider = self.get_streaming(encoding).ok_or_else(|| {
453            ConnectError::unimplemented(format!(
454                "streaming compression not supported for encoding: {encoding}"
455            ))
456        })?;
457
458        Ok(provider.compress_stream(reader))
459    }
460}
461
462/// Policy controlling when compression is applied.
463///
464/// By default, messages below 1 KiB are not compressed — at that size,
465/// compression overhead (headers, Huffman tables, checksums) typically
466/// exceeds the space savings, and the CPU cost of initializing the
467/// compressor dominates.
468///
469/// # Example
470///
471/// ```rust
472/// use connectrpc::CompressionPolicy;
473///
474/// // Only compress messages >= 4 KiB
475/// let policy = CompressionPolicy::default().min_size(4096);
476/// assert!(!policy.should_compress(1024));
477/// assert!(policy.should_compress(8192));
478///
479/// // Disable compression entirely
480/// let policy = CompressionPolicy::disabled();
481/// assert!(!policy.should_compress(1_000_000));
482/// ```
483#[derive(Debug, Clone, Copy)]
484pub struct CompressionPolicy {
485    /// Whether compression is enabled at all.
486    enabled: bool,
487    /// Minimum message size in bytes before compression is applied.
488    /// Messages smaller than this are sent uncompressed.
489    min_size: usize,
490}
491
492/// Default minimum message size for compression (1 KiB).
493///
494/// Below this threshold, compression typically adds overhead without
495/// meaningful size reduction. This matches common defaults in HTTP
496/// servers and gRPC implementations (gRPC-Java uses 1 KiB).
497pub const DEFAULT_COMPRESSION_MIN_SIZE: usize = 1024;
498
499impl Default for CompressionPolicy {
500    fn default() -> Self {
501        Self {
502            enabled: true,
503            min_size: DEFAULT_COMPRESSION_MIN_SIZE,
504        }
505    }
506}
507
508impl CompressionPolicy {
509    /// Create a policy that disables compression entirely.
510    pub fn disabled() -> Self {
511        Self {
512            enabled: false,
513            min_size: 0,
514        }
515    }
516
517    /// Set the minimum message size for compression.
518    ///
519    /// Messages smaller than this (in bytes, before compression) will
520    /// be sent uncompressed even if compression is negotiated.
521    #[must_use]
522    pub fn min_size(mut self, size: usize) -> Self {
523        self.min_size = size;
524        self
525    }
526
527    /// Check whether compression should be applied for a message of the given size.
528    ///
529    /// Zero-length bodies are compressed when `min_size == 0` (useful for
530    /// conformance testing where the runner checks that advertised encodings
531    /// are used even for empty payloads). The Connect spec requires receivers
532    /// to skip decompression for zero-length content, so this is safe.
533    #[inline]
534    pub fn should_compress(&self, message_size: usize) -> bool {
535        self.enabled && message_size >= self.min_size
536    }
537
538    /// Return an effective policy that accounts for a per-call override.
539    ///
540    /// - `None` → use this policy as-is.
541    /// - `Some(true)` → force compression (min_size = 0, enabled = true).
542    /// - `Some(false)` → disable compression.
543    pub(crate) fn with_override(&self, override_compress: Option<bool>) -> Self {
544        match override_compress {
545            None => *self,
546            Some(true) => Self {
547                enabled: true,
548                min_size: 0,
549            },
550            Some(false) => Self::disabled(),
551        }
552    }
553}
554
555impl Default for CompressionRegistry {
556    /// Create a registry with all feature-enabled built-in providers.
557    #[allow(unused_mut)]
558    fn default() -> Self {
559        let mut registry = Self::new();
560
561        // When streaming is enabled, use register_streaming to get both capabilities
562        #[cfg(all(feature = "gzip", feature = "streaming"))]
563        {
564            registry = registry.register_streaming(GzipProvider::default());
565        }
566
567        #[cfg(all(feature = "gzip", not(feature = "streaming")))]
568        {
569            registry = registry.register(GzipProvider::default());
570        }
571
572        #[cfg(all(feature = "zstd", feature = "streaming"))]
573        {
574            registry = registry.register_streaming(ZstdProvider::default());
575        }
576
577        #[cfg(all(feature = "zstd", not(feature = "streaming")))]
578        {
579            registry = registry.register(ZstdProvider::default());
580        }
581
582        registry
583    }
584}
585
586// ============================================================================
587// Built-in Providers
588// ============================================================================
589
590/// Gzip compression provider with internal state pooling.
591///
592/// Pools `flate2::Compress` and `flate2::Decompress` objects to avoid the
593/// ~200 KB allocation overhead per request that comes from creating fresh
594/// gzip state tables. The pool is shared across all clones of the
595/// `CompressionRegistry` that holds this provider (via `Arc`).
596///
597/// # Defaults
598///
599/// The default compression level is **1** (fastest). RPC payloads are
600/// latency-sensitive and short-lived; level 1 typically captures most of
601/// the size reduction at a fraction of the CPU cost of level 6. Use
602/// [`GzipProvider::with_level`] for a different speed/ratio trade-off, or
603/// prefer `ZstdProvider` when the peer supports it — zstd at its default
604/// level is typically both faster and smaller than gzip on RPC payloads.
605///
606/// This crate enables `flate2`'s `zlib-rs` backend (a pure-Rust port of
607/// zlib-ng), which is substantially faster than the `miniz_oxide` default.
608/// Because Cargo features are additive, this selection also applies to any
609/// other `flate2` use in the same dependency graph.
610///
611/// Available when the `gzip` feature is enabled (default).
612#[cfg(feature = "gzip")]
613#[cfg_attr(docsrs, doc(cfg(feature = "gzip")))]
614pub struct GzipProvider {
615    /// Compression level (0-9, default is 1).
616    level: u32,
617    compressors: std::sync::Mutex<Vec<flate2::Compress>>,
618    decompressors: std::sync::Mutex<Vec<flate2::Decompress>>,
619}
620
621#[cfg(feature = "gzip")]
622#[cfg_attr(docsrs, doc(cfg(feature = "gzip")))]
623impl std::fmt::Debug for GzipProvider {
624    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
625        f.debug_struct("GzipProvider")
626            .field("level", &self.level)
627            .field(
628                "pool_compressors",
629                &self.compressors.lock().map(|v| v.len()).unwrap_or(0),
630            )
631            .field(
632                "pool_decompressors",
633                &self.decompressors.lock().map(|v| v.len()).unwrap_or(0),
634            )
635            .finish()
636    }
637}
638
639#[cfg(feature = "gzip")]
640#[cfg_attr(docsrs, doc(cfg(feature = "gzip")))]
641impl Default for GzipProvider {
642    fn default() -> Self {
643        Self::with_level(Self::DEFAULT_LEVEL)
644    }
645}
646
647#[cfg(feature = "gzip")]
648#[cfg_attr(docsrs, doc(cfg(feature = "gzip")))]
649impl GzipProvider {
650    /// Default compression level: 1 (fastest).
651    ///
652    /// See the [type-level docs](GzipProvider#defaults) for rationale.
653    pub const DEFAULT_LEVEL: u32 = 1;
654
655    /// Create a new Gzip provider with the default compression level (1).
656    pub fn new() -> Self {
657        Self::default()
658    }
659
660    /// Create a new Gzip provider with the specified compression level.
661    ///
662    /// Level should be 0-9, where 0 is no compression and 9 is maximum.
663    /// The default is 1 (fastest); use 6 for the conventional zlib default
664    /// trade-off, or 9 for maximum compression.
665    ///
666    /// # Panics
667    ///
668    /// `flate2` panics at compress time if `level > 9`.
669    pub fn with_level(level: u32) -> Self {
670        debug_assert!(level <= 9, "gzip level must be 0-9, got {level}");
671        Self {
672            level,
673            compressors: std::sync::Mutex::new(Vec::new()),
674            decompressors: std::sync::Mutex::new(Vec::new()),
675        }
676    }
677
678    /// Maximum number of compressor/decompressor instances to retain in
679    /// the pool. Excess instances are dropped to bound memory usage.
680    const MAX_POOL_SIZE: usize = 64;
681
682    fn take_compressor(&self) -> flate2::Compress {
683        self.compressors
684            .lock()
685            .unwrap_or_else(|e| e.into_inner())
686            .pop()
687            .unwrap_or_else(|| flate2::Compress::new(flate2::Compression::new(self.level), false))
688    }
689
690    fn return_compressor(&self, mut c: flate2::Compress) {
691        c.reset();
692        let mut pool = self.compressors.lock().unwrap_or_else(|e| e.into_inner());
693        if pool.len() < Self::MAX_POOL_SIZE {
694            pool.push(c);
695        }
696    }
697
698    fn take_decompressor(&self) -> flate2::Decompress {
699        self.decompressors
700            .lock()
701            .unwrap_or_else(|e| e.into_inner())
702            .pop()
703            .unwrap_or_else(|| flate2::Decompress::new(false))
704    }
705
706    fn return_decompressor(&self, mut d: flate2::Decompress) {
707        d.reset(false);
708        let mut pool = self.decompressors.lock().unwrap_or_else(|e| e.into_inner());
709        if pool.len() < Self::MAX_POOL_SIZE {
710            pool.push(d);
711        }
712    }
713
714    fn compress_inner(
715        compressor: &mut flate2::Compress,
716        data: &[u8],
717    ) -> Result<Bytes, ConnectError> {
718        let mut output = Vec::with_capacity(data.len() + 32);
719
720        // Gzip header (RFC 1952): fixed 10 bytes, no optional fields
721        output.extend_from_slice(&[
722            0x1f, 0x8b, // magic
723            0x08, // method = deflate
724            0x00, // flags = none
725            0x00, 0x00, 0x00, 0x00, // mtime = 0
726            0x00, // extra flags
727            0xff, // OS = unknown
728        ]);
729
730        // Deflate-compress the data
731        let start_in = compressor.total_in();
732        loop {
733            let consumed = (compressor.total_in() - start_in) as usize;
734            output.reserve(output.capacity().max(4096));
735            let status = compressor
736                .compress_vec(
737                    &data[consumed..],
738                    &mut output,
739                    flate2::FlushCompress::Finish,
740                )
741                .map_err(|e| ConnectError::internal(format!("gzip compression failed: {e}")))?;
742            if status == flate2::Status::StreamEnd {
743                break;
744            }
745        }
746
747        // Gzip trailer: CRC32 + ISIZE (original size mod 2^32)
748        let mut crc = flate2::Crc::new();
749        crc.update(data);
750        output.extend_from_slice(&crc.sum().to_le_bytes());
751        output.extend_from_slice(&(data.len() as u32).to_le_bytes());
752
753        Ok(Bytes::from(output))
754    }
755
756    fn decompress_inner(
757        decompressor: &mut flate2::Decompress,
758        data: &[u8],
759        max_size: Option<usize>,
760    ) -> Result<Bytes, ConnectError> {
761        let deflate_start = gzip_header_len(data)?;
762        let stream_data = &data[deflate_start..];
763
764        let mut output = Vec::with_capacity(initial_decompress_capacity(data.len(), 2, max_size));
765
766        // Decompress the deflate stream, letting the decompressor find its
767        // own end-of-stream marker rather than pre-slicing.
768        let start_in = decompressor.total_in();
769        loop {
770            let consumed = (decompressor.total_in() - start_in) as usize;
771            if output.capacity() == output.len() {
772                if let Some(limit) = max_size
773                    && output.len() > limit
774                {
775                    return Err(ConnectError::resource_exhausted(format!(
776                        "decompressed size exceeds limit {limit}"
777                    )));
778                }
779                // Grow on demand, but never reserve past `limit + 1`: once the
780                // buffer fills at that point the over-limit check above fires,
781                // so the peak allocation for an over-limit payload stays the
782                // same as it was with a limit-sized pre-allocation.
783                let mut additional = output.len().max(4096);
784                if let Some(limit) = max_size {
785                    additional =
786                        additional.min(limit.saturating_add(1).saturating_sub(output.capacity()));
787                }
788                output.reserve_exact(additional);
789            }
790            let status = decompressor
791                .decompress_vec(
792                    &stream_data[consumed..],
793                    &mut output,
794                    flate2::FlushDecompress::None,
795                )
796                .map_err(|e| {
797                    malformed_compressed_payload(format!("gzip decompression failed: {e}"))
798                })?;
799            match status {
800                flate2::Status::StreamEnd => break,
801                flate2::Status::Ok => {}
802                // Output capacity is always available at this point (ensured
803                // above), so `BufError` means the decompressor cannot make
804                // progress with the remaining input: the deflate stream ended
805                // without an end-of-stream marker. Without this check the
806                // loop would never terminate on such input.
807                flate2::Status::BufError => {
808                    return Err(malformed_compressed_payload(
809                        "gzip decompression stalled: truncated or invalid deflate stream",
810                    ));
811                }
812            }
813        }
814
815        if let Some(limit) = max_size
816            && output.len() > limit
817        {
818            return Err(ConnectError::resource_exhausted(format!(
819                "decompressed size exceeds limit {limit}"
820            )));
821        }
822
823        // The 8-byte trailer (CRC32 + ISIZE) follows the deflate stream.
824        let deflate_consumed = (decompressor.total_in() - start_in) as usize;
825        let trailer_start = deflate_consumed;
826        if stream_data.len() < trailer_start + 8 {
827            return Err(malformed_compressed_payload(
828                "gzip data too short for trailer",
829            ));
830        }
831        let trailer = &stream_data[trailer_start..trailer_start + 8];
832
833        let expected_crc = u32::from_le_bytes([trailer[0], trailer[1], trailer[2], trailer[3]]);
834        let expected_size = u32::from_le_bytes([trailer[4], trailer[5], trailer[6], trailer[7]]);
835
836        let mut crc = flate2::Crc::new();
837        crc.update(&output);
838        if crc.sum() != expected_crc {
839            return Err(malformed_compressed_payload("gzip CRC32 mismatch"));
840        }
841        if expected_size != (output.len() as u32) {
842            return Err(malformed_compressed_payload("gzip size mismatch"));
843        }
844
845        Ok(Bytes::from(output))
846    }
847}
848
849/// Parse a gzip header (RFC 1952) and return the byte offset where the
850/// deflate stream begins.
851#[cfg(feature = "gzip")]
852fn gzip_header_len(data: &[u8]) -> Result<usize, ConnectError> {
853    if data.len() < 10 {
854        return Err(malformed_compressed_payload(
855            "gzip data too short for header",
856        ));
857    }
858    if data[0] != 0x1f || data[1] != 0x8b {
859        return Err(malformed_compressed_payload("invalid gzip magic"));
860    }
861    if data[2] != 0x08 {
862        return Err(malformed_compressed_payload(
863            "unsupported gzip compression method",
864        ));
865    }
866    let flags = data[3];
867    let mut pos = 10;
868
869    // FEXTRA
870    if flags & 0x04 != 0 {
871        if pos + 2 > data.len() {
872            return Err(malformed_compressed_payload("truncated gzip header"));
873        }
874        let xlen = u16::from_le_bytes([data[pos], data[pos + 1]]) as usize;
875        pos += 2 + xlen;
876    }
877
878    // FNAME (null-terminated)
879    if flags & 0x08 != 0 {
880        while pos < data.len() && data[pos] != 0 {
881            pos += 1;
882        }
883        if pos >= data.len() {
884            return Err(malformed_compressed_payload("truncated gzip header"));
885        }
886        pos += 1; // skip null terminator
887    }
888
889    // FCOMMENT (null-terminated)
890    if flags & 0x10 != 0 {
891        while pos < data.len() && data[pos] != 0 {
892            pos += 1;
893        }
894        if pos >= data.len() {
895            return Err(malformed_compressed_payload("truncated gzip header"));
896        }
897        pos += 1; // skip null terminator
898    }
899
900    // FHCRC
901    if flags & 0x02 != 0 {
902        pos += 2;
903    }
904
905    if pos > data.len() {
906        return Err(malformed_compressed_payload("truncated gzip header"));
907    }
908    Ok(pos)
909}
910
911#[cfg(feature = "gzip")]
912#[cfg_attr(docsrs, doc(cfg(feature = "gzip")))]
913impl CompressionProvider for GzipProvider {
914    fn name(&self) -> &'static str {
915        "gzip"
916    }
917
918    fn compress(&self, data: &[u8]) -> Result<Bytes, ConnectError> {
919        let mut compressor = self.take_compressor();
920        let result = Self::compress_inner(&mut compressor, data);
921        self.return_compressor(compressor);
922        result
923    }
924
925    fn decompressor<'a>(
926        &self,
927        data: &'a [u8],
928    ) -> Result<Box<dyn std::io::Read + 'a>, ConnectError> {
929        Ok(Box::new(flate2::read::GzDecoder::new(data)))
930    }
931
932    fn decompress_with_limit(&self, data: &[u8], max_size: usize) -> Result<Bytes, ConnectError> {
933        let mut decompressor = self.take_decompressor();
934        let result = Self::decompress_inner(&mut decompressor, data, Some(max_size));
935        self.return_decompressor(decompressor);
936        result
937    }
938}
939
940#[cfg(all(feature = "gzip", feature = "streaming"))]
941#[cfg_attr(docsrs, doc(cfg(all(feature = "gzip", feature = "streaming"))))]
942impl StreamingCompressionProvider for GzipProvider {
943    fn decompress_stream(&self, reader: BoxedAsyncBufRead) -> BoxedAsyncRead {
944        Box::pin(async_compression::tokio::bufread::GzipDecoder::new(reader))
945    }
946
947    fn compress_stream(&self, reader: BoxedAsyncBufRead) -> BoxedAsyncRead {
948        Box::pin(
949            async_compression::tokio::bufread::GzipEncoder::with_quality(
950                reader,
951                async_compression::Level::Precise(self.level as i32),
952            ),
953        )
954    }
955}
956
957/// Zstandard compression provider with internal compressor pooling.
958///
959/// Pools `zstd::bulk::Compressor` objects to avoid repeated allocation of
960/// zstd compression contexts. Decompression uses the streaming decoder
961/// (`zstd::Decoder`) which handles arbitrary compression ratios without
962/// guessing output buffer sizes.
963///
964/// Available when the `zstd` feature is enabled (default).
965#[cfg(feature = "zstd")]
966#[cfg_attr(docsrs, doc(cfg(feature = "zstd")))]
967pub struct ZstdProvider {
968    /// Compression level (1-22, default is 3).
969    level: i32,
970    compressors: std::sync::Mutex<Vec<zstd::bulk::Compressor<'static>>>,
971}
972
973#[cfg(feature = "zstd")]
974#[cfg_attr(docsrs, doc(cfg(feature = "zstd")))]
975impl std::fmt::Debug for ZstdProvider {
976    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
977        f.debug_struct("ZstdProvider")
978            .field("level", &self.level)
979            .field(
980                "pool_compressors",
981                &self.compressors.lock().map(|v| v.len()).unwrap_or(0),
982            )
983            .finish()
984    }
985}
986
987#[cfg(feature = "zstd")]
988#[cfg_attr(docsrs, doc(cfg(feature = "zstd")))]
989impl ZstdProvider {
990    /// Default compression level: 3 (the zstd library default).
991    pub const DEFAULT_LEVEL: i32 = 3;
992
993    /// Create a new Zstd provider with default compression level.
994    pub fn new() -> Self {
995        Self::default()
996    }
997
998    /// Create a new Zstd provider with the specified compression level.
999    ///
1000    /// Level should be 1-22, where higher values give better compression
1001    /// but are slower.
1002    pub fn with_level(level: i32) -> Self {
1003        Self {
1004            level,
1005            compressors: std::sync::Mutex::new(Vec::new()),
1006        }
1007    }
1008}
1009
1010#[cfg(feature = "zstd")]
1011#[cfg_attr(docsrs, doc(cfg(feature = "zstd")))]
1012impl Default for ZstdProvider {
1013    fn default() -> Self {
1014        Self {
1015            level: Self::DEFAULT_LEVEL,
1016            compressors: std::sync::Mutex::new(Vec::new()),
1017        }
1018    }
1019}
1020
1021#[cfg(feature = "zstd")]
1022impl ZstdProvider {
1023    /// Maximum number of compressor instances to retain in the pool.
1024    const MAX_POOL_SIZE: usize = 64;
1025
1026    fn take_compressor(&self) -> Result<zstd::bulk::Compressor<'static>, ConnectError> {
1027        if let Some(c) = self
1028            .compressors
1029            .lock()
1030            .unwrap_or_else(|e| e.into_inner())
1031            .pop()
1032        {
1033            return Ok(c);
1034        }
1035        zstd::bulk::Compressor::new(self.level)
1036            .map_err(|e| ConnectError::internal(format!("failed to create zstd compressor: {e}")))
1037    }
1038
1039    fn return_compressor(&self, c: zstd::bulk::Compressor<'static>) {
1040        let mut pool = self.compressors.lock().unwrap_or_else(|e| e.into_inner());
1041        if pool.len() < Self::MAX_POOL_SIZE {
1042            pool.push(c);
1043        }
1044    }
1045
1046    /// Decompress zstd data with an optional output-size cap.
1047    ///
1048    /// Uses the streaming decoder rather than `bulk::Decompressor` because
1049    /// the bulk API requires guessing an output buffer size up front (and
1050    /// fails if the guess is too small). The streaming decoder handles any
1051    /// compression ratio and allows precise `Read::take()` bounding.
1052    fn decompress_impl(data: &[u8], max_size: Option<usize>) -> Result<Bytes, ConnectError> {
1053        use std::io::Read;
1054
1055        let mut decoder = zstd::Decoder::new(data)
1056            .map_err(|e| malformed_compressed_payload(format!("zstd decompression failed: {e}")))?;
1057
1058        let mut decompressed =
1059            Vec::with_capacity(initial_decompress_capacity(data.len(), 4, max_size));
1060
1061        match max_size {
1062            Some(limit) => {
1063                // Read at most limit+1 so we can detect overflow without
1064                // allocating the entire stream.
1065                decoder
1066                    .take((limit as u64).saturating_add(1))
1067                    .read_to_end(&mut decompressed)
1068                    .map_err(|e| {
1069                        malformed_compressed_payload(format!("zstd decompression failed: {e}"))
1070                    })?;
1071                if decompressed.len() > limit {
1072                    return Err(ConnectError::resource_exhausted(format!(
1073                        "decompressed size exceeds limit {limit}"
1074                    )));
1075                }
1076            }
1077            None => {
1078                decoder.read_to_end(&mut decompressed).map_err(|e| {
1079                    malformed_compressed_payload(format!("zstd decompression failed: {e}"))
1080                })?;
1081            }
1082        }
1083        Ok(Bytes::from(decompressed))
1084    }
1085}
1086
1087#[cfg(feature = "zstd")]
1088#[cfg_attr(docsrs, doc(cfg(feature = "zstd")))]
1089impl CompressionProvider for ZstdProvider {
1090    fn name(&self) -> &'static str {
1091        "zstd"
1092    }
1093
1094    fn compress(&self, data: &[u8]) -> Result<Bytes, ConnectError> {
1095        let mut compressor = self.take_compressor()?;
1096        let result = compressor
1097            .compress(data)
1098            .map(Bytes::from)
1099            .map_err(|e| ConnectError::internal(format!("zstd compression failed: {e}")));
1100        self.return_compressor(compressor);
1101        result
1102    }
1103
1104    fn decompressor<'a>(
1105        &self,
1106        data: &'a [u8],
1107    ) -> Result<Box<dyn std::io::Read + 'a>, ConnectError> {
1108        let decoder = zstd::Decoder::new(data)
1109            .map_err(|e| malformed_compressed_payload(format!("zstd decompression failed: {e}")))?;
1110        Ok(Box::new(decoder))
1111    }
1112
1113    fn decompress_with_limit(&self, data: &[u8], max_size: usize) -> Result<Bytes, ConnectError> {
1114        Self::decompress_impl(data, Some(max_size))
1115    }
1116}
1117
1118#[cfg(all(feature = "zstd", feature = "streaming"))]
1119#[cfg_attr(docsrs, doc(cfg(all(feature = "zstd", feature = "streaming"))))]
1120impl StreamingCompressionProvider for ZstdProvider {
1121    fn decompress_stream(&self, reader: BoxedAsyncBufRead) -> BoxedAsyncRead {
1122        Box::pin(async_compression::tokio::bufread::ZstdDecoder::new(reader))
1123    }
1124
1125    fn compress_stream(&self, reader: BoxedAsyncBufRead) -> BoxedAsyncRead {
1126        Box::pin(
1127            async_compression::tokio::bufread::ZstdEncoder::with_quality(
1128                reader,
1129                async_compression::Level::Precise(self.level),
1130            ),
1131        )
1132    }
1133}
1134
1135// ============================================================================
1136// Tests
1137// ============================================================================
1138
1139/// Initial output-buffer capacity for buffered decompression.
1140///
1141/// The output buffer becomes the backing allocation of the returned `Bytes`,
1142/// so it is sized from the compressed input rather than from the configured
1143/// limit — a limit-sized allocation would stay resident for the lifetime of
1144/// every (possibly tiny) message. The guess is `input_len × multiplier`
1145/// (gzip and the trait default use 2; zstd uses 4 because it typically
1146/// achieves higher ratios on RPC payloads), with a 256-byte floor, capped at
1147/// `limit + 1` so the initial allocation never exceeds what the limit allows.
1148///
1149/// Callers grow the buffer on demand and enforce the limit as it grows; the
1150/// `read_to_end`-based callers may transiently reserve up to roughly twice
1151/// the bytes actually written (amortized growth), still bounded by their
1152/// `Read::take(limit + 1)` readers.
1153fn initial_decompress_capacity(
1154    input_len: usize,
1155    multiplier: usize,
1156    max_size: Option<usize>,
1157) -> usize {
1158    let mut capacity = input_len.saturating_mul(multiplier).max(256);
1159    if let Some(limit) = max_size {
1160        capacity = capacity.min(limit.saturating_add(1));
1161    }
1162    capacity
1163}
1164
1165#[cfg(test)]
1166mod tests {
1167    use super::*;
1168
1169    #[cfg(any(feature = "gzip", feature = "zstd"))]
1170    fn assert_invalid_argument(err: &ConnectError) {
1171        assert_eq!(
1172            err.code,
1173            crate::error::ErrorCode::InvalidArgument,
1174            "{err:?}"
1175        );
1176    }
1177
1178    #[test]
1179    fn test_empty_registry() {
1180        let registry = CompressionRegistry::new();
1181        assert!(!registry.supports("gzip"));
1182        assert!(!registry.supports("zstd"));
1183        assert!(registry.supported_encodings().is_empty());
1184    }
1185
1186    #[test]
1187    fn test_identity_always_works() {
1188        let registry = CompressionRegistry::new();
1189        let data = b"hello world";
1190        let result = registry
1191            .decompress_with_limit("identity", Bytes::from_static(data), usize::MAX)
1192            .unwrap();
1193        assert_eq!(&result[..], data);
1194    }
1195
1196    #[cfg(feature = "gzip")]
1197    #[test]
1198    fn test_gzip_large_roundtrip() {
1199        let provider = GzipProvider::default();
1200        let data: Vec<u8> = (0..1_000_000).map(|i| (i % 256) as u8).collect();
1201        let compressed = provider.compress(&data).unwrap();
1202        let decompressed = provider
1203            .decompress_with_limit(&compressed, usize::MAX)
1204            .unwrap();
1205        assert_eq!(&decompressed[..], &data[..]);
1206    }
1207
1208    #[cfg(feature = "gzip")]
1209    #[test]
1210    fn test_gzip_pooled_cross_compat_with_gz_decoder() {
1211        use std::io::Read;
1212        let provider = GzipProvider::default();
1213        let data: Vec<u8> = (0..100_000).map(|i| (i % 256) as u8).collect();
1214        let compressed = provider.compress(&data).unwrap();
1215        // Verify standard GzDecoder can read our output
1216        let mut decoder = flate2::read::GzDecoder::new(&compressed[..]);
1217        let mut decompressed = Vec::new();
1218        decoder.read_to_end(&mut decompressed).unwrap();
1219        assert_eq!(&decompressed[..], &data[..]);
1220    }
1221
1222    #[cfg(feature = "gzip")]
1223    #[test]
1224    fn test_gzip_pooled_cross_compat_with_gz_encoder() {
1225        use std::io::Write;
1226        let provider = GzipProvider::default();
1227        let data: Vec<u8> = (0..100_000).map(|i| (i % 256) as u8).collect();
1228        // Compress with standard GzEncoder
1229        let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::new(6));
1230        encoder.write_all(&data).unwrap();
1231        let compressed = encoder.finish().unwrap();
1232        // Decompress with our pooled provider
1233        let decompressed = provider
1234            .decompress_with_limit(&compressed, usize::MAX)
1235            .unwrap();
1236        assert_eq!(&decompressed[..], &data[..]);
1237    }
1238
1239    #[cfg(feature = "gzip")]
1240    #[test]
1241    fn test_gzip_default_level_is_fast() {
1242        assert_eq!(GzipProvider::DEFAULT_LEVEL, 1);
1243        // Round-trip at the default (fastest) level.
1244        let provider = GzipProvider::default();
1245        let data = vec![b'x'; 50_000];
1246        let compressed = provider.compress(&data).unwrap();
1247        assert!(compressed.len() < data.len());
1248        let decompressed = provider
1249            .decompress_with_limit(&compressed, usize::MAX)
1250            .unwrap();
1251        assert_eq!(&decompressed[..], &data[..]);
1252    }
1253
1254    #[cfg(feature = "gzip")]
1255    #[test]
1256    fn test_gzip_cross_level_decode() {
1257        // Output from any level must decode with any provider instance
1258        // (level only affects encode); also exercises pool reuse across
1259        // two compress calls on the level-6 provider.
1260        let fast = GzipProvider::default();
1261        let slow = GzipProvider::with_level(6);
1262        let data: Vec<u8> = (0..32_768).map(|i| (i % 251) as u8).collect();
1263        for src in [&fast, &slow] {
1264            let _ = src.compress(&data).unwrap();
1265            let compressed = src.compress(&data).unwrap();
1266            for dst in [&fast, &slow] {
1267                let out = dst.decompress_with_limit(&compressed, usize::MAX).unwrap();
1268                assert_eq!(&out[..], &data[..]);
1269            }
1270        }
1271    }
1272
1273    #[cfg(all(feature = "gzip", feature = "streaming"))]
1274    #[tokio::test]
1275    async fn test_gzip_streaming_honors_level() {
1276        use tokio::io::AsyncReadExt;
1277        async fn stream_compress(p: &GzipProvider, data: &[u8]) -> Vec<u8> {
1278            let reader: BoxedAsyncBufRead = Box::pin(std::io::Cursor::new(data.to_vec()));
1279            let mut enc = p.compress_stream(reader);
1280            let mut out = Vec::new();
1281            enc.read_to_end(&mut out).await.unwrap();
1282            out
1283        }
1284        let data = b"hello world, streaming gzip at the configured level".repeat(200);
1285        let fast = stream_compress(&GzipProvider::with_level(1), &data).await;
1286        let best = stream_compress(&GzipProvider::with_level(9), &data).await;
1287        // Both must round-trip via the buffered decoder.
1288        for c in [&fast, &best] {
1289            let out = GzipProvider::default()
1290                .decompress_with_limit(c, usize::MAX)
1291                .unwrap();
1292            assert_eq!(&out[..], &data[..]);
1293        }
1294        // Level must actually affect output (previously ignored): level 9 on
1295        // highly repetitive input compresses strictly smaller than level 1.
1296        assert!(
1297            best.len() < fast.len(),
1298            "level 9 ({}) should be smaller than level 1 ({})",
1299            best.len(),
1300            fast.len()
1301        );
1302    }
1303
1304    // ── gzip_header_len tests (RFC 1952 flag parsing) ────────────────
1305
1306    #[cfg(feature = "gzip")]
1307    /// Build a gzip header with the given flag byte and optional extra fields.
1308    /// Returns the header bytes. Does NOT append deflate stream or trailer.
1309    fn gz_hdr(flags: u8, extra: &[u8]) -> Vec<u8> {
1310        let mut h = vec![
1311            0x1f, 0x8b, // magic
1312            0x08, // method = deflate
1313            flags, 0, 0, 0, 0,    // mtime
1314            0,    // XFL
1315            0xff, // OS = unknown
1316        ];
1317        h.extend_from_slice(extra);
1318        h
1319    }
1320
1321    #[cfg(feature = "gzip")]
1322    #[test]
1323    fn test_gzip_header_len_basic() {
1324        // No flags → 10-byte fixed header
1325        assert_eq!(gzip_header_len(&gz_hdr(0x00, &[])).unwrap(), 10);
1326    }
1327
1328    #[cfg(feature = "gzip")]
1329    #[test]
1330    fn test_gzip_header_len_fextra() {
1331        // FEXTRA (0x04): 2-byte LE length + that many bytes
1332        let extra = [3u8, 0, 0xAA, 0xBB, 0xCC]; // xlen=3, then 3 bytes
1333        assert_eq!(gzip_header_len(&gz_hdr(0x04, &extra)).unwrap(), 10 + 2 + 3);
1334    }
1335
1336    #[cfg(feature = "gzip")]
1337    #[test]
1338    fn test_gzip_header_len_fextra_truncated() {
1339        // FEXTRA declares xlen=100 but only 2 bytes follow
1340        let extra = [100u8, 0, 0xAA, 0xBB];
1341        assert!(gzip_header_len(&gz_hdr(0x04, &extra)).is_err());
1342    }
1343
1344    #[cfg(feature = "gzip")]
1345    #[test]
1346    fn test_gzip_header_len_fname() {
1347        // FNAME (0x08): null-terminated string
1348        let extra = b"test.txt\0";
1349        assert_eq!(gzip_header_len(&gz_hdr(0x08, extra)).unwrap(), 10 + 9);
1350    }
1351
1352    #[cfg(feature = "gzip")]
1353    #[test]
1354    fn test_gzip_header_len_fname_truncated() {
1355        // FNAME with no null terminator
1356        assert!(gzip_header_len(&gz_hdr(0x08, b"nonul")).is_err());
1357    }
1358
1359    #[cfg(feature = "gzip")]
1360    #[test]
1361    fn test_gzip_header_len_fcomment() {
1362        // FCOMMENT (0x10): null-terminated string
1363        let extra = b"a comment\0";
1364        assert_eq!(gzip_header_len(&gz_hdr(0x10, extra)).unwrap(), 10 + 10);
1365    }
1366
1367    #[cfg(feature = "gzip")]
1368    #[test]
1369    fn test_gzip_header_len_fcomment_truncated() {
1370        assert!(gzip_header_len(&gz_hdr(0x10, b"nonul")).is_err());
1371    }
1372
1373    #[cfg(feature = "gzip")]
1374    #[test]
1375    fn test_gzip_header_len_fhcrc() {
1376        // FHCRC (0x02): 2-byte CRC of header
1377        assert_eq!(gzip_header_len(&gz_hdr(0x02, &[0xAB, 0xCD])).unwrap(), 12);
1378    }
1379
1380    #[cfg(feature = "gzip")]
1381    #[test]
1382    fn test_gzip_header_len_fhcrc_truncated() {
1383        // FHCRC but only 1 byte follows
1384        assert!(gzip_header_len(&gz_hdr(0x02, &[0xAB])).is_err());
1385    }
1386
1387    #[cfg(feature = "gzip")]
1388    #[test]
1389    fn test_gzip_header_len_all_flags() {
1390        // FEXTRA + FNAME + FCOMMENT + FHCRC, in that order per RFC 1952
1391        let mut extra = Vec::new();
1392        extra.extend_from_slice(&[2u8, 0, 0xAA, 0xBB]); // FEXTRA: xlen=2, 2 bytes
1393        extra.extend_from_slice(b"name\0"); // FNAME: 5 bytes
1394        extra.extend_from_slice(b"cmt\0"); // FCOMMENT: 4 bytes
1395        extra.extend_from_slice(&[0x12, 0x34]); // FHCRC: 2 bytes
1396        let flags = 0x04 | 0x08 | 0x10 | 0x02;
1397        assert_eq!(
1398            gzip_header_len(&gz_hdr(flags, &extra)).unwrap(),
1399            10 + 4 + 5 + 4 + 2
1400        );
1401    }
1402
1403    #[cfg(feature = "gzip")]
1404    #[test]
1405    fn test_gzip_header_len_bad_magic() {
1406        let mut hdr = gz_hdr(0x00, &[]);
1407        hdr[0] = 0x00;
1408        assert!(gzip_header_len(&hdr).is_err());
1409    }
1410
1411    #[cfg(feature = "gzip")]
1412    #[test]
1413    fn test_gzip_header_len_bad_method() {
1414        let mut hdr = gz_hdr(0x00, &[]);
1415        hdr[2] = 0x07; // not deflate
1416        assert!(gzip_header_len(&hdr).is_err());
1417    }
1418
1419    #[cfg(feature = "gzip")]
1420    #[test]
1421    fn test_gzip_header_len_too_short() {
1422        assert!(gzip_header_len(&[0x1f, 0x8b, 0x08]).is_err());
1423    }
1424
1425    #[cfg(feature = "gzip")]
1426    #[test]
1427    fn test_gzip_provider() {
1428        let provider = GzipProvider::default();
1429        let data = b"hello world, this is a test of gzip compression";
1430
1431        let compressed = provider.compress(data).unwrap();
1432        assert_ne!(&compressed[..], data);
1433
1434        let decompressed = provider
1435            .decompress_with_limit(&compressed, usize::MAX)
1436            .unwrap();
1437        assert_eq!(&decompressed[..], data);
1438    }
1439
1440    /// Limit used by the small-message allocation tests: the default
1441    /// per-message limit configured by `Limits::default()`.
1442    const ALLOCATION_TEST_LIMIT: usize = 4 * 1024 * 1024;
1443
1444    /// Returns the capacity of the allocation backing `bytes`.
1445    ///
1446    /// `Bytes::try_into_mut` reuses the original allocation when the handle
1447    /// is unique, so the resulting `BytesMut::capacity()` exposes how much
1448    /// memory the decompressed message actually retains.
1449    fn backing_capacity(bytes: Bytes) -> usize {
1450        bytes
1451            .try_into_mut()
1452            .expect("freshly decompressed Bytes has no other references")
1453            .capacity()
1454    }
1455
1456    /// Upper bound on the backing allocation accepted for a tiny decompressed
1457    /// message. The sizing heuristic yields 256 bytes today; this leaves
1458    /// headroom for modest changes while still failing if a limit-sized (or
1459    /// even tens-of-KiB) buffer is retained per message.
1460    const SMALL_MESSAGE_RETENTION_BOUND: usize = 4096;
1461
1462    /// `backing_capacity` must actually observe over-allocation — otherwise
1463    /// the small-message tests below could pass vacuously if `Bytes::from`
1464    /// ever started shrinking the allocation itself.
1465    #[test]
1466    fn test_backing_capacity_observes_overallocation() {
1467        let mut vec = Vec::with_capacity(1024 * 1024);
1468        vec.extend_from_slice(b"tiny payload");
1469        let capacity = backing_capacity(Bytes::from(vec));
1470        assert!(
1471            capacity >= 1024 * 1024,
1472            "expected the over-allocated backing buffer to be visible, got {capacity}"
1473        );
1474    }
1475
1476    /// Decompressing a small gzip message must not retain a buffer sized by
1477    /// the configured limit: the returned `Bytes` should be backed by an
1478    /// allocation proportional to the actual message.
1479    #[cfg(feature = "gzip")]
1480    #[test]
1481    fn test_gzip_decompress_small_message_allocation() {
1482        let provider = GzipProvider::default();
1483        let compressed = provider.compress(b"tiny payload").unwrap();
1484        let out = provider
1485            .decompress_with_limit(&compressed, ALLOCATION_TEST_LIMIT)
1486            .unwrap();
1487        assert_eq!(&out[..], b"tiny payload");
1488        let capacity = backing_capacity(out);
1489        assert!(
1490            capacity < SMALL_MESSAGE_RETENTION_BOUND,
1491            "small gzip message retained a {capacity}-byte backing buffer"
1492        );
1493    }
1494
1495    /// Same as the gzip allocation test, for the zstd provider.
1496    #[cfg(feature = "zstd")]
1497    #[test]
1498    fn test_zstd_decompress_small_message_allocation() {
1499        let provider = ZstdProvider::default();
1500        let compressed = provider.compress(b"tiny payload").unwrap();
1501        let out = provider
1502            .decompress_with_limit(&compressed, ALLOCATION_TEST_LIMIT)
1503            .unwrap();
1504        assert_eq!(&out[..], b"tiny payload");
1505        let capacity = backing_capacity(out);
1506        assert!(
1507            capacity < SMALL_MESSAGE_RETENTION_BOUND,
1508            "small zstd message retained a {capacity}-byte backing buffer"
1509        );
1510    }
1511
1512    /// Same as the gzip allocation test, for the trait's default
1513    /// `decompress_with_limit` implementation (used by custom providers).
1514    #[test]
1515    fn test_default_trait_decompress_small_message_allocation() {
1516        let provider = MockProvider;
1517        let compressed = provider.compress(b"tiny payload").unwrap();
1518        let out = provider
1519            .decompress_with_limit(&compressed, ALLOCATION_TEST_LIMIT)
1520            .unwrap();
1521        assert_eq!(&out[..], b"tiny payload");
1522        let capacity = backing_capacity(out);
1523        assert!(
1524            capacity < SMALL_MESSAGE_RETENTION_BOUND,
1525            "small message retained a {capacity}-byte backing buffer via the default impl"
1526        );
1527    }
1528
1529    /// Run `f` on a separate thread and require it to produce a result within
1530    /// `timeout`, failing the test immediately otherwise.
1531    ///
1532    /// Threads cannot be killed in Rust, so on timeout the worker is simply
1533    /// abandoned (it ends when the test process exits). The point is that the
1534    /// test itself fails fast with a clear message, instead of hanging until
1535    /// the CI job timeout, if decompression of the input ever stops
1536    /// terminating.
1537    #[cfg(feature = "gzip")]
1538    fn run_with_timeout<T, F>(timeout: std::time::Duration, f: F) -> T
1539    where
1540        T: Send + 'static,
1541        F: FnOnce() -> T + Send + 'static,
1542    {
1543        let (tx, rx) = std::sync::mpsc::channel();
1544        std::thread::spawn(move || {
1545            let _ = tx.send(f());
1546        });
1547        match rx.recv_timeout(timeout) {
1548            Ok(value) => value,
1549            Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
1550                panic!("operation did not complete within {timeout:?}; decompression appears stuck")
1551            }
1552            Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
1553                panic!("worker thread panicked before producing a result")
1554            }
1555        }
1556    }
1557
1558    /// Deadline for the truncated-input decompression tests; generous so the
1559    /// tests stay deterministic on slow CI runners while still failing fast
1560    /// compared to the job timeout.
1561    #[cfg(feature = "gzip")]
1562    const TRUNCATION_TEST_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
1563
1564    /// A minimal, valid gzip member header with nothing after it:
1565    /// id1, id2, CM=8 (deflate), FLG=0, MTIME=0, XFL=0, OS=0xff (unknown).
1566    #[cfg(feature = "gzip")]
1567    const MINIMAL_GZIP_HEADER: [u8; 10] =
1568        [0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff];
1569
1570    /// A gzip member that is only a header — no deflate data, no trailer —
1571    /// must be rejected rather than treated as an incomplete stream to wait
1572    /// on.
1573    #[cfg(feature = "gzip")]
1574    #[test]
1575    fn test_gzip_decompress_header_only() {
1576        let err = run_with_timeout(TRUNCATION_TEST_TIMEOUT, move || {
1577            GzipProvider::default().decompress_with_limit(&MINIMAL_GZIP_HEADER, 1024)
1578        })
1579        .expect_err("header-only gzip member must be rejected");
1580        assert_invalid_argument(&err);
1581        assert!(
1582            err.to_string()
1583                .contains("truncated or invalid deflate stream"),
1584            "unexpected error message: {err}"
1585        );
1586    }
1587
1588    /// A gzip stream cut off in the middle of the deflate data must produce
1589    /// an error.
1590    #[cfg(feature = "gzip")]
1591    #[test]
1592    fn test_gzip_decompress_truncated_deflate_stream() {
1593        let provider = GzipProvider::default();
1594        let data = b"hello world, this is a test of gzip compression";
1595        let compressed = provider.compress(data).unwrap();
1596
1597        let err = run_with_timeout(TRUNCATION_TEST_TIMEOUT, move || {
1598            // Keep the 10-byte header plus a prefix of the deflate stream,
1599            // drop the rest (including the 8-byte trailer).
1600            provider.decompress_with_limit(&compressed[..14], 1024)
1601        })
1602        .expect_err("truncated deflate stream must be rejected");
1603        assert_invalid_argument(&err);
1604        // Which check rejects the prefix depends on where the deflate encoder
1605        // happened to place block boundaries: an incomplete block is caught by
1606        // the stalled-stream handling, while a prefix that ends on a complete
1607        // block is caught by the trailer-length check. Either way the
1608        // truncated payload must be rejected.
1609        let msg = err.to_string();
1610        assert!(
1611            msg.contains("truncated or invalid deflate stream")
1612                || msg.contains("too short for trailer"),
1613            "unexpected error message: {msg}"
1614        );
1615    }
1616
1617    /// A truncated gzip payload is also rejected when it arrives through the
1618    /// registry (the path the request/response handling code uses).
1619    #[cfg(feature = "gzip")]
1620    #[test]
1621    fn test_gzip_registry_decompress_truncated() {
1622        let registry = CompressionRegistry::new().register(GzipProvider::default());
1623        let err = run_with_timeout(TRUNCATION_TEST_TIMEOUT, move || {
1624            registry.decompress_with_limit(
1625                "gzip",
1626                Bytes::copy_from_slice(&MINIMAL_GZIP_HEADER),
1627                1024,
1628            )
1629        })
1630        .expect_err("truncated gzip payload must be rejected via the registry");
1631        assert_invalid_argument(&err);
1632        assert!(
1633            err.to_string()
1634                .contains("truncated or invalid deflate stream"),
1635            "unexpected error message: {err}"
1636        );
1637    }
1638
1639    /// A complete deflate stream with the 8-byte CRC/length trailer cut off
1640    /// must produce an error. (The deflate stream itself decodes fully here;
1641    /// this is rejected by the trailer-length check rather than the
1642    /// truncated-stream handling.)
1643    #[cfg(feature = "gzip")]
1644    #[test]
1645    fn test_gzip_decompress_missing_trailer() {
1646        let provider = GzipProvider::default();
1647        let data = b"hello world, this is a test of gzip compression";
1648        let compressed = provider.compress(data).unwrap();
1649
1650        let missing_trailer = &compressed[..compressed.len() - 8];
1651        let err = provider
1652            .decompress_with_limit(missing_trailer, 1024)
1653            .expect_err("gzip member without its trailer must be rejected");
1654        assert_invalid_argument(&err);
1655        assert!(
1656            err.to_string().contains("too short for trailer"),
1657            "unexpected error message: {err}"
1658        );
1659    }
1660
1661    #[cfg(feature = "gzip")]
1662    #[test]
1663    fn test_gzip_malformed_payloads_are_invalid_argument() {
1664        let provider = GzipProvider::default();
1665
1666        let err = provider
1667            .decompress_with_limit(b"not gzip", 1024)
1668            .expect_err("bad gzip header must be rejected");
1669        assert_invalid_argument(&err);
1670
1671        let data = b"hello world, this is a test of gzip compression";
1672        let compressed = provider.compress(data).unwrap();
1673        let trailer_start = compressed.len() - 8;
1674
1675        let mut bad_crc = compressed.to_vec();
1676        bad_crc[trailer_start] ^= 0xff;
1677        let err = provider
1678            .decompress_with_limit(&bad_crc, 1024)
1679            .expect_err("gzip CRC mismatch must be rejected");
1680        assert_invalid_argument(&err);
1681
1682        let mut bad_size = compressed.to_vec();
1683        let last = bad_size.len() - 1;
1684        bad_size[last] ^= 0xff;
1685        let err = provider
1686            .decompress_with_limit(&bad_size, 1024)
1687            .expect_err("gzip size mismatch must be rejected");
1688        assert_invalid_argument(&err);
1689    }
1690
1691    #[cfg(feature = "gzip")]
1692    #[test]
1693    fn test_gzip_registry() {
1694        let registry = CompressionRegistry::new().register(GzipProvider::default());
1695
1696        assert!(registry.supports("gzip"));
1697        assert!(!registry.supports("zstd"));
1698
1699        let data = b"test data";
1700        let compressed = registry.compress("gzip", data).unwrap();
1701        let decompressed = registry
1702            .decompress_with_limit("gzip", compressed, usize::MAX)
1703            .unwrap();
1704        assert_eq!(&decompressed[..], data);
1705    }
1706
1707    #[cfg(feature = "zstd")]
1708    #[test]
1709    fn test_zstd_provider() {
1710        let provider = ZstdProvider::default();
1711        let data = b"hello world, this is a test of zstd compression";
1712
1713        let compressed = provider.compress(data).unwrap();
1714        assert_ne!(&compressed[..], data);
1715
1716        let decompressed = provider
1717            .decompress_with_limit(&compressed, usize::MAX)
1718            .unwrap();
1719        assert_eq!(&decompressed[..], data);
1720    }
1721
1722    #[cfg(feature = "zstd")]
1723    #[test]
1724    fn test_zstd_malformed_payload_is_invalid_argument() {
1725        let err = ZstdProvider::default()
1726            .decompress_with_limit(b"not zstd", 1024)
1727            .expect_err("malformed zstd payload must be rejected");
1728        assert_invalid_argument(&err);
1729    }
1730
1731    #[cfg(feature = "zstd")]
1732    #[test]
1733    fn test_zstd_high_compression_ratio() {
1734        // Regression test for the old bulk::Decompressor path which sized
1735        // the output buffer at `input.len() * 4` — highly-compressible data
1736        // (e.g. zeroes) can compress >100×, making the guess far too small.
1737        // The streaming decoder handles any ratio.
1738        let provider = ZstdProvider::default();
1739        let data = vec![0u8; 100_000];
1740        let compressed = provider.compress(&data).unwrap();
1741        // Sanity: compression ratio should be well above 4×
1742        assert!(
1743            compressed.len() * 4 < data.len(),
1744            "expected high compression ratio; got {} bytes -> {} bytes",
1745            data.len(),
1746            compressed.len()
1747        );
1748        let decompressed = provider
1749            .decompress_with_limit(&compressed, usize::MAX)
1750            .unwrap();
1751        assert_eq!(decompressed.len(), data.len());
1752        assert!(decompressed.iter().all(|&b| b == 0));
1753    }
1754
1755    #[cfg(feature = "zstd")]
1756    #[test]
1757    fn test_zstd_registry() {
1758        let registry = CompressionRegistry::new().register(ZstdProvider::default());
1759
1760        assert!(registry.supports("zstd"));
1761        assert!(!registry.supports("gzip"));
1762
1763        let data = b"test data";
1764        let compressed = registry.compress("zstd", data).unwrap();
1765        let decompressed = registry
1766            .decompress_with_limit("zstd", compressed, usize::MAX)
1767            .unwrap();
1768        assert_eq!(&decompressed[..], data);
1769    }
1770
1771    #[test]
1772    fn test_unsupported_encoding() {
1773        let registry = CompressionRegistry::new();
1774        let result =
1775            registry.decompress_with_limit("unknown", Bytes::from_static(b"data"), usize::MAX);
1776        assert!(result.is_err());
1777    }
1778
1779    #[test]
1780    #[cfg(feature = "zstd")]
1781    fn test_decompress_empty_body_with_encoding_header() {
1782        // Connect spec: "Servers must not attempt to decompress zero-length
1783        // HTTP request content." Clients may set Content-Encoding but skip
1784        // compressing empty payloads. The decoder (especially zstd) would
1785        // reject this as an incomplete frame — the registry must short-circuit.
1786        let registry = CompressionRegistry::default();
1787        let result = registry.decompress_with_limit("zstd", Bytes::new(), usize::MAX);
1788        assert_eq!(result.unwrap().len(), 0);
1789
1790        let result = registry.decompress_with_limit("gzip", Bytes::new(), usize::MAX);
1791        assert_eq!(result.unwrap().len(), 0);
1792
1793        // Also works via decompress_with_limit
1794        let result = registry.decompress_with_limit("zstd", Bytes::new(), 1024);
1795        assert_eq!(result.unwrap().len(), 0);
1796    }
1797
1798    #[test]
1799    fn test_decompress_empty_body_unknown_encoding_still_errors() {
1800        // The empty-body short-circuit must NOT mask unsupported encodings.
1801        // Content-Encoding: foo (unknown) should error even with empty body
1802        // (conformance "unexpected-compression" test).
1803        let registry = CompressionRegistry::default();
1804        let result = registry.decompress_with_limit("foo", Bytes::new(), usize::MAX);
1805        let err = result.unwrap_err();
1806        assert_eq!(err.code, crate::error::ErrorCode::Unimplemented);
1807    }
1808
1809    #[cfg(all(feature = "gzip", feature = "zstd"))]
1810    #[test]
1811    fn test_default_registry() {
1812        let registry = CompressionRegistry::default();
1813        assert!(registry.supports("gzip"));
1814        assert!(registry.supports("zstd"));
1815    }
1816
1817    #[test]
1818    fn test_accept_encoding_header() {
1819        let registry = CompressionRegistry::new();
1820        assert_eq!(registry.accept_encoding_header(), "");
1821
1822        #[cfg(feature = "gzip")]
1823        {
1824            let registry = CompressionRegistry::new().register(GzipProvider::default());
1825            assert_eq!(registry.accept_encoding_header(), "gzip");
1826        }
1827    }
1828
1829    #[cfg(all(feature = "gzip", feature = "zstd"))]
1830    #[test]
1831    fn test_accept_encoding_header_sorted_deterministic() {
1832        // Cached string must be identical regardless of registration order.
1833        let r1 = CompressionRegistry::new()
1834            .register(GzipProvider::default())
1835            .register(ZstdProvider::default());
1836        let r2 = CompressionRegistry::new()
1837            .register(ZstdProvider::default())
1838            .register(GzipProvider::default());
1839        assert_eq!(r1.accept_encoding_header(), "gzip, zstd");
1840        assert_eq!(r2.accept_encoding_header(), "gzip, zstd");
1841    }
1842
1843    // Test custom provider
1844    struct MockProvider;
1845
1846    impl CompressionProvider for MockProvider {
1847        fn name(&self) -> &'static str {
1848            "mock"
1849        }
1850
1851        fn compress(&self, data: &[u8]) -> Result<Bytes, ConnectError> {
1852            // Just reverse the bytes as a mock "compression"
1853            Ok(Bytes::from(data.iter().rev().copied().collect::<Vec<_>>()))
1854        }
1855
1856        fn decompressor<'a>(
1857            &self,
1858            data: &'a [u8],
1859        ) -> Result<Box<dyn std::io::Read + 'a>, ConnectError> {
1860            // Reverse bytes and return a reader over the result
1861            let reversed: Vec<u8> = data.iter().rev().copied().collect();
1862            Ok(Box::new(std::io::Cursor::new(reversed)))
1863        }
1864    }
1865
1866    /// Provider whose reader fails mid-read, exercising the default
1867    /// `decompress_with_limit` error path for malformed input.
1868    struct FailingReadProvider;
1869
1870    impl CompressionProvider for FailingReadProvider {
1871        fn name(&self) -> &'static str {
1872            "failing"
1873        }
1874
1875        fn compress(&self, data: &[u8]) -> Result<Bytes, ConnectError> {
1876            Ok(Bytes::copy_from_slice(data))
1877        }
1878
1879        fn decompressor<'a>(
1880            &self,
1881            _data: &'a [u8],
1882        ) -> Result<Box<dyn std::io::Read + 'a>, ConnectError> {
1883            struct FailingReader;
1884            impl std::io::Read for FailingReader {
1885                fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
1886                    Err(std::io::Error::new(
1887                        std::io::ErrorKind::InvalidData,
1888                        "corrupt stream",
1889                    ))
1890                }
1891            }
1892            Ok(Box::new(FailingReader))
1893        }
1894    }
1895
1896    /// The default `decompress_with_limit` reports malformed input as
1897    /// `invalid_argument`, matching the built-in gzip/zstd providers.
1898    #[test]
1899    fn test_default_trait_decompress_malformed_is_invalid_argument() {
1900        let err = FailingReadProvider
1901            .decompress_with_limit(b"whatever", usize::MAX)
1902            .unwrap_err();
1903        assert_eq!(err.code, crate::error::ErrorCode::InvalidArgument);
1904    }
1905
1906    #[test]
1907    fn test_custom_provider() {
1908        let registry = CompressionRegistry::new().register(MockProvider);
1909
1910        assert!(registry.supports("mock"));
1911
1912        let data = b"hello";
1913        let compressed = registry.compress("mock", data).unwrap();
1914        assert_eq!(&compressed[..], b"olleh");
1915
1916        let decompressed = registry
1917            .decompress_with_limit("mock", compressed, usize::MAX)
1918            .unwrap();
1919        assert_eq!(&decompressed[..], data);
1920    }
1921
1922    #[cfg(all(feature = "gzip", feature = "streaming"))]
1923    #[tokio::test]
1924    async fn test_gzip_streaming() {
1925        use tokio::io::AsyncReadExt;
1926
1927        let registry = CompressionRegistry::default();
1928        assert!(registry.supports_streaming("gzip"));
1929
1930        // Create test data
1931        let data = b"hello world, this is a test of streaming gzip compression";
1932
1933        // Compress using buffered method
1934        let compressed = registry.compress("gzip", data).unwrap();
1935
1936        // Decompress using streaming
1937        let reader: BoxedAsyncBufRead = Box::pin(std::io::Cursor::new(compressed.to_vec()));
1938        let mut decompressor = registry.decompress_stream("gzip", reader).unwrap();
1939
1940        let mut decompressed = Vec::new();
1941        decompressor.read_to_end(&mut decompressed).await.unwrap();
1942
1943        assert_eq!(&decompressed[..], data);
1944    }
1945
1946    #[cfg(all(feature = "zstd", feature = "streaming"))]
1947    #[tokio::test]
1948    async fn test_zstd_streaming() {
1949        use tokio::io::AsyncReadExt;
1950
1951        let registry = CompressionRegistry::default();
1952        assert!(registry.supports_streaming("zstd"));
1953
1954        // Create test data
1955        let data = b"hello world, this is a test of streaming zstd compression";
1956
1957        // Compress using buffered method
1958        let compressed = registry.compress("zstd", data).unwrap();
1959
1960        // Decompress using streaming
1961        let reader: BoxedAsyncBufRead = Box::pin(std::io::Cursor::new(compressed.to_vec()));
1962        let mut decompressor = registry.decompress_stream("zstd", reader).unwrap();
1963
1964        let mut decompressed = Vec::new();
1965        decompressor.read_to_end(&mut decompressed).await.unwrap();
1966
1967        assert_eq!(&decompressed[..], data);
1968    }
1969
1970    #[cfg(all(feature = "gzip", feature = "streaming"))]
1971    #[tokio::test]
1972    async fn test_streaming_compress_decompress_roundtrip() {
1973        use tokio::io::AsyncReadExt;
1974
1975        let registry = CompressionRegistry::default();
1976
1977        // Create test data
1978        let data = b"hello world, this is a roundtrip test of streaming compression";
1979
1980        // Compress using streaming
1981        let input: BoxedAsyncBufRead = Box::pin(std::io::Cursor::new(data.to_vec()));
1982        let mut compressor = registry.compress_stream("gzip", input).unwrap();
1983
1984        let mut compressed = Vec::new();
1985        compressor.read_to_end(&mut compressed).await.unwrap();
1986
1987        // Decompress using streaming
1988        let reader: BoxedAsyncBufRead = Box::pin(std::io::Cursor::new(compressed));
1989        let mut decompressor = registry.decompress_stream("gzip", reader).unwrap();
1990
1991        let mut decompressed = Vec::new();
1992        decompressor.read_to_end(&mut decompressed).await.unwrap();
1993
1994        assert_eq!(&decompressed[..], data);
1995    }
1996
1997    #[cfg(feature = "gzip")]
1998    #[test]
1999    fn test_gzip_decompress_with_limit_under() {
2000        let provider = GzipProvider::default();
2001        let data = b"hello world";
2002        let compressed = provider.compress(data).unwrap();
2003
2004        // Limit larger than data — succeeds
2005        let result = provider.decompress_with_limit(&compressed, 1024);
2006        assert!(result.is_ok());
2007        assert_eq!(&result.unwrap()[..], data);
2008    }
2009
2010    #[cfg(feature = "gzip")]
2011    #[test]
2012    fn test_gzip_decompress_with_limit_exact() {
2013        let provider = GzipProvider::default();
2014        let data = b"hello world";
2015        let compressed = provider.compress(data).unwrap();
2016
2017        // Limit exactly equal to data length — succeeds
2018        let result = provider.decompress_with_limit(&compressed, data.len());
2019        assert!(result.is_ok());
2020        assert_eq!(&result.unwrap()[..], data);
2021    }
2022
2023    #[cfg(feature = "gzip")]
2024    #[test]
2025    fn test_gzip_decompress_with_limit_exceeded() {
2026        let provider = GzipProvider::default();
2027        // Create data larger than our limit
2028        let data = vec![0u8; 1024];
2029        let compressed = provider.compress(&data).unwrap();
2030
2031        // Limit smaller than decompressed size — fails
2032        let result = provider.decompress_with_limit(&compressed, 512);
2033        assert!(result.is_err());
2034        let err = result.unwrap_err();
2035        assert_eq!(err.code, crate::ErrorCode::ResourceExhausted);
2036    }
2037
2038    #[cfg(feature = "zstd")]
2039    #[test]
2040    fn test_zstd_decompress_with_limit_under() {
2041        let provider = ZstdProvider::default();
2042        let data = b"hello world";
2043        let compressed = provider.compress(data).unwrap();
2044
2045        let result = provider.decompress_with_limit(&compressed, 1024);
2046        assert!(result.is_ok());
2047        assert_eq!(&result.unwrap()[..], data);
2048    }
2049
2050    #[cfg(feature = "zstd")]
2051    #[test]
2052    fn test_zstd_decompress_with_limit_exact() {
2053        let provider = ZstdProvider::default();
2054        let data = b"hello world";
2055        let compressed = provider.compress(data).unwrap();
2056
2057        let result = provider.decompress_with_limit(&compressed, data.len());
2058        assert!(result.is_ok());
2059        assert_eq!(&result.unwrap()[..], data);
2060    }
2061
2062    #[cfg(feature = "zstd")]
2063    #[test]
2064    fn test_zstd_decompress_with_limit_exceeded() {
2065        let provider = ZstdProvider::default();
2066        let data = vec![0u8; 1024];
2067        let compressed = provider.compress(&data).unwrap();
2068
2069        let result = provider.decompress_with_limit(&compressed, 512);
2070        assert!(result.is_err());
2071        let err = result.unwrap_err();
2072        assert_eq!(err.code, crate::ErrorCode::ResourceExhausted);
2073    }
2074
2075    #[test]
2076    fn test_compression_policy_default() {
2077        let policy = CompressionPolicy::default();
2078        // Below threshold — should not compress
2079        assert!(!policy.should_compress(512));
2080        assert!(!policy.should_compress(1023));
2081        // At and above threshold — should compress
2082        assert!(policy.should_compress(1024));
2083        assert!(policy.should_compress(4096));
2084    }
2085
2086    #[test]
2087    fn test_compression_policy_disabled() {
2088        let policy = CompressionPolicy::disabled();
2089        assert!(!policy.should_compress(0));
2090        assert!(!policy.should_compress(1024));
2091        assert!(!policy.should_compress(1_000_000));
2092    }
2093
2094    #[test]
2095    fn test_compression_policy_custom_min_size() {
2096        let policy = CompressionPolicy::default().min_size(4096);
2097        assert!(!policy.should_compress(1024));
2098        assert!(!policy.should_compress(4095));
2099        assert!(policy.should_compress(4096));
2100        assert!(policy.should_compress(8192));
2101    }
2102
2103    #[test]
2104    fn test_compression_policy_empty_message() {
2105        // Default policy (min_size=1024) skips empty bodies.
2106        let default_policy = CompressionPolicy::default();
2107        assert!(!default_policy.should_compress(0));
2108
2109        // min_size=0 compresses even empty bodies — the Connect spec permits
2110        // this (receivers skip decompression for zero-length content), and
2111        // conformance runners check that advertised encodings are applied.
2112        let zero_min = CompressionPolicy::default().min_size(0);
2113        assert!(zero_min.should_compress(0));
2114
2115        let disabled = CompressionPolicy::disabled();
2116        assert!(!disabled.should_compress(0));
2117    }
2118
2119    #[test]
2120    fn test_compression_policy_with_override() {
2121        let policy = CompressionPolicy::default();
2122
2123        // No override — uses policy as-is
2124        let effective = policy.with_override(None);
2125        assert!(!effective.should_compress(512));
2126        assert!(effective.should_compress(2048));
2127
2128        // Force compression — min_size = 0 means even empty bodies compress
2129        // (conformance runners verify advertised encodings are applied).
2130        let forced = policy.with_override(Some(true));
2131        assert!(forced.should_compress(0));
2132        assert!(forced.should_compress(1));
2133
2134        // Disable compression
2135        let disabled = policy.with_override(Some(false));
2136        assert!(!disabled.should_compress(0));
2137        assert!(!disabled.should_compress(1_000_000));
2138    }
2139
2140    #[test]
2141    fn test_identity_decompress_with_limit() {
2142        let registry = CompressionRegistry::new();
2143        let data = Bytes::from_static(b"hello world");
2144
2145        // Under limit
2146        let result = registry.decompress_with_limit("identity", data.clone(), 1024);
2147        assert!(result.is_ok());
2148
2149        // Exact limit
2150        let result = registry.decompress_with_limit("identity", data.clone(), data.len());
2151        assert!(result.is_ok());
2152
2153        // Over limit
2154        let result = registry.decompress_with_limit("identity", data, 5);
2155        assert!(result.is_err());
2156    }
2157}