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}