1use std::io::{Read, Write};
23
24use bytes::Bytes;
25use flate2::read::{GzDecoder, ZlibDecoder};
26use flate2::write::{GzEncoder, ZlibEncoder};
27use flate2::Compression;
28
29const BROTLI_BUFFER_SIZE: usize = 4096;
32
33const BROTLI_LGWIN: u32 = 22;
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum ContentEncoding {
40 Gzip,
41 Deflate,
43 Brotli,
44}
45
46impl ContentEncoding {
47 pub fn parse(value: &str) -> Result<Option<Self>, String> {
55 let normalized = value.trim().to_ascii_lowercase();
56 match normalized.as_str() {
57 "" | "identity" => Ok(None),
58 "gzip" => Ok(Some(Self::Gzip)),
59 "deflate" => Ok(Some(Self::Deflate)),
60 "br" => Ok(Some(Self::Brotli)),
61 other => Err(format!("unsupported content-encoding: {}", other)),
62 }
63 }
64
65 pub fn header_value(&self) -> &'static str {
68 match self {
69 Self::Gzip => "gzip",
70 Self::Deflate => "deflate",
71 Self::Brotli => "br",
72 }
73 }
74}
75
76pub fn decode(encoding: &ContentEncoding, body: &Bytes) -> Result<Bytes, String> {
79 let mut out = Vec::new();
80 match encoding {
81 ContentEncoding::Gzip => {
82 GzDecoder::new(body.as_ref())
83 .read_to_end(&mut out)
84 .map_err(|e| format!("gzip decode error: {}", e))?;
85 }
86 ContentEncoding::Deflate => {
87 ZlibDecoder::new(body.as_ref())
88 .read_to_end(&mut out)
89 .map_err(|e| format!("deflate decode error: {}", e))?;
90 }
91 ContentEncoding::Brotli => {
92 brotli::Decompressor::new(body.as_ref(), BROTLI_BUFFER_SIZE)
93 .read_to_end(&mut out)
94 .map_err(|e| format!("brotli decode error: {}", e))?;
95 }
96 }
97 Ok(Bytes::from(out))
98}
99
100pub fn encode(encoding: &ContentEncoding, body: &Bytes, level: u32) -> Result<Bytes, String> {
104 match encoding {
105 ContentEncoding::Gzip => {
106 let mut encoder = GzEncoder::new(Vec::new(), Compression::new(level.min(9)));
107 encoder
108 .write_all(body)
109 .and_then(|_| encoder.finish())
110 .map(Bytes::from)
111 .map_err(|e| format!("gzip encode error: {}", e))
112 }
113 ContentEncoding::Deflate => {
114 let mut encoder = ZlibEncoder::new(Vec::new(), Compression::new(level.min(9)));
115 encoder
116 .write_all(body)
117 .and_then(|_| encoder.finish())
118 .map(Bytes::from)
119 .map_err(|e| format!("deflate encode error: {}", e))
120 }
121 ContentEncoding::Brotli => {
122 let mut out = Vec::new();
123 {
124 let mut writer = brotli::CompressorWriter::new(
125 &mut out,
126 BROTLI_BUFFER_SIZE,
127 level.min(11),
128 BROTLI_LGWIN,
129 );
130 writer
131 .write_all(body)
132 .and_then(|_| writer.flush())
133 .map_err(|e| format!("brotli encode error: {}", e))?;
134 }
136 Ok(Bytes::from(out))
137 }
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 const SAMPLE: &[u8] = b"the quick brown fox jumps over the lazy dog, \
146 repeated so compression has something to chew on: \
147 the quick brown fox jumps over the lazy dog";
148
149 #[test]
150 fn test_content_codec_round_trip_gzip() {
151 let body = Bytes::from_static(SAMPLE);
152 let encoded = encode(&ContentEncoding::Gzip, &body, 6).unwrap();
153 assert_ne!(encoded, body);
154 let decoded = decode(&ContentEncoding::Gzip, &encoded).unwrap();
155 assert_eq!(decoded, body);
156 }
157
158 #[test]
159 fn test_content_codec_round_trip_deflate() {
160 let body = Bytes::from_static(SAMPLE);
161 let encoded = encode(&ContentEncoding::Deflate, &body, 6).unwrap();
162 assert_ne!(encoded, body);
163 let decoded = decode(&ContentEncoding::Deflate, &encoded).unwrap();
164 assert_eq!(decoded, body);
165 }
166
167 #[test]
168 fn test_content_codec_round_trip_brotli() {
169 let body = Bytes::from_static(SAMPLE);
170 let encoded = encode(&ContentEncoding::Brotli, &body, 6).unwrap();
171 assert_ne!(encoded, body);
172 let decoded = decode(&ContentEncoding::Brotli, &encoded).unwrap();
173 assert_eq!(decoded, body);
174 }
175
176 #[test]
177 fn test_content_codec_round_trip_empty_body() {
178 for encoding in [
179 ContentEncoding::Gzip,
180 ContentEncoding::Deflate,
181 ContentEncoding::Brotli,
182 ] {
183 let encoded = encode(&encoding, &Bytes::new(), 6).unwrap();
184 let decoded = decode(&encoding, &encoded).unwrap();
185 assert!(decoded.is_empty(), "{:?} empty round trip", encoding);
186 }
187 }
188
189 #[test]
190 fn test_content_codec_decode_corrupt_errors() {
191 let garbage = Bytes::from_static(b"\x00\x01definitely not a compressed stream\xff\xfe");
192 for encoding in [
193 ContentEncoding::Gzip,
194 ContentEncoding::Deflate,
195 ContentEncoding::Brotli,
196 ] {
197 let result = decode(&encoding, &garbage);
198 assert!(result.is_err(), "{:?} should reject corrupt data", encoding);
199 }
200 }
201
202 #[test]
203 fn test_content_codec_decode_truncated_gzip_errors() {
204 let body = Bytes::from_static(SAMPLE);
205 let encoded = encode(&ContentEncoding::Gzip, &body, 6).unwrap();
206 let truncated = encoded.slice(..encoded.len() / 2);
207 assert!(decode(&ContentEncoding::Gzip, &truncated).is_err());
208 }
209
210 #[test]
211 fn test_content_codec_parse() {
212 assert_eq!(
213 ContentEncoding::parse("gzip"),
214 Ok(Some(ContentEncoding::Gzip))
215 );
216 assert_eq!(
217 ContentEncoding::parse("GZIP"),
218 Ok(Some(ContentEncoding::Gzip))
219 );
220 assert_eq!(
221 ContentEncoding::parse(" gzip "),
222 Ok(Some(ContentEncoding::Gzip))
223 );
224 assert_eq!(
225 ContentEncoding::parse("br"),
226 Ok(Some(ContentEncoding::Brotli))
227 );
228 assert_eq!(
229 ContentEncoding::parse("deflate"),
230 Ok(Some(ContentEncoding::Deflate))
231 );
232 assert_eq!(ContentEncoding::parse("identity"), Ok(None));
233 assert_eq!(ContentEncoding::parse("Identity"), Ok(None));
234 assert_eq!(ContentEncoding::parse(""), Ok(None));
235 assert_eq!(ContentEncoding::parse(" "), Ok(None));
236 assert!(ContentEncoding::parse("x-unknown").is_err());
237 assert!(ContentEncoding::parse("gzip, br").is_err());
239 }
240
241 #[test]
242 fn test_content_codec_header_value() {
243 assert_eq!(ContentEncoding::Gzip.header_value(), "gzip");
244 assert_eq!(ContentEncoding::Deflate.header_value(), "deflate");
245 assert_eq!(ContentEncoding::Brotli.header_value(), "br");
246 }
247
248 #[test]
249 fn test_content_codec_level_clamped() {
250 let body = Bytes::from_static(SAMPLE);
251 for encoding in [
252 ContentEncoding::Gzip,
253 ContentEncoding::Deflate,
254 ContentEncoding::Brotli,
255 ] {
256 let encoded = encode(&encoding, &body, 99).unwrap();
258 let decoded = decode(&encoding, &encoded).unwrap();
259 assert_eq!(decoded, body, "{:?} with clamped level", encoding);
260 }
261 }
262}