Skip to main content

apache_avro/
codec.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Logic for all supported compression codecs in Avro.
19
20use crate::{AvroResult, Error, error::Details, types::Value};
21use strum::{EnumIter, EnumString, IntoStaticStr};
22
23/// Settings for the `Deflate` codec.
24#[derive(Clone, Copy, Eq, PartialEq, Debug)]
25pub struct DeflateSettings {
26    compression_level: miniz_oxide::deflate::CompressionLevel,
27}
28
29impl DeflateSettings {
30    pub fn new(compression_level: miniz_oxide::deflate::CompressionLevel) -> Self {
31        DeflateSettings { compression_level }
32    }
33
34    /// Get the compression level as a `u8`, note that this means the [`miniz_oxide::deflate::CompressionLevel::DefaultCompression`] variant
35    /// will appear as `255`, this is normalized by the [`miniz_oxide`] crate later.
36    pub fn compression_level(&self) -> u8 {
37        self.compression_level as u8
38    }
39}
40
41impl Default for DeflateSettings {
42    /// Default compression level is `miniz_oxide::deflate::CompressionLevel::DefaultCompression`.
43    fn default() -> Self {
44        Self::new(miniz_oxide::deflate::CompressionLevel::DefaultCompression)
45    }
46}
47
48/// The compression codec used to compress blocks.
49#[derive(Clone, Copy, Debug, Eq, PartialEq, EnumIter, EnumString, IntoStaticStr)]
50#[strum(serialize_all = "kebab_case")]
51pub enum Codec {
52    /// The `Null` codec simply passes through data uncompressed.
53    Null,
54    /// The `Deflate` codec writes the data block using the deflate algorithm
55    /// as specified in RFC 1951, and typically implemented using the zlib library.
56    /// Note that this format (unlike the "zlib format" in RFC 1950) does not have a checksum.
57    Deflate(DeflateSettings),
58    #[cfg(feature = "snappy")]
59    /// The `Snappy` codec uses Google's [Snappy](http://google.github.io/snappy/)
60    /// compression library. Each compressed block is followed by the 4-byte, big-endian
61    /// CRC32 checksum of the uncompressed data in the block.
62    Snappy,
63    #[cfg(feature = "zstandard")]
64    /// The `Zstandard` codec uses Facebook's [Zstandard](https://facebook.github.io/zstd/)
65    Zstandard(zstandard::ZstandardSettings),
66    #[cfg(feature = "bzip")]
67    /// The `BZip2` codec uses [BZip2](https://sourceware.org/bzip2/)
68    /// compression library.
69    Bzip2(bzip::Bzip2Settings),
70    #[cfg(feature = "xz")]
71    /// The `Xz` codec uses [Xz utils](https://tukaani.org/xz/)
72    /// compression library.
73    Xz(xz::XzSettings),
74}
75
76impl From<Codec> for Value {
77    fn from(value: Codec) -> Self {
78        Self::Bytes(<&str>::from(value).as_bytes().to_vec())
79    }
80}
81
82impl Codec {
83    /// Compress a stream of bytes in-place.
84    pub fn compress(self, stream: &mut Vec<u8>) -> AvroResult<()> {
85        match self {
86            Codec::Null => (),
87            Codec::Deflate(settings) => {
88                let compressed =
89                    miniz_oxide::deflate::compress_to_vec(stream, settings.compression_level());
90                *stream = compressed;
91            }
92            #[cfg(feature = "snappy")]
93            Codec::Snappy => {
94                let mut encoded: Vec<u8> = vec![0; snap::raw::max_compress_len(stream.len())];
95                let compressed_size = snap::raw::Encoder::new()
96                    .compress(&stream[..], &mut encoded[..])
97                    .map_err(Details::SnappyCompress)?;
98
99                let mut hasher = crc32fast::Hasher::new();
100                hasher.update(&stream[..]);
101                let checksum = hasher.finalize();
102                let checksum_as_bytes = checksum.to_be_bytes();
103                let checksum_len = checksum_as_bytes.len();
104                encoded.truncate(compressed_size + checksum_len);
105                encoded[compressed_size..].copy_from_slice(&checksum_as_bytes);
106
107                *stream = encoded;
108            }
109            #[cfg(feature = "zstandard")]
110            Codec::Zstandard(settings) => {
111                use std::io::Write;
112                let mut encoder = zstd::Encoder::new(Vec::new(), settings.compression_level as i32)
113                    .map_err(Details::ZstdCompress)?;
114                encoder.write_all(stream).map_err(Details::ZstdCompress)?;
115                *stream = encoder.finish().map_err(Details::ZstdCompress)?;
116            }
117            #[cfg(feature = "bzip")]
118            Codec::Bzip2(settings) => {
119                use bzip2::read::BzEncoder;
120                use std::io::Read;
121
122                let mut encoder = BzEncoder::new(&stream[..], settings.compression());
123                let mut buffer = Vec::new();
124                encoder
125                    .read_to_end(&mut buffer)
126                    .unwrap_or_else(|_| unreachable!("No I/O errors possible with Vec<u8>"));
127                *stream = buffer;
128            }
129            #[cfg(feature = "xz")]
130            Codec::Xz(settings) => {
131                use liblzma::read::XzEncoder;
132                use std::io::Read;
133
134                let mut encoder = XzEncoder::new(&stream[..], settings.compression_level as u32);
135                let mut buffer = Vec::new();
136                encoder
137                    .read_to_end(&mut buffer)
138                    .unwrap_or_else(|_| unreachable!("No I/O errors possible with Vec<u8>"));
139                *stream = buffer;
140            }
141        };
142
143        Ok(())
144    }
145
146    /// Decompress a stream of bytes in-place.
147    pub fn decompress(self, stream: &mut Vec<u8>) -> AvroResult<()> {
148        // Cap the decompressed output at the configured allocation budget so a
149        // small compressed block cannot inflate to an enormous buffer (a
150        // "decompression bomb") and exhaust memory.
151        let max_bytes =
152            crate::util::max_allocation_bytes(crate::util::DEFAULT_MAX_ALLOCATION_BYTES);
153        *stream = match self {
154            Codec::Null => return Ok(()),
155            Codec::Deflate(_settings) => miniz_oxide::inflate::decompress_to_vec_with_limit(stream, max_bytes).map_err(|e| {
156                use std::io::ErrorKind;
157                use miniz_oxide::inflate::TINFLStatus;
158
159                let details = match e.status {
160                    TINFLStatus::FailedCannotMakeProgress | TINFLStatus::NeedsMoreInput => Details::DeflateDecompress(ErrorKind::UnexpectedEof.into()),
161                    TINFLStatus::Adler32Mismatch | TINFLStatus::Failed | TINFLStatus::BadParam => Details::DeflateDecompress(ErrorKind::InvalidData.into()),
162                    TINFLStatus::Done => Details::DeflateDecompress(std::io::Error::other("Unexpected error: miniz_oxide reported an error with a success status. Please report this to avro-rs developers.")),
163                    // Output is larger than max allocation allowed
164                    TINFLStatus::HasMoreOutput => Details::MemoryAllocation {
165                        desired: None,
166                        maximum: max_bytes,
167                    },
168                    other => Details::DeflateDecompress(std::io::Error::other(format!("Unexpected error: {other:?}")))
169                };
170                Error::new(details)
171            })?,
172            #[cfg(feature = "snappy")]
173            Codec::Snappy => {
174                // The block ends with a 4-byte CRC32; a truncated/corrupt block
175                // shorter than that must error rather than underflow the slice.
176                let data_end = stream
177                    .len()
178                    .checked_sub(4)
179                    .ok_or(Details::BadSnappyLength(stream.len()))?;
180                let decompressed_size = snap::raw::decompress_len(&stream[..data_end])
181                    .map_err(Details::GetSnappyDecompressLen)?;
182                // The decompressed size is taken from the (untrusted) block
183                // header, so bound it before allocating for it.
184                let decompressed_size = crate::util::safe_len(decompressed_size)?;
185                let mut decoded = vec![0; decompressed_size];
186                snap::raw::Decoder::new()
187                    .decompress(&stream[..data_end], &mut decoded[..])
188                    .map_err(Details::SnappyDecompress)?;
189
190                let mut last_four: [u8; 4] = [0; 4];
191                last_four.copy_from_slice(&stream[data_end..]);
192                let expected: u32 = u32::from_be_bytes(last_four);
193
194                let mut hasher = crc32fast::Hasher::new();
195                hasher.update(&decoded);
196                let actual = hasher.finalize();
197
198                if expected != actual {
199                    return Err(Details::SnappyCrc32{expected, actual}.into());
200                }
201                decoded
202            }
203            #[cfg(feature = "zstandard")]
204            Codec::Zstandard(_settings) => {
205                use std::io::{BufReader, Read};
206                use zstd::zstd_safe;
207
208                let mut decoded = Vec::new();
209                let buffer_size = zstd_safe::DCtx::in_size();
210                let buffer = BufReader::with_capacity(buffer_size, &stream[..]);
211                let decoder = zstd::Decoder::new(buffer).map_err(Details::ZstdDecompress)?;
212                // Read one byte past the budget so an output that exactly fills
213                // it is allowed, while a larger (bomb) output is detected.
214                decoder
215                    .take((max_bytes as u64).saturating_add(1))
216                    .read_to_end(&mut decoded)
217                    .map_err(Details::ZstdDecompress)?;
218                if decoded.len() > max_bytes {
219                    return Err(Details::MemoryAllocation { desired: None, maximum: max_bytes }.into());
220                }
221                decoded
222            }
223            #[cfg(feature = "bzip")]
224            Codec::Bzip2(_) => {
225                use bzip2::read::BzDecoder;
226                use std::io::Read;
227
228                let mut decoded = Vec::new();
229                BzDecoder::new(&stream[..])
230                    .take((max_bytes as u64).saturating_add(1))
231                    .read_to_end(&mut decoded)
232                    .map_err(Details::Bzip2Decompress)?;
233                if decoded.len() > max_bytes {
234                    return Err(Details::MemoryAllocation { desired: None, maximum: max_bytes }.into());
235                }
236                decoded
237            }
238            #[cfg(feature = "xz")]
239            Codec::Xz(_) => {
240                use liblzma::read::XzDecoder;
241                use std::io::Read;
242
243                let mut decoded: Vec<u8> = Vec::new();
244                XzDecoder::new(&stream[..])
245                    .take((max_bytes as u64).saturating_add(1))
246                    .read_to_end(&mut decoded)
247                    .map_err(Details::XzDecompress)?;
248                if decoded.len() > max_bytes {
249                    return Err(Details::MemoryAllocation { desired: None, maximum: max_bytes }.into());
250                }
251                decoded
252            }
253        };
254        Ok(())
255    }
256}
257
258#[cfg(feature = "bzip")]
259pub mod bzip {
260    use bzip2::Compression;
261
262    #[derive(Clone, Copy, Eq, PartialEq, Debug)]
263    pub struct Bzip2Settings {
264        pub compression_level: u8,
265    }
266
267    impl Bzip2Settings {
268        pub fn new(compression_level: u8) -> Self {
269            Self { compression_level }
270        }
271
272        pub(crate) fn compression(&self) -> Compression {
273            Compression::new(self.compression_level as u32)
274        }
275    }
276
277    impl Default for Bzip2Settings {
278        fn default() -> Self {
279            Bzip2Settings::new(Compression::best().level() as u8)
280        }
281    }
282}
283
284#[cfg(feature = "zstandard")]
285pub mod zstandard {
286    #[derive(Clone, Copy, Eq, PartialEq, Debug)]
287    pub struct ZstandardSettings {
288        pub compression_level: u8,
289    }
290
291    impl ZstandardSettings {
292        pub fn new(compression_level: u8) -> Self {
293            Self { compression_level }
294        }
295    }
296
297    impl Default for ZstandardSettings {
298        fn default() -> Self {
299            Self::new(0)
300        }
301    }
302}
303
304#[cfg(feature = "xz")]
305pub mod xz {
306    #[derive(Clone, Copy, Eq, PartialEq, Debug)]
307    pub struct XzSettings {
308        pub compression_level: u8,
309    }
310
311    impl XzSettings {
312        pub fn new(compression_level: u8) -> Self {
313            Self { compression_level }
314        }
315    }
316
317    impl Default for XzSettings {
318        fn default() -> Self {
319            XzSettings::new(9)
320        }
321    }
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327    use apache_avro_test_helper::TestResult;
328    use miniz_oxide::deflate::CompressionLevel;
329    use pretty_assertions::{assert_eq, assert_ne};
330
331    const INPUT: &[u8] = b"theanswertolifetheuniverseandeverythingis42theanswertolifetheuniverseandeverythingis4theanswertolifetheuniverseandeverythingis2";
332
333    #[test]
334    fn null_compress_and_decompress() -> TestResult {
335        let codec = Codec::Null;
336        let mut stream = INPUT.to_vec();
337        codec.compress(&mut stream)?;
338        assert_eq!(INPUT, stream.as_slice());
339        codec.decompress(&mut stream)?;
340        assert_eq!(INPUT, stream.as_slice());
341        Ok(())
342    }
343
344    #[test]
345    fn deflate_compress_and_decompress() -> TestResult {
346        compress_and_decompress(Codec::Deflate(DeflateSettings::new(
347            CompressionLevel::BestCompression,
348        )))
349    }
350
351    #[cfg(feature = "snappy")]
352    #[test]
353    fn snappy_compress_and_decompress() -> TestResult {
354        compress_and_decompress(Codec::Snappy)
355    }
356
357    #[cfg(feature = "snappy")]
358    #[test]
359    fn snappy_decompress_short_block_errors_without_panicking() {
360        // A block shorter than the trailing 4-byte CRC must return an error
361        // rather than underflowing `stream.len() - 4` and panicking.
362        for len in 0..4usize {
363            let mut stream = vec![0u8; len];
364            let result = Codec::Snappy.decompress(&mut stream);
365            assert!(result.is_err(), "len={len} should error, got {result:?}");
366        }
367    }
368
369    #[cfg(feature = "zstandard")]
370    #[test]
371    fn zstd_compress_and_decompress() -> TestResult {
372        compress_and_decompress(Codec::Zstandard(zstandard::ZstandardSettings::default()))
373    }
374
375    #[cfg(feature = "bzip")]
376    #[test]
377    fn bzip_compress_and_decompress() -> TestResult {
378        compress_and_decompress(Codec::Bzip2(bzip::Bzip2Settings::default()))
379    }
380
381    #[cfg(feature = "xz")]
382    #[test]
383    fn xz_compress_and_decompress() -> TestResult {
384        compress_and_decompress(Codec::Xz(xz::XzSettings::default()))
385    }
386
387    fn compress_and_decompress(codec: Codec) -> TestResult {
388        let mut stream = INPUT.to_vec();
389        codec.compress(&mut stream)?;
390        assert_ne!(INPUT, stream.as_slice());
391        assert!(INPUT.len() > stream.len());
392        codec.decompress(&mut stream)?;
393        assert_eq!(INPUT, stream.as_slice());
394        Ok(())
395    }
396
397    #[test]
398    fn codec_to_str() {
399        assert_eq!(<&str>::from(Codec::Null), "null");
400        assert_eq!(
401            <&str>::from(Codec::Deflate(DeflateSettings::default())),
402            "deflate"
403        );
404
405        #[cfg(feature = "snappy")]
406        assert_eq!(<&str>::from(Codec::Snappy), "snappy");
407
408        #[cfg(feature = "zstandard")]
409        assert_eq!(
410            <&str>::from(Codec::Zstandard(zstandard::ZstandardSettings::default())),
411            "zstandard"
412        );
413
414        #[cfg(feature = "bzip")]
415        assert_eq!(
416            <&str>::from(Codec::Bzip2(bzip::Bzip2Settings::default())),
417            "bzip2"
418        );
419
420        #[cfg(feature = "xz")]
421        assert_eq!(<&str>::from(Codec::Xz(xz::XzSettings::default())), "xz");
422    }
423
424    #[test]
425    fn codec_from_str() {
426        use std::str::FromStr;
427
428        assert_eq!(Codec::from_str("null").unwrap(), Codec::Null);
429        assert_eq!(
430            Codec::from_str("deflate").unwrap(),
431            Codec::Deflate(DeflateSettings::default())
432        );
433
434        #[cfg(feature = "snappy")]
435        assert_eq!(Codec::from_str("snappy").unwrap(), Codec::Snappy);
436
437        #[cfg(feature = "zstandard")]
438        assert_eq!(
439            Codec::from_str("zstandard").unwrap(),
440            Codec::Zstandard(zstandard::ZstandardSettings::default())
441        );
442
443        #[cfg(feature = "bzip")]
444        assert_eq!(
445            Codec::from_str("bzip2").unwrap(),
446            Codec::Bzip2(bzip::Bzip2Settings::default())
447        );
448
449        #[cfg(feature = "xz")]
450        assert_eq!(
451            Codec::from_str("xz").unwrap(),
452            Codec::Xz(xz::XzSettings::default())
453        );
454
455        assert!(Codec::from_str("not a codec").is_err());
456    }
457}