codecs/decoding/
decompression.rs1use std::{
10 fmt::Debug,
11 io::{self, Cursor},
12 path::PathBuf,
13 sync::Arc,
14};
15
16use vector_common::decompression::{CappedDecoder, DecoderDictionary};
17use vector_config::configurable_component;
18
19#[configurable_component]
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22#[serde(rename_all = "lowercase")]
23pub enum DecompressionAlgorithm {
24 Gzip,
28
29 Zlib,
33
34 Zstd,
38}
39
40impl DecompressionAlgorithm {
41 const fn as_str(self) -> &'static str {
42 match self {
43 Self::Gzip => "gzip",
44 Self::Zlib => "zlib",
45 Self::Zstd => "zstd",
46 }
47 }
48}
49
50#[configurable_component]
58#[derive(Clone, Debug)]
59pub struct DecompressionConfig {
60 pub algorithm: DecompressionAlgorithm,
62
63 #[serde(default, skip_serializing_if = "vector_core::serde::is_default")]
68 #[configurable(metadata(docs::examples = "/etc/vector/compression.dict"))]
69 pub dictionary_path: Option<PathBuf>,
70}
71
72impl DecompressionConfig {
73 pub fn build(&self) -> vector_common::Result<Decompressor> {
83 let dictionary = match (&self.dictionary_path, self.algorithm) {
84 (None, _) => None,
85 (Some(path), DecompressionAlgorithm::Zstd) => {
86 let contents = std::fs::read(path).map_err(|error| {
87 format!(
88 "Failed to read decompression dictionary file {}: {error}.",
89 path.display()
90 )
91 })?;
92 Some(Arc::new(DecoderDictionary::copy(&contents)))
93 }
94 (Some(_), algorithm) => {
95 return Err(format!(
96 "`dictionary_path` is not supported with the `{}` algorithm, only `zstd`.",
97 algorithm.as_str()
98 )
99 .into());
100 }
101 };
102
103 Ok(Decompressor {
104 algorithm: self.algorithm,
105 dictionary,
106 })
107 }
108}
109
110#[derive(Clone)]
112pub struct Decompressor {
113 algorithm: DecompressionAlgorithm,
114 dictionary: Option<Arc<DecoderDictionary<'static>>>,
115}
116
117impl Debug for Decompressor {
118 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119 f.debug_struct("Decompressor")
120 .field("algorithm", &self.algorithm)
121 .field("dictionary", &self.dictionary.as_ref().map(|_| "<dict>"))
122 .finish()
123 }
124}
125
126impl Decompressor {
127 pub fn decompress(&self, payload: &[u8]) -> io::Result<Vec<u8>> {
138 let reader = Cursor::new(payload);
139 match (self.algorithm, &self.dictionary) {
140 (DecompressionAlgorithm::Gzip, _) => CappedDecoder::gzip(reader).decompress(),
141 (DecompressionAlgorithm::Zlib, _) => CappedDecoder::zlib(reader).decompress(),
142 (DecompressionAlgorithm::Zstd, None) => CappedDecoder::zstd(reader)?.decompress(),
143 (DecompressionAlgorithm::Zstd, Some(dictionary)) => {
144 CappedDecoder::zstd_with_dictionary(reader, dictionary)?.decompress()
145 }
146 }
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use std::io::Write;
153
154 use flate2::write::{GzEncoder, ZlibEncoder};
155
156 use super::*;
157
158 fn gzip_compress(data: &[u8]) -> Vec<u8> {
159 let mut encoder = GzEncoder::new(Vec::new(), flate2::Compression::default());
160 encoder.write_all(data).unwrap();
161 encoder.finish().unwrap()
162 }
163
164 fn zlib_compress(data: &[u8]) -> Vec<u8> {
165 let mut encoder = ZlibEncoder::new(Vec::new(), flate2::Compression::default());
166 encoder.write_all(data).unwrap();
167 encoder.finish().unwrap()
168 }
169
170 fn train_dictionary() -> Vec<u8> {
171 let samples: Vec<Vec<u8>> = (0..100)
172 .map(|i| format!(r#"{{"id":{i},"message":"sample event {i}"}}"#).into_bytes())
173 .collect();
174 zstd::dict::from_samples(&samples, 4 * 1024).expect("dictionary training failed")
175 }
176
177 fn write_temp_dictionary(contents: &[u8]) -> tempfile::NamedTempFile {
178 let file = tempfile::NamedTempFile::new().unwrap();
179 std::fs::write(file.path(), contents).unwrap();
180 file
181 }
182
183 fn config(
184 algorithm: DecompressionAlgorithm,
185 dictionary_path: Option<PathBuf>,
186 ) -> DecompressionConfig {
187 DecompressionConfig {
188 algorithm,
189 dictionary_path,
190 }
191 }
192
193 #[test]
194 fn deserializes_config() {
195 let config: DecompressionConfig = toml::from_str(
196 r#"
197 algorithm = "zstd"
198 dictionary_path = "/etc/vector/compression.dict"
199 "#,
200 )
201 .unwrap();
202 assert_eq!(config.algorithm, DecompressionAlgorithm::Zstd);
203 assert_eq!(
204 config.dictionary_path,
205 Some(PathBuf::from("/etc/vector/compression.dict"))
206 );
207
208 let config: DecompressionConfig = toml::from_str(r#"algorithm = "gzip""#).unwrap();
209 assert_eq!(config.algorithm, DecompressionAlgorithm::Gzip);
210 assert_eq!(config.dictionary_path, None);
211 }
212
213 #[test]
214 fn gzip_round_trip() {
215 let decompressor = config(DecompressionAlgorithm::Gzip, None).build().unwrap();
216 let payload = b"hello gzip";
217 assert_eq!(
218 decompressor.decompress(&gzip_compress(payload)).unwrap(),
219 payload
220 );
221 }
222
223 #[test]
224 fn zlib_round_trip() {
225 let decompressor = config(DecompressionAlgorithm::Zlib, None).build().unwrap();
226 let payload = b"hello zlib";
227 assert_eq!(
228 decompressor.decompress(&zlib_compress(payload)).unwrap(),
229 payload
230 );
231 }
232
233 #[test]
234 fn zstd_round_trip() {
235 let decompressor = config(DecompressionAlgorithm::Zstd, None).build().unwrap();
236 let payload = b"hello zstd";
237 let compressed = zstd::stream::encode_all(&payload[..], 3).unwrap();
238 assert_eq!(decompressor.decompress(&compressed).unwrap(), payload);
239 }
240
241 #[test]
242 fn zstd_dictionary_round_trip() {
243 let dictionary = train_dictionary();
244 let dictionary_file = write_temp_dictionary(&dictionary);
245 let decompressor = config(
246 DecompressionAlgorithm::Zstd,
247 Some(dictionary_file.path().to_path_buf()),
248 )
249 .build()
250 .unwrap();
251
252 let payload = br#"{"id":123,"message":"hello dictionary"}"#;
253 let compressed = zstd::bulk::Compressor::with_dictionary(3, &dictionary)
254 .unwrap()
255 .compress(payload)
256 .unwrap();
257
258 assert_eq!(decompressor.decompress(&compressed).unwrap(), payload);
259 }
260
261 #[test]
262 fn invalid_payload_is_an_error() {
263 let decompressor = config(DecompressionAlgorithm::Zstd, None).build().unwrap();
264 assert!(decompressor.decompress(b"not zstd data").is_err());
265 }
266
267 #[test]
268 fn build_rejects_dictionary_with_non_zstd_algorithm() {
269 let error = config(
270 DecompressionAlgorithm::Gzip,
271 Some(PathBuf::from("/etc/vector/compression.dict")),
272 )
273 .build()
274 .expect_err("dictionary with gzip should be rejected");
275 assert!(error.to_string().contains("only `zstd`"));
276 }
277
278 #[test]
279 fn build_rejects_missing_dictionary_file() {
280 let error = config(
281 DecompressionAlgorithm::Zstd,
282 Some(PathBuf::from("/nonexistent/compression.dict")),
283 )
284 .build()
285 .expect_err("missing dictionary file should be rejected");
286 assert!(error.to_string().contains("Failed to read"));
287 }
288}