Skip to main content

rama_http_headers/common/
content_encoding.rs

1use rama_utils::collections::NonEmptyVec;
2
3use self::sealed::CmpCoding;
4
5rama_utils::macros::enums::enum_builder! {
6    /// Directive for the [`ContentEncoding`] header.
7    @String
8    pub enum ContentEncodingDirective {
9        /// A format using the [Lempel-Ziv coding](https://en.wikipedia.org/wiki/LZ77_and_LZ78#LZ77) (LZ77),
10        /// with a 32-bit CRC. This is the original format of the UNIX gzip program.
11        ///
12        /// The HTTP/1.1 standard also recommends that the servers supporting this
13        /// content-encoding should recognize x-gzip as an alias, for compatibility purposes.
14        Gzip => "gzip" | "x-gzip",
15        /// A format using the [Lempel-Ziv-Welch](https://en.wikipedia.org/wiki/LZW) (LZW) algorithm.
16        /// The value name was taken from the UNIX compress program,
17        /// which implemented this algorithm. Like the compress program,
18        /// which has disappeared from most UNIX distributions,
19        /// this content-encoding is not used by many browsers today,
20        /// partly because of a patent issue (it expired in 2003).
21        Compress => "compress",
22        /// Using the zlib structure (defined in [RFC 1950](https://datatracker.ietf.org/doc/html/rfc1950))
23        /// with the [deflate](https://en.wikipedia.org/wiki/Deflate) compression algorithm
24        /// (defined in [RFC 1951](https://datatracker.ietf.org/doc/html/rfc1951)).
25        Deflate => "deflate",
26        /// A format using the [Brotli](https://developer.mozilla.org/en-US/docs/Glossary/Brotli_compression)
27        /// algorithm structure (defined in [RFC 7932](https://datatracker.ietf.org/doc/html/rfc7932)).
28        Brotli => "br",
29        /// A format using the [Zstandard](https://developer.mozilla.org/en-US/docs/Glossary/Zstandard_compression)
30        /// algorithm structure (defined in [RFC 8878](https://datatracker.ietf.org/doc/html/rfc8878)).
31        ZStandard => "zstd",
32        /// A format that uses the [Dictionary-Compressed Brotli algorithm](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-compression-dictionary#name-dictionary-compressed-brotl).
33        /// See [Compression Dictionary Transport](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Compression_dictionary_transport).
34        ///
35        /// Experimental directive still in Draft!
36        DCBrotli => "dcb",
37        /// A format that uses the [Dictionary-Compressed Zstandard algorithm](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-compression-dictionary#name-dictionary-compressed-zstan).
38        /// See [Compression Dictionary Transport](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Compression_dictionary_transport).
39        ///
40        /// Experimental directive still in Draft!
41        DCZStandard => "dcz",
42    }
43}
44
45derive_non_empty_flat_csv_header! {
46    #[header(name = CONTENT_ENCODING, sep = Comma)]
47    #[derive(Clone, Debug)]
48    /// `Content-Encoding` header, defined in
49    /// [RFC7231](https://datatracker.ietf.org/doc/html/rfc7231#section-3.1.2.2)
50    ///
51    /// The `Content-Encoding` header field indicates what content codings
52    /// have been applied to the representation, beyond those inherent in the
53    /// media type, and thus what decoding mechanisms have to be applied in
54    /// order to obtain data in the media type referenced by the Content-Type
55    /// header field.  Content-Encoding is primarily used to allow a
56    /// representation's data to be compressed without losing the identity of
57    /// its underlying media type.
58    ///
59    /// # ABNF
60    ///
61    /// ```text
62    /// Content-Encoding = 1#content-coding
63    /// ```
64    ///
65    /// # Example values
66    ///
67    /// * `gzip`
68    /// * `br`
69    /// * `zstd`
70    /// * `deflate, gzip`
71    ///
72    /// # Examples
73    ///
74    /// ```
75    /// use rama_http_headers::ContentEncoding;
76    ///
77    /// let content_enc = ContentEncoding::gzip();
78    /// ```
79    pub struct ContentEncoding(pub NonEmptyVec<ContentEncodingDirective>);
80}
81
82impl ContentEncoding {
83    /// Create a new [`ContentEncoding`] header with multiple directives.
84    ///
85    /// Meaning the content is encoded with all given directives and in order.
86    #[inline]
87    #[must_use]
88    pub fn new_multi(directives: NonEmptyVec<ContentEncodingDirective>) -> Self {
89        Self(directives)
90    }
91
92    /// A constructor to easily create a `Content-Encoding: gzip` header.
93    #[inline]
94    #[must_use]
95    pub fn gzip() -> Self {
96        Self(NonEmptyVec::new(ContentEncodingDirective::Gzip))
97    }
98
99    /// A constructor to easily create a `Content-Encoding: br` header.
100    #[inline]
101    #[must_use]
102    pub fn brotli() -> Self {
103        Self(NonEmptyVec::new(ContentEncodingDirective::Brotli))
104    }
105
106    /// A constructor to easily create a `Content-Encoding: zstd` header.
107    #[inline]
108    #[must_use]
109    pub fn zstd() -> Self {
110        Self(NonEmptyVec::new(ContentEncodingDirective::ZStandard))
111    }
112
113    /// Check if this header contains a given "coding".
114    ///
115    /// This can be used with these argument types:
116    ///
117    /// - `&str`
118    ///
119    /// # Example
120    ///
121    /// ```
122    /// use rama_http_headers::ContentEncoding;
123    ///
124    /// let content_enc = ContentEncoding::gzip();
125    ///
126    /// assert!(content_enc.contains_directive("gzip"));
127    /// assert!(!content_enc.contains_directive("br"));
128    /// ```
129    #[expect(clippy::needless_pass_by_value)]
130    pub fn contains_directive(&self, coding: impl CmpCoding) -> bool {
131        self.0.iter().any(|other| coding.cmp_coding(other))
132    }
133}
134
135mod sealed {
136    use super::ContentEncodingDirective;
137
138    pub trait CmpCoding: Sealed {}
139
140    pub trait Sealed {
141        fn cmp_coding(&self, other: &ContentEncodingDirective) -> bool;
142    }
143
144    impl CmpCoding for &str {}
145
146    impl Sealed for &str {
147        #[inline(always)]
148        fn cmp_coding(&self, other: &ContentEncodingDirective) -> bool {
149            self.trim().eq_ignore_ascii_case(other.as_str())
150        }
151    }
152
153    impl CmpCoding for ContentEncodingDirective {}
154
155    impl Sealed for ContentEncodingDirective {
156        fn cmp_coding(&self, other: &ContentEncodingDirective) -> bool {
157            self.eq(other)
158        }
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::super::{test_decode, test_encode};
165    use super::*;
166    use rama_utils::collections::non_empty_vec;
167
168    #[test]
169    fn decode_header_single() {
170        let ContentEncoding(directives) = test_decode(&["deflate, gzip"]).unwrap();
171
172        assert_eq!(directives.len(), 2);
173        assert_eq!(directives[0], ContentEncodingDirective::Deflate);
174        assert_eq!(directives[1], ContentEncodingDirective::Gzip);
175
176        let header = ContentEncoding(directives);
177
178        assert!(header.contains_directive(ContentEncodingDirective::Deflate));
179        assert!(header.contains_directive(ContentEncodingDirective::Gzip));
180        assert!(!header.contains_directive(ContentEncodingDirective::Brotli));
181    }
182
183    #[test]
184    fn decode_header_multi() {
185        let ContentEncoding(directives) = test_decode(&["deflate, gzip", "compress"]).unwrap();
186
187        assert_eq!(directives.len(), 3);
188        assert_eq!(directives[0], ContentEncodingDirective::Deflate);
189        assert_eq!(directives[1], ContentEncodingDirective::Gzip);
190        assert_eq!(directives[2], ContentEncodingDirective::Compress);
191
192        let header = ContentEncoding(directives);
193
194        assert!(header.contains_directive(ContentEncodingDirective::Deflate));
195        assert!(header.contains_directive(ContentEncodingDirective::Gzip));
196        assert!(header.contains_directive(ContentEncodingDirective::Compress));
197        assert!(!header.contains_directive(ContentEncodingDirective::Brotli));
198    }
199
200    #[test]
201    fn encode_single() {
202        let allow = ContentEncoding::new(ContentEncodingDirective::Brotli);
203
204        let headers = test_encode(allow);
205        assert_eq!(headers["content-encoding"], "br");
206    }
207
208    #[test]
209    fn encode_multi() {
210        let allow = ContentEncoding::new_multi(non_empty_vec![
211            ContentEncodingDirective::Deflate,
212            ContentEncodingDirective::Gzip
213        ]);
214
215        let headers = test_encode(allow);
216        assert_eq!(headers["content-encoding"], "deflate, gzip");
217    }
218
219    #[test]
220    fn decode_with_empty_header_value() {
221        let ContentEncoding(directives) = test_decode(&[""]).unwrap();
222
223        assert_eq!(directives.len(), 1);
224        assert_eq!(directives[0], ContentEncodingDirective::Unknown("".into()));
225    }
226
227    #[test]
228    fn decode_with_no_headers() {
229        assert!(test_decode::<ContentEncoding>(&[]).is_none());
230    }
231
232    #[test]
233    fn decode_header_unknown_directive() {
234        let ContentEncoding(directives) = test_decode(&["foobar"]).unwrap();
235
236        assert_eq!(directives.len(), 1);
237        assert_eq!(
238            directives[0],
239            ContentEncodingDirective::Unknown("foobar".into())
240        );
241    }
242}