1use crate::{Bytes, Error, Result};
30
31pub const MAX_INFLATED_BYTES: u64 = 1 << 30;
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
39#[non_exhaustive]
40pub enum ContentEncoding {
41 Gzip,
43}
44
45impl ContentEncoding {
46 #[must_use]
48 pub const fn as_str(self) -> &'static str {
49 match self {
50 ContentEncoding::Gzip => "gzip",
51 }
52 }
53
54 pub fn parse(token: &str) -> Result<Option<Self>> {
58 let t = token.trim();
59 if t.is_empty() || t.eq_ignore_ascii_case("identity") {
60 Ok(None)
61 } else if t.eq_ignore_ascii_case("gzip") {
62 Ok(Some(ContentEncoding::Gzip))
63 } else {
64 Err(unsupported(t))
65 }
66 }
67}
68
69impl std::fmt::Display for ContentEncoding {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 f.write_str(self.as_str())
72 }
73}
74
75fn unsupported(token: &str) -> Error {
76 Error::UnsupportedEncoding { encoding: token.chars().take(64).collect() }
78}
79
80#[cfg_attr(not(feature = "gzip"), allow(clippy::unnecessary_wraps))]
82pub(crate) fn encode(encoding: ContentEncoding, body: &[u8]) -> Result<Bytes> {
83 #[cfg(feature = "gzip")]
84 {
85 use std::io::Write;
86 let ContentEncoding::Gzip = encoding;
87 let mut enc = flate2::write::GzEncoder::new(
91 Vec::with_capacity(body.len() / 4 + 64),
92 flate2::Compression::default(),
93 );
94 enc.write_all(body)?;
95 Ok(Bytes::from(enc.finish()?))
96 }
97 #[cfg(not(feature = "gzip"))]
98 {
99 let _ = body;
100 Err(unsupported(encoding.as_str()))
101 }
102}
103
104pub(crate) fn decode(encoding: ContentEncoding, stored: &[u8], limit: u64) -> Result<Bytes> {
108 #[cfg(feature = "gzip")]
109 {
110 use std::io::Read;
111 let ContentEncoding::Gzip = encoding;
112 let isize_hint = stored
116 .len()
117 .checked_sub(4)
118 .and_then(|at| stored.get(at..))
119 .map_or(0, |t| u64::from(u32::from_le_bytes([t[0], t[1], t[2], t[3]])));
120 let ceiling = (stored.len() as u64).saturating_mul(1032);
121 let hint = isize_hint.min(ceiling).min(limit);
122 let mut out = Vec::with_capacity(usize::try_from(hint).unwrap_or(0));
123 let mut dec = flate2::read::MultiGzDecoder::new(stored).take(limit.saturating_add(1));
127 dec.read_to_end(&mut out).map_err(|e| Error::corrupt(format!("gzip stream: {e}")))?;
128 if out.len() as u64 > limit {
129 return Err(Error::TooLarge { limit });
130 }
131 Ok(Bytes::from(out))
132 }
133 #[cfg(not(feature = "gzip"))]
134 {
135 let _ = (stored, limit);
136 Err(unsupported(encoding.as_str()))
137 }
138}
139
140#[doc(hidden)]
143pub fn fuzz(stored: &[u8]) {
144 const LIMIT: u64 = 1 << 20;
145 if let Ok(b) = decode(ContentEncoding::Gzip, stored, LIMIT) {
146 assert!(b.len() as u64 <= LIMIT);
147 if let Ok(again) = encode(ContentEncoding::Gzip, &b) {
148 assert_eq!(decode(ContentEncoding::Gzip, &again, LIMIT).ok().as_ref(), Some(&b));
149 }
150 }
151 let _ = ContentEncoding::parse(&String::from_utf8_lossy(stored));
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn tokens_parse_and_print() {
160 assert_eq!(ContentEncoding::parse("gzip").unwrap(), Some(ContentEncoding::Gzip));
161 assert_eq!(ContentEncoding::parse(" GZip ").unwrap(), Some(ContentEncoding::Gzip));
162 assert_eq!(ContentEncoding::parse("identity").unwrap(), None);
163 assert_eq!(ContentEncoding::parse("").unwrap(), None);
164 for bad in ["br", "deflate", "gzip, br", "x-gzip"] {
165 assert!(
166 matches!(ContentEncoding::parse(bad), Err(Error::UnsupportedEncoding { .. })),
167 "{bad}"
168 );
169 }
170 assert_eq!(ContentEncoding::Gzip.to_string(), "gzip");
171 let long = "z".repeat(10_000);
172 let Err(Error::UnsupportedEncoding { encoding }) = ContentEncoding::parse(&long) else {
173 panic!("unknown token accepted")
174 };
175 assert_eq!(encoding.len(), 64, "a hostile token is bounded in the error");
176 }
177
178 #[cfg(feature = "gzip")]
179 #[test]
180 fn round_trip_bounds_and_damage() {
181 let body: Vec<u8> = b"the same line, again and again\n".repeat(1000);
182 let gz = encode(ContentEncoding::Gzip, &body).unwrap();
183 assert!(gz.len() * 10 < body.len(), "{} -> {}", body.len(), gz.len());
184 assert_eq!(gz, encode(ContentEncoding::Gzip, &body).unwrap(), "deterministic");
185 assert_eq!(decode(ContentEncoding::Gzip, &gz, body.len() as u64).unwrap(), body);
186 assert!(matches!(
187 decode(ContentEncoding::Gzip, &gz, body.len() as u64 - 1),
188 Err(Error::TooLarge { .. })
189 ));
190 assert!(matches!(
191 decode(ContentEncoding::Gzip, &gz[..gz.len() - 3], u64::MAX),
192 Err(Error::Corrupt { .. })
193 ));
194 let mut bad = gz.to_vec();
195 let mid = bad.len() / 2;
196 bad[mid] ^= 0x55;
197 assert!(decode(ContentEncoding::Gzip, &bad, u64::MAX).is_err());
198 assert!(matches!(decode(ContentEncoding::Gzip, b"", 10), Err(Error::Corrupt { .. })));
199 assert!(matches!(
200 decode(ContentEncoding::Gzip, b"plain text", 10),
201 Err(Error::Corrupt { .. })
202 ));
203 let mut two = encode(ContentEncoding::Gzip, b"ab").unwrap().to_vec();
205 two.extend_from_slice(&encode(ContentEncoding::Gzip, b"cd").unwrap());
206 assert_eq!(decode(ContentEncoding::Gzip, &two, 10).unwrap(), &b"abcd"[..]);
207 let empty = encode(ContentEncoding::Gzip, b"").unwrap();
209 assert!(decode(ContentEncoding::Gzip, &empty, 0).unwrap().is_empty());
210 }
211
212 #[cfg(not(feature = "gzip"))]
213 #[test]
214 fn without_the_codec_encoded_io_is_refused() {
215 assert!(matches!(
216 encode(ContentEncoding::Gzip, b"x"),
217 Err(Error::UnsupportedEncoding { .. })
218 ));
219 assert!(matches!(
220 decode(ContentEncoding::Gzip, b"x", 1),
221 Err(Error::UnsupportedEncoding { .. })
222 ));
223 }
224}