Skip to main content

tonic/codec/
compression.rs

1/*
2 *
3 * Copyright 2025 gRPC authors.
4 *
5 * Permission is hereby granted, free of charge, to any person obtaining a copy
6 * of this software and associated documentation files (the "Software"), to
7 * deal in the Software without restriction, including without limitation the
8 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
9 * sell copies of the Software, and to permit persons to whom the Software is
10 * furnished to do so, subject to the following conditions:
11 *
12 * The above copyright notice and this permission notice shall be included in
13 * all copies or substantial portions of the Software.
14 *
15 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21 * IN THE SOFTWARE.
22 *
23 */
24
25use crate::{Status, metadata::MetadataValue};
26use bytes::{Buf, BufMut, BytesMut};
27#[cfg(feature = "gzip")]
28use flate2::read::{GzDecoder, GzEncoder};
29#[cfg(feature = "deflate")]
30use flate2::read::{ZlibDecoder, ZlibEncoder};
31use std::{borrow::Cow, fmt};
32#[cfg(feature = "zstd")]
33use zstd::stream::read::{Decoder, Encoder};
34
35pub(crate) const ENCODING_HEADER: &str = "grpc-encoding";
36pub(crate) const ACCEPT_ENCODING_HEADER: &str = "grpc-accept-encoding";
37
38/// Struct used to configure which encodings are enabled on a server or channel.
39///
40/// Represents an ordered list of compression encodings that are enabled.
41#[derive(Debug, Default, Clone, Copy)]
42pub struct EnabledCompressionEncodings {
43    inner: [Option<CompressionEncoding>; 3],
44}
45
46impl EnabledCompressionEncodings {
47    /// Enable a [`CompressionEncoding`].
48    ///
49    /// Adds the new encoding to the end of the encoding list.
50    pub fn enable(&mut self, encoding: CompressionEncoding) {
51        for e in self.inner.iter_mut() {
52            match e {
53                Some(e) if *e == encoding => return,
54                None => {
55                    *e = Some(encoding);
56                    return;
57                }
58                _ => continue,
59            }
60        }
61    }
62
63    /// Remove the last [`CompressionEncoding`].
64    pub fn pop(&mut self) -> Option<CompressionEncoding> {
65        self.inner
66            .iter_mut()
67            .rev()
68            .find(|entry| entry.is_some())?
69            .take()
70    }
71
72    pub(crate) fn into_accept_encoding_header_value(self) -> Option<http::HeaderValue> {
73        let mut value = BytesMut::new();
74        for encoding in self.inner.into_iter().flatten() {
75            value.put_slice(encoding.as_str().as_bytes());
76            value.put_u8(b',');
77        }
78
79        if value.is_empty() {
80            return None;
81        }
82
83        value.put_slice(b"identity");
84        Some(http::HeaderValue::from_maybe_shared(value).unwrap())
85    }
86
87    /// Check if a [`CompressionEncoding`] is enabled.
88    pub fn is_enabled(&self, encoding: CompressionEncoding) -> bool {
89        self.inner.contains(&Some(encoding))
90    }
91
92    /// Check if any [`CompressionEncoding`]s are enabled.
93    pub fn is_empty(&self) -> bool {
94        self.inner.iter().all(|e| e.is_none())
95    }
96}
97
98#[derive(Clone, Copy, Debug, PartialEq, Eq)]
99pub(crate) struct CompressionSettings {
100    pub(crate) encoding: CompressionEncoding,
101    /// buffer_growth_interval controls memory growth for internal buffers to balance resizing cost against memory waste.
102    /// The default buffer growth interval is 8 kilobytes.
103    pub(crate) buffer_growth_interval: usize,
104}
105
106/// The compression encodings Tonic supports.
107#[derive(Clone, Copy, Debug, PartialEq, Eq)]
108#[non_exhaustive]
109pub enum CompressionEncoding {
110    #[allow(missing_docs)]
111    #[cfg(feature = "gzip")]
112    Gzip,
113    #[allow(missing_docs)]
114    #[cfg(feature = "deflate")]
115    Deflate,
116    #[allow(missing_docs)]
117    #[cfg(feature = "zstd")]
118    Zstd,
119}
120
121impl CompressionEncoding {
122    pub(crate) const ENCODINGS: &'static [CompressionEncoding] = &[
123        #[cfg(feature = "gzip")]
124        CompressionEncoding::Gzip,
125        #[cfg(feature = "deflate")]
126        CompressionEncoding::Deflate,
127        #[cfg(feature = "zstd")]
128        CompressionEncoding::Zstd,
129    ];
130
131    /// Based on the `grpc-accept-encoding` header, pick an encoding to use.
132    pub(crate) fn from_accept_encoding_header(
133        map: &http::HeaderMap,
134        enabled_encodings: EnabledCompressionEncodings,
135    ) -> Option<Self> {
136        if enabled_encodings.is_empty() {
137            return None;
138        }
139
140        let header_value = map.get(ACCEPT_ENCODING_HEADER)?;
141        let header_value_str = header_value.to_str().ok()?;
142
143        split_by_comma(header_value_str).find_map(|value| match value {
144            #[cfg(feature = "gzip")]
145            "gzip" if enabled_encodings.is_enabled(CompressionEncoding::Gzip) => {
146                Some(CompressionEncoding::Gzip)
147            }
148            #[cfg(feature = "deflate")]
149            "deflate" if enabled_encodings.is_enabled(CompressionEncoding::Deflate) => {
150                Some(CompressionEncoding::Deflate)
151            }
152            #[cfg(feature = "zstd")]
153            "zstd" if enabled_encodings.is_enabled(CompressionEncoding::Zstd) => {
154                Some(CompressionEncoding::Zstd)
155            }
156            _ => None,
157        })
158    }
159
160    /// Get the value of `grpc-encoding` header. Returns an error if the encoding isn't supported.
161    pub(crate) fn from_encoding_header(
162        map: &http::HeaderMap,
163        enabled_encodings: EnabledCompressionEncodings,
164    ) -> Result<Option<Self>, Status> {
165        let Some(header_value) = map.get(ENCODING_HEADER) else {
166            return Ok(None);
167        };
168
169        match header_value.as_bytes() {
170            #[cfg(feature = "gzip")]
171            b"gzip" if enabled_encodings.is_enabled(CompressionEncoding::Gzip) => {
172                Ok(Some(CompressionEncoding::Gzip))
173            }
174            #[cfg(feature = "deflate")]
175            b"deflate" if enabled_encodings.is_enabled(CompressionEncoding::Deflate) => {
176                Ok(Some(CompressionEncoding::Deflate))
177            }
178            #[cfg(feature = "zstd")]
179            b"zstd" if enabled_encodings.is_enabled(CompressionEncoding::Zstd) => {
180                Ok(Some(CompressionEncoding::Zstd))
181            }
182            b"identity" => Ok(None),
183            other => {
184                let other = match std::str::from_utf8(other) {
185                    Ok(s) => Cow::Borrowed(s),
186                    Err(_) => Cow::Owned(format!("{other:?}")),
187                };
188
189                let mut status = Status::unimplemented(format!(
190                    "Content is compressed with `{other}` which isn't supported"
191                ));
192
193                let header_value = enabled_encodings
194                    .into_accept_encoding_header_value()
195                    .map(MetadataValue::unchecked_from_header_value)
196                    .unwrap_or_else(|| MetadataValue::from_static("identity"));
197                status
198                    .metadata_mut()
199                    .insert(ACCEPT_ENCODING_HEADER, header_value);
200
201                Err(status)
202            }
203        }
204    }
205
206    pub(crate) fn as_str(self) -> &'static str {
207        match self {
208            #[cfg(feature = "gzip")]
209            CompressionEncoding::Gzip => "gzip",
210            #[cfg(feature = "deflate")]
211            CompressionEncoding::Deflate => "deflate",
212            #[cfg(feature = "zstd")]
213            CompressionEncoding::Zstd => "zstd",
214        }
215    }
216
217    #[cfg(any(feature = "gzip", feature = "deflate", feature = "zstd"))]
218    pub(crate) fn into_header_value(self) -> http::HeaderValue {
219        http::HeaderValue::from_static(self.as_str())
220    }
221}
222
223impl fmt::Display for CompressionEncoding {
224    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
225        f.write_str(self.as_str())
226    }
227}
228
229fn split_by_comma(s: &str) -> impl Iterator<Item = &str> {
230    s.split(',').map(|s| s.trim())
231}
232
233/// Compress `len` bytes from `decompressed_buf` into `out_buf`.
234/// buffer_size_increment is a hint to control the growth of out_buf versus the cost of resizing it.
235#[allow(unused_variables, unreachable_code)]
236pub(crate) fn compress(
237    settings: CompressionSettings,
238    decompressed_buf: &mut BytesMut,
239    out_buf: &mut BytesMut,
240    len: usize,
241) -> Result<(), std::io::Error> {
242    let buffer_growth_interval = settings.buffer_growth_interval;
243    let capacity = ((len / buffer_growth_interval) + 1) * buffer_growth_interval;
244    out_buf.reserve(capacity);
245
246    #[cfg(any(feature = "gzip", feature = "deflate", feature = "zstd"))]
247    let mut out_writer = out_buf.writer();
248
249    match settings.encoding {
250        #[cfg(feature = "gzip")]
251        CompressionEncoding::Gzip => {
252            let mut gzip_encoder = GzEncoder::new(
253                &decompressed_buf[0..len],
254                // FIXME: support customizing the compression level
255                flate2::Compression::new(6),
256            );
257            std::io::copy(&mut gzip_encoder, &mut out_writer)?;
258        }
259        #[cfg(feature = "deflate")]
260        CompressionEncoding::Deflate => {
261            let mut deflate_encoder = ZlibEncoder::new(
262                &decompressed_buf[0..len],
263                // FIXME: support customizing the compression level
264                flate2::Compression::new(6),
265            );
266            std::io::copy(&mut deflate_encoder, &mut out_writer)?;
267        }
268        #[cfg(feature = "zstd")]
269        CompressionEncoding::Zstd => {
270            let mut zstd_encoder = Encoder::new(
271                &decompressed_buf[0..len],
272                // FIXME: support customizing the compression level
273                zstd::DEFAULT_COMPRESSION_LEVEL,
274            )?;
275            std::io::copy(&mut zstd_encoder, &mut out_writer)?;
276        }
277    }
278
279    decompressed_buf.advance(len);
280
281    Ok(())
282}
283
284/// Decompress `len` bytes from `compressed_buf` into `out_buf`.
285#[allow(unused_variables, unreachable_code)]
286pub(crate) fn decompress(
287    settings: CompressionSettings,
288    compressed_buf: &mut BytesMut,
289    mut out_buf: bytes::buf::Limit<&mut BytesMut>,
290    len: usize,
291) -> Result<(), std::io::Error> {
292    let buffer_growth_interval = settings.buffer_growth_interval;
293    let estimate_decompressed_len = len * 2;
294    let capacity = std::cmp::min(
295        bytes::buf::Limit::limit(&out_buf),
296        ((estimate_decompressed_len / buffer_growth_interval) + 1) * buffer_growth_interval,
297    );
298
299    out_buf.get_mut().reserve(capacity);
300
301    #[cfg(any(feature = "gzip", feature = "deflate", feature = "zstd"))]
302    let mut out_writer = out_buf.writer();
303
304    match settings.encoding {
305        #[cfg(feature = "gzip")]
306        CompressionEncoding::Gzip => {
307            let mut gzip_decoder = GzDecoder::new(&compressed_buf[0..len]);
308            std::io::copy(&mut gzip_decoder, &mut out_writer)?;
309        }
310        #[cfg(feature = "deflate")]
311        CompressionEncoding::Deflate => {
312            let mut deflate_decoder = ZlibDecoder::new(&compressed_buf[0..len]);
313            std::io::copy(&mut deflate_decoder, &mut out_writer)?;
314        }
315        #[cfg(feature = "zstd")]
316        CompressionEncoding::Zstd => {
317            let mut zstd_decoder = Decoder::new(&compressed_buf[0..len])?;
318            std::io::copy(&mut zstd_decoder, &mut out_writer)?;
319        }
320    }
321
322    compressed_buf.advance(len);
323
324    Ok(())
325}
326
327/// Controls compression behavior for individual messages within a stream.
328#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
329pub enum SingleMessageCompressionOverride {
330    /// Inherit whatever compression is already configured. If the stream is compressed this
331    /// message will also be configured.
332    ///
333    /// This is the default.
334    #[default]
335    Inherit,
336    /// Don't compress this message, even if compression is enabled on the stream.
337    Disable,
338}
339
340#[cfg(test)]
341mod tests {
342    #[cfg(any(feature = "gzip", feature = "deflate", feature = "zstd"))]
343    use http::HeaderValue;
344
345    use super::*;
346
347    #[test]
348    fn convert_none_into_header_value() {
349        let encodings = EnabledCompressionEncodings::default();
350
351        assert!(encodings.into_accept_encoding_header_value().is_none());
352    }
353
354    #[test]
355    #[cfg(feature = "gzip")]
356    fn convert_gzip_into_header_value() {
357        const GZIP: HeaderValue = HeaderValue::from_static("gzip,identity");
358
359        let encodings = EnabledCompressionEncodings {
360            inner: [Some(CompressionEncoding::Gzip), None, None],
361        };
362
363        assert_eq!(encodings.into_accept_encoding_header_value().unwrap(), GZIP);
364
365        let encodings = EnabledCompressionEncodings {
366            inner: [None, None, Some(CompressionEncoding::Gzip)],
367        };
368
369        assert_eq!(encodings.into_accept_encoding_header_value().unwrap(), GZIP);
370    }
371
372    #[test]
373    #[cfg(feature = "zstd")]
374    fn convert_zstd_into_header_value() {
375        const ZSTD: HeaderValue = HeaderValue::from_static("zstd,identity");
376
377        let encodings = EnabledCompressionEncodings {
378            inner: [Some(CompressionEncoding::Zstd), None, None],
379        };
380
381        assert_eq!(encodings.into_accept_encoding_header_value().unwrap(), ZSTD);
382
383        let encodings = EnabledCompressionEncodings {
384            inner: [None, None, Some(CompressionEncoding::Zstd)],
385        };
386
387        assert_eq!(encodings.into_accept_encoding_header_value().unwrap(), ZSTD);
388    }
389
390    #[test]
391    #[cfg(all(feature = "gzip", feature = "zstd"))]
392    fn from_accept_encoding_header_respects_server_enabled_encodings() {
393        // Regression test for the case where the client advertises multiple
394        // encodings in its `grpc-accept-encoding` header but the server only
395        // enabled a subset of them via `.send_compressed(...)`.
396        //
397        // Previously, `from_accept_encoding_header` would pick the first
398        // encoding in the client's list whose `cfg(feature = ...)` was
399        // compiled in, without checking whether the server actually enabled
400        // that encoding. That meant a server configured for Zstd-only would
401        // gzip its responses whenever a client listed `gzip` before `zstd`
402        // in `grpc-accept-encoding` — even though the server never asked for
403        // gzip and (in `into_accept_encoding_header_value`) would never have
404        // advertised it.
405        //
406        // The selected encoding must come from the intersection of the
407        // server's enabled set and the client's accept list, preserving the
408        // client's preference order.
409        let mut enabled = EnabledCompressionEncodings::default();
410        enabled.enable(CompressionEncoding::Zstd);
411        assert!(enabled.is_enabled(CompressionEncoding::Zstd));
412        assert!(!enabled.is_enabled(CompressionEncoding::Gzip));
413
414        let mut headers = http::HeaderMap::new();
415        headers.insert(
416            ACCEPT_ENCODING_HEADER,
417            HeaderValue::from_static("gzip,zstd,identity"),
418        );
419
420        assert_eq!(
421            CompressionEncoding::from_accept_encoding_header(&headers, enabled),
422            Some(CompressionEncoding::Zstd),
423            "server has only Zstd enabled; must not pick Gzip just because \
424             the client listed it first",
425        );
426    }
427
428    #[test]
429    #[cfg(all(feature = "gzip", feature = "zstd"))]
430    fn from_accept_encoding_header_returns_none_when_no_overlap() {
431        // If the client's `grpc-accept-encoding` and the server's enabled
432        // encodings have no overlap, no compression should be selected — the
433        // server should fall back to sending the response uncompressed.
434        let mut enabled = EnabledCompressionEncodings::default();
435        enabled.enable(CompressionEncoding::Zstd);
436
437        let mut headers = http::HeaderMap::new();
438        headers.insert(
439            ACCEPT_ENCODING_HEADER,
440            HeaderValue::from_static("gzip,identity"),
441        );
442
443        assert_eq!(
444            CompressionEncoding::from_accept_encoding_header(&headers, enabled),
445            None,
446        );
447    }
448
449    #[test]
450    #[cfg(all(feature = "gzip", feature = "zstd"))]
451    fn from_accept_encoding_header_uses_client_preference_order() {
452        // When multiple enabled encodings appear in the client's accept list,
453        // the client's ordering wins. Both encodings are enabled on the
454        // server, but the client prefers gzip.
455        let mut enabled = EnabledCompressionEncodings::default();
456        enabled.enable(CompressionEncoding::Zstd);
457        enabled.enable(CompressionEncoding::Gzip);
458
459        let mut headers = http::HeaderMap::new();
460        headers.insert(
461            ACCEPT_ENCODING_HEADER,
462            HeaderValue::from_static("gzip,zstd,identity"),
463        );
464
465        assert_eq!(
466            CompressionEncoding::from_accept_encoding_header(&headers, enabled),
467            Some(CompressionEncoding::Gzip),
468        );
469    }
470
471    #[test]
472    #[cfg(all(feature = "gzip", feature = "deflate", feature = "zstd"))]
473    fn convert_compression_encodings_into_header_value() {
474        let encodings = EnabledCompressionEncodings {
475            inner: [
476                Some(CompressionEncoding::Gzip),
477                Some(CompressionEncoding::Deflate),
478                Some(CompressionEncoding::Zstd),
479            ],
480        };
481
482        assert_eq!(
483            encodings.into_accept_encoding_header_value().unwrap(),
484            HeaderValue::from_static("gzip,deflate,zstd,identity"),
485        );
486
487        let encodings = EnabledCompressionEncodings {
488            inner: [
489                Some(CompressionEncoding::Zstd),
490                Some(CompressionEncoding::Deflate),
491                Some(CompressionEncoding::Gzip),
492            ],
493        };
494
495        assert_eq!(
496            encodings.into_accept_encoding_header_value().unwrap(),
497            HeaderValue::from_static("zstd,deflate,gzip,identity"),
498        );
499    }
500}