Skip to main content

parquet/
compression.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: See the NOTICE file distributed with this work for additional information regarding copyright ownership
3// SPDX-FileContributor: Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements
4
5//! Contains codec interface and supported codec implementations.
6//!
7//! See [`Compression`](crate::basic::Compression) enum for all available compression
8//! algorithms.
9//!
10// NOTE: this must be an inner attribute so that the example is attached to the module (and
11// therefore actually run as a doc test) rather than to the `use` statement below.
12#![cfg_attr(
13    feature = "experimental",
14    doc = r"
15# Example
16
17```no_run
18use parquet::{basic::Compression, compression::{create_codec, CodecOptionsBuilder}};
19
20let codec_options = CodecOptionsBuilder::default()
21    .set_backward_compatible_lz4(false)
22    .build();
23let mut codec = match create_codec(Compression::SNAPPY, &codec_options) {
24 Ok(Some(codec)) => codec,
25 _ => panic!(),
26};
27
28let data = vec![b'p', b'a', b'r', b'q', b'u', b'e', b't'];
29let mut compressed = vec![];
30codec.compress(&data[..], &mut compressed).unwrap();
31
32let mut output = vec![];
33codec.decompress(&compressed[..], &mut output, None).unwrap();
34
35assert_eq!(output, data);
36```
37"
38)]
39use crate::basic::Compression as CodecType;
40use crate::errors::{ParquetError, Result};
41
42/// Parquet compression codec interface.
43pub trait Codec: Send {
44    /// Compresses data stored in slice `input_buf` and appends the compressed result
45    /// to `output_buf`.
46    ///
47    /// Note that you'll need to call `clear()` before reusing the same `output_buf`
48    /// across different `compress` calls.
49    fn compress(&mut self, input_buf: &[u8], output_buf: &mut Vec<u8>) -> Result<()>;
50
51    /// Decompresses data stored in slice `input_buf` and appends output to `output_buf`.
52    ///
53    /// If the uncompress_size is provided it will allocate the exact amount of memory.
54    /// Otherwise, it will estimate the uncompressed size, allocating an amount of memory
55    /// greater or equal to the real uncompress_size.
56    ///
57    /// Returns the total number of bytes written.
58    fn decompress(
59        &mut self,
60        input_buf: &[u8],
61        output_buf: &mut Vec<u8>,
62        uncompress_size: Option<usize>,
63    ) -> Result<usize>;
64}
65
66/// Struct to hold `Codec` creation options.
67#[derive(Debug, PartialEq, Eq)]
68pub struct CodecOptions {
69    /// Whether or not to fallback to other LZ4 older implementations on error in LZ4_HADOOP.
70    backward_compatible_lz4: bool,
71}
72
73impl Default for CodecOptions {
74    fn default() -> Self {
75        CodecOptionsBuilder::default().build()
76    }
77}
78
79pub struct CodecOptionsBuilder {
80    /// Whether or not to fallback to other LZ4 older implementations on error in LZ4_HADOOP.
81    backward_compatible_lz4: bool,
82}
83
84impl Default for CodecOptionsBuilder {
85    fn default() -> Self {
86        Self {
87            backward_compatible_lz4: true,
88        }
89    }
90}
91
92impl CodecOptionsBuilder {
93    /// Enable/disable backward compatible LZ4.
94    ///
95    /// If backward compatible LZ4 is enable, on LZ4_HADOOP error it will fallback
96    /// to the older versions LZ4 algorithms. That is LZ4_FRAME, for backward compatibility
97    /// with files generated by older versions of this library, and LZ4_RAW, for backward
98    /// compatibility with files generated by older versions of parquet-cpp.
99    ///
100    /// If backward compatible LZ4 is disabled, on LZ4_HADOOP error it will return the error.
101    pub fn set_backward_compatible_lz4(mut self, value: bool) -> CodecOptionsBuilder {
102        self.backward_compatible_lz4 = value;
103        self
104    }
105
106    pub fn build(self) -> CodecOptions {
107        CodecOptions {
108            backward_compatible_lz4: self.backward_compatible_lz4,
109        }
110    }
111}
112
113/// Defines valid compression levels.
114pub(crate) trait CompressionLevel<T: std::fmt::Display + std::cmp::PartialOrd> {
115    const MINIMUM_LEVEL: T;
116    const MAXIMUM_LEVEL: T;
117
118    /// Tests if the provided compression level is valid.
119    fn is_valid_level(level: T) -> Result<()> {
120        let compression_range = Self::MINIMUM_LEVEL..=Self::MAXIMUM_LEVEL;
121        if compression_range.contains(&level) {
122            Ok(())
123        } else {
124            Err(ParquetError::General(format!(
125                "valid compression range {}..={} exceeded.",
126                compression_range.start(),
127                compression_range.end()
128            )))
129        }
130    }
131}
132
133/// Given the compression type `codec`, returns a codec used to compress and decompress
134/// bytes for the compression type.
135/// This returns `None` if the codec type is `UNCOMPRESSED`.
136#[cfg_attr(
137    any(feature = "lz4", test),
138    expect(
139        clippy::used_underscore_binding,
140        reason = "`_options` is only read when the `lz4` feature is on"
141    )
142)]
143pub fn create_codec(codec: CodecType, _options: &CodecOptions) -> Result<Option<Box<dyn Codec>>> {
144    #[cfg_attr(
145        any(
146            test,
147            feature = "brotli",
148            feature = "flate2",
149            feature = "lz4",
150            feature = "snap",
151            feature = "zstd"
152        ),
153        expect(unreachable_code)
154    )]
155    #[cfg_attr(
156        all(
157            not(test),
158            not(all(feature = "brotli", feature = "flate2", feature = "zstd"))
159        ),
160        expect(unused_variables)
161    )]
162    match codec {
163        CodecType::BROTLI(level) => {
164            #[cfg(any(feature = "brotli", test))]
165            return Ok(Some(Box::new(BrotliCodec::new(level))));
166            Err(ParquetError::General(
167                "Disabled feature at compile time: brotli".into(),
168            ))
169        }
170        CodecType::GZIP(level) => {
171            #[cfg(any(feature = "flate2", test))]
172            return Ok(Some(Box::new(GZipCodec::new(level))));
173            Err(ParquetError::General(
174                "Disabled feature at compile time: flate2".into(),
175            ))
176        }
177        CodecType::SNAPPY => {
178            #[cfg(any(feature = "snap", test))]
179            return Ok(Some(Box::new(SnappyCodec::new())));
180            Err(ParquetError::General(
181                "Disabled feature at compile time: snap".into(),
182            ))
183        }
184        CodecType::LZ4 => {
185            #[cfg(any(feature = "lz4", test))]
186            return Ok(Some(Box::new(LZ4HadoopCodec::new(
187                _options.backward_compatible_lz4,
188            ))));
189            Err(ParquetError::General(
190                "Disabled feature at compile time: lz4".into(),
191            ))
192        }
193        CodecType::ZSTD(level) => {
194            #[cfg(any(feature = "zstd", test))]
195            return Ok(Some(Box::new(ZSTDCodec::new(level))));
196            Err(ParquetError::General(
197                "Disabled feature at compile time: zstd".into(),
198            ))
199        }
200        CodecType::LZ4_RAW => {
201            #[cfg(any(feature = "lz4", test))]
202            return Ok(Some(Box::new(LZ4RawCodec::new())));
203            Err(ParquetError::General(
204                "Disabled feature at compile time: lz4".into(),
205            ))
206        }
207        CodecType::UNCOMPRESSED => Ok(None),
208        CodecType::LZO => Err(nyi_err!("The codec type {} is not supported yet", codec)),
209    }
210}
211
212#[cfg(any(feature = "snap", test))]
213mod snappy_codec {
214    use snap::raw::{Decoder, Encoder, decompress_len, max_compress_len};
215
216    use crate::compression::Codec;
217    use crate::errors::Result;
218
219    /// Codec for Snappy compression format.
220    pub struct SnappyCodec {
221        decoder: Decoder,
222        encoder: Encoder,
223    }
224
225    impl SnappyCodec {
226        /// Creates new Snappy compression codec.
227        pub(crate) fn new() -> Self {
228            Self {
229                decoder: Decoder::new(),
230                encoder: Encoder::new(),
231            }
232        }
233    }
234
235    impl Codec for SnappyCodec {
236        fn decompress(
237            &mut self,
238            input_buf: &[u8],
239            output_buf: &mut Vec<u8>,
240            uncompress_size: Option<usize>,
241        ) -> Result<usize> {
242            let len = match uncompress_size {
243                Some(size) => size,
244                None => decompress_len(input_buf)?,
245            };
246            let offset = output_buf.len();
247            output_buf.resize(offset + len, 0);
248            self.decoder
249                .decompress(input_buf, &mut output_buf[offset..])
250                .map_err(|e| e.into())
251        }
252
253        fn compress(&mut self, input_buf: &[u8], output_buf: &mut Vec<u8>) -> Result<()> {
254            let output_buf_len = output_buf.len();
255            let required_len = max_compress_len(input_buf.len());
256            output_buf.resize(output_buf_len + required_len, 0);
257            let n = self
258                .encoder
259                .compress(input_buf, &mut output_buf[output_buf_len..])?;
260            output_buf.truncate(output_buf_len + n);
261            Ok(())
262        }
263    }
264}
265#[cfg(any(feature = "snap", test))]
266pub use snappy_codec::*;
267
268#[cfg(any(feature = "flate2", test))]
269mod gzip_codec {
270
271    use std::io::{Read, Write};
272
273    use flate2::{Compression, read, write};
274
275    use crate::compression::Codec;
276    use crate::errors::Result;
277
278    use super::GzipLevel;
279
280    /// Codec for GZIP compression algorithm.
281    pub struct GZipCodec {
282        level: GzipLevel,
283    }
284
285    impl GZipCodec {
286        /// Creates new GZIP compression codec.
287        pub(crate) fn new(level: GzipLevel) -> Self {
288            Self { level }
289        }
290    }
291
292    impl Codec for GZipCodec {
293        fn decompress(
294            &mut self,
295            input_buf: &[u8],
296            output_buf: &mut Vec<u8>,
297            _uncompress_size: Option<usize>,
298        ) -> Result<usize> {
299            let mut decoder = read::MultiGzDecoder::new(input_buf);
300            decoder.read_to_end(output_buf).map_err(|e| e.into())
301        }
302
303        fn compress(&mut self, input_buf: &[u8], output_buf: &mut Vec<u8>) -> Result<()> {
304            let mut encoder = write::GzEncoder::new(output_buf, Compression::new(self.level.0));
305            encoder.write_all(input_buf)?;
306            encoder.try_finish().map_err(|e| e.into())
307        }
308    }
309}
310#[cfg(any(feature = "flate2", test))]
311pub use gzip_codec::*;
312
313/// Represents a valid gzip compression level.
314///
315/// Defaults to 6.
316///
317/// * 0: least compression
318/// * 9: most compression (that other software can read)
319/// * 10: most compression (incompatible with other software, see below)
320/// #### WARNING:
321/// Level 10 compression can offer smallest file size,
322/// but Parquet files created with it will not be readable
323/// by other "standard" paquet readers.
324///
325/// Do **NOT** use level 10 if you need other software to
326/// be able to read the files. Read below for details.
327///
328/// ### IMPORTANT:
329/// There's often confusion about the compression levels in `flate2` vs `arrow`
330/// as highlighted in issue [#1011](https://github.com/apache/arrow-rs/issues/6282).
331///
332/// `flate2` supports two compression backends: `miniz_oxide` and `zlib`.
333///
334/// - `zlib` supports levels from 0 to 9.
335/// - `miniz_oxide` supports levels from 0 to 10.
336///
337/// `arrow` uses `flate2` with `rust_backend` feature,
338/// which provides `miniz_oxide` as the backend.
339/// Therefore 0-10 levels are supported.
340///
341/// `flate2` documents this behavior properly with
342/// [this commit](https://github.com/rust-lang/flate2-rs/pull/430).
343#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy)]
344pub struct GzipLevel(u32);
345
346impl Default for GzipLevel {
347    fn default() -> Self {
348        // The default as of miniz_oxide 0.5.1 is 6 for compression level
349        // (miniz_oxide::deflate::CompressionLevel::DefaultLevel)
350        Self(6)
351    }
352}
353
354impl CompressionLevel<u32> for GzipLevel {
355    const MINIMUM_LEVEL: u32 = 0;
356    const MAXIMUM_LEVEL: u32 = 9;
357}
358
359impl GzipLevel {
360    /// Attempts to create a gzip compression level.
361    ///
362    /// Compression levels must be valid (i.e. be acceptable for [`flate2::Compression`]).
363    pub fn try_new(level: u32) -> Result<Self> {
364        Self::is_valid_level(level).map(|()| Self(level))
365    }
366
367    /// Returns the compression level.
368    pub fn compression_level(&self) -> u32 {
369        self.0
370    }
371}
372
373#[cfg(any(feature = "brotli", test))]
374mod brotli_codec {
375
376    use std::io::{Read, Write};
377
378    use crate::compression::Codec;
379    use crate::errors::Result;
380
381    use super::BrotliLevel;
382
383    const BROTLI_DEFAULT_BUFFER_SIZE: usize = 4096;
384    const BROTLI_DEFAULT_LG_WINDOW_SIZE: u32 = 22; // recommended between 20-22
385
386    /// Codec for Brotli compression algorithm.
387    pub struct BrotliCodec {
388        level: BrotliLevel,
389    }
390
391    impl BrotliCodec {
392        /// Creates new Brotli compression codec.
393        pub(crate) fn new(level: BrotliLevel) -> Self {
394            Self { level }
395        }
396    }
397
398    impl Codec for BrotliCodec {
399        fn decompress(
400            &mut self,
401            input_buf: &[u8],
402            output_buf: &mut Vec<u8>,
403            uncompress_size: Option<usize>,
404        ) -> Result<usize> {
405            let buffer_size = uncompress_size.unwrap_or(BROTLI_DEFAULT_BUFFER_SIZE);
406            brotli::Decompressor::new(input_buf, buffer_size)
407                .read_to_end(output_buf)
408                .map_err(|e| e.into())
409        }
410
411        fn compress(&mut self, input_buf: &[u8], output_buf: &mut Vec<u8>) -> Result<()> {
412            let mut encoder = brotli::CompressorWriter::new(
413                output_buf,
414                BROTLI_DEFAULT_BUFFER_SIZE,
415                self.level.0,
416                BROTLI_DEFAULT_LG_WINDOW_SIZE,
417            );
418            encoder.write_all(input_buf)?;
419            encoder.flush().map_err(|e| e.into())
420        }
421    }
422}
423#[cfg(any(feature = "brotli", test))]
424pub use brotli_codec::*;
425
426/// Represents a valid brotli compression level.
427#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy)]
428pub struct BrotliLevel(u32);
429
430impl Default for BrotliLevel {
431    fn default() -> Self {
432        Self(1)
433    }
434}
435
436impl CompressionLevel<u32> for BrotliLevel {
437    const MINIMUM_LEVEL: u32 = 0;
438    const MAXIMUM_LEVEL: u32 = 11;
439}
440
441impl BrotliLevel {
442    /// Attempts to create a brotli compression level.
443    ///
444    /// Compression levels must be valid.
445    pub fn try_new(level: u32) -> Result<Self> {
446        Self::is_valid_level(level).map(|()| Self(level))
447    }
448
449    /// Returns the compression level.
450    pub fn compression_level(&self) -> u32 {
451        self.0
452    }
453}
454
455#[cfg(any(feature = "lz4", test))]
456mod lz4_codec {
457    use std::io::{Read, Write};
458
459    use crate::compression::Codec;
460    use crate::errors::{ParquetError, Result};
461
462    const LZ4_BUFFER_SIZE: usize = 4096;
463
464    /// Codec for LZ4 compression algorithm.
465    pub struct LZ4Codec {}
466
467    impl LZ4Codec {
468        /// Creates new LZ4 compression codec.
469        pub(crate) fn new() -> Self {
470            Self {}
471        }
472    }
473
474    impl Codec for LZ4Codec {
475        fn decompress(
476            &mut self,
477            input_buf: &[u8],
478            output_buf: &mut Vec<u8>,
479            _uncompress_size: Option<usize>,
480        ) -> Result<usize> {
481            let mut decoder = lz4_flex::frame::FrameDecoder::new(input_buf);
482            let mut buffer: [u8; LZ4_BUFFER_SIZE] = [0; LZ4_BUFFER_SIZE];
483            let mut total_len = 0;
484            loop {
485                let len = decoder.read(&mut buffer)?;
486                if len == 0 {
487                    break;
488                }
489                total_len += len;
490                output_buf.write_all(&buffer[0..len])?;
491            }
492            Ok(total_len)
493        }
494
495        fn compress(&mut self, input_buf: &[u8], output_buf: &mut Vec<u8>) -> Result<()> {
496            let mut encoder = lz4_flex::frame::FrameEncoder::new(output_buf);
497            let mut from = 0;
498            loop {
499                let to = std::cmp::min(from + LZ4_BUFFER_SIZE, input_buf.len());
500                encoder.write_all(&input_buf[from..to])?;
501                from += LZ4_BUFFER_SIZE;
502                if from >= input_buf.len() {
503                    break;
504                }
505            }
506            match encoder.finish() {
507                Ok(_) => Ok(()),
508                Err(e) => Err(ParquetError::External(Box::new(e))),
509            }
510        }
511    }
512}
513
514#[cfg(all(feature = "experimental", any(feature = "lz4", test)))]
515pub use lz4_codec::*;
516
517#[cfg(any(feature = "zstd", test))]
518mod zstd_codec {
519    use crate::compression::{Codec, ZstdLevel};
520    use crate::errors::Result;
521    use std::io::Cursor;
522
523    /// Codec for Zstandard compression algorithm.
524    ///
525    /// Uses `zstd::bulk` API with reusable compressor/decompressor contexts
526    /// to avoid the overhead of reinitializing contexts for each operation.
527    pub struct ZSTDCodec {
528        level: ZstdLevel,
529        // Created on first use, because writers only compress and readers only
530        // decompress.
531        compressor: Option<zstd::bulk::Compressor<'static>>,
532        decompressor: Option<zstd::bulk::Decompressor<'static>>,
533    }
534
535    impl ZSTDCodec {
536        /// Creates new Zstandard compression codec.
537        pub(crate) fn new(level: ZstdLevel) -> Self {
538            Self {
539                level,
540                compressor: None,
541                decompressor: None,
542            }
543        }
544    }
545
546    impl Codec for ZSTDCodec {
547        fn decompress(
548            &mut self,
549            input_buf: &[u8],
550            output_buf: &mut Vec<u8>,
551            uncompress_size: Option<usize>,
552        ) -> Result<usize> {
553            let offset = output_buf.len();
554            let len = uncompress_size
555                .or_else(|| {
556                    // Get the decompressed size from the zstd frame header
557                    zstd::zstd_safe::get_frame_content_size(input_buf)
558                        .ok()
559                        .flatten()
560                        .map(|size| size as usize)
561                })
562                .unwrap_or_else(|| input_buf.len().saturating_mul(4));
563            output_buf.reserve(len);
564
565            let mut cursor = Cursor::new(output_buf);
566            cursor.set_position(offset as u64);
567            let len = self
568                .decompressor
569                .get_or_insert_with(|| {
570                    zstd::bulk::Decompressor::new().expect("can create zstd decompressor")
571                })
572                .decompress_to_buffer(input_buf, &mut cursor)?;
573            Ok(len)
574        }
575
576        fn compress(&mut self, input_buf: &[u8], output_buf: &mut Vec<u8>) -> Result<()> {
577            let offset = output_buf.len();
578            let len = zstd::zstd_safe::compress_bound(input_buf.len());
579            output_buf.reserve(len);
580
581            let mut cursor = Cursor::new(output_buf);
582            cursor.set_position(offset as u64);
583            let _written = self
584                .compressor
585                .get_or_insert_with(|| {
586                    zstd::bulk::Compressor::new(self.level.compression_level())
587                        .expect("valid zstd compression level")
588                })
589                .compress_to_buffer(input_buf, &mut cursor)?;
590            Ok(())
591        }
592    }
593}
594#[cfg(any(feature = "zstd", test))]
595pub use zstd_codec::*;
596
597/// Represents a valid zstd compression level.
598#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy)]
599pub struct ZstdLevel(i32);
600
601impl CompressionLevel<i32> for ZstdLevel {
602    // zstd binds to C, and hence zstd::compression_level_range() is not const as this calls the
603    // underlying C library.
604    const MINIMUM_LEVEL: i32 = -131072;
605    const MAXIMUM_LEVEL: i32 = 22;
606}
607
608impl ZstdLevel {
609    /// Attempts to create a zstd compression level from a given compression level.
610    ///
611    /// Compression levels must be valid (i.e. be acceptable for [`zstd::compression_level_range`]).
612    pub fn try_new(level: i32) -> Result<Self> {
613        Self::is_valid_level(level).map(|()| Self(level))
614    }
615
616    /// Returns the compression level.
617    pub fn compression_level(&self) -> i32 {
618        self.0
619    }
620}
621
622impl Default for ZstdLevel {
623    fn default() -> Self {
624        Self(1)
625    }
626}
627
628#[cfg(any(feature = "lz4", test))]
629mod lz4_raw_codec {
630    use crate::compression::Codec;
631    use crate::errors::ParquetError;
632    use crate::errors::Result;
633
634    /// Codec for LZ4 Raw compression algorithm.
635    pub struct LZ4RawCodec {}
636
637    impl LZ4RawCodec {
638        /// Creates new LZ4 Raw compression codec.
639        pub(crate) fn new() -> Self {
640            Self {}
641        }
642    }
643
644    impl Codec for LZ4RawCodec {
645        fn decompress(
646            &mut self,
647            input_buf: &[u8],
648            output_buf: &mut Vec<u8>,
649            uncompress_size: Option<usize>,
650        ) -> Result<usize> {
651            let offset = output_buf.len();
652            let Some(required_len) = uncompress_size else {
653                return Err(ParquetError::General(
654                    "LZ4RawCodec unsupported without uncompress_size".into(),
655                ));
656            };
657            output_buf.resize(offset + required_len, 0);
658            match lz4_flex::block::decompress_into(input_buf, &mut output_buf[offset..]) {
659                Ok(n) => {
660                    if n != required_len {
661                        return Err(ParquetError::General(
662                            "LZ4RawCodec uncompress_size is not the expected one".into(),
663                        ));
664                    }
665                    Ok(n)
666                }
667                Err(e) => Err(ParquetError::External(Box::new(e))),
668            }
669        }
670
671        fn compress(&mut self, input_buf: &[u8], output_buf: &mut Vec<u8>) -> Result<()> {
672            let offset = output_buf.len();
673            let required_len = lz4_flex::block::get_maximum_output_size(input_buf.len());
674            output_buf.resize(offset + required_len, 0);
675            match lz4_flex::block::compress_into(input_buf, &mut output_buf[offset..]) {
676                Ok(n) => {
677                    output_buf.truncate(offset + n);
678                    Ok(())
679                }
680                Err(e) => Err(ParquetError::External(Box::new(e))),
681            }
682        }
683    }
684}
685#[cfg(any(feature = "lz4", test))]
686pub use lz4_raw_codec::*;
687
688#[cfg(any(feature = "lz4", test))]
689mod lz4_hadoop_codec {
690    use crate::compression::Codec;
691    use crate::compression::lz4_codec::LZ4Codec;
692    use crate::compression::lz4_raw_codec::LZ4RawCodec;
693    use crate::errors::{ParquetError, Result};
694    use std::io;
695
696    /// Size of u32 type.
697    const SIZE_U32: usize = std::mem::size_of::<u32>();
698
699    /// Length of the LZ4_HADOOP prefix.
700    const PREFIX_LEN: usize = SIZE_U32 * 2;
701
702    /// Codec for LZ4 Hadoop compression algorithm.
703    pub struct LZ4HadoopCodec {
704        /// Whether or not to fallback to other LZ4 implementations on error.
705        /// Fallback is done to be backward compatible with older versions of this
706        /// library and older versions parquet-cpp.
707        backward_compatible_lz4: bool,
708    }
709
710    impl LZ4HadoopCodec {
711        /// Creates new LZ4 Hadoop compression codec.
712        pub(crate) fn new(backward_compatible_lz4: bool) -> Self {
713            Self {
714                backward_compatible_lz4,
715            }
716        }
717    }
718
719    /// Try to decompress the buffer as if it was compressed with the Hadoop Lz4Codec.
720    /// Adapted from pola-rs [compression.rs:try_decompress_hadoop](https://pola-rs.github.io/polars/src/parquet2/compression.rs.html#225)
721    /// Translated from the apache arrow c++ function [TryDecompressHadoop](https://github.com/apache/arrow/blob/bf18e6e4b5bb6180706b1ba0d597a65a4ce5ca48/cpp/src/arrow/util/compression_lz4.cc#L474).
722    /// Returns error if decompression failed.
723    fn try_decompress_hadoop(input_buf: &[u8], output_buf: &mut [u8]) -> io::Result<usize> {
724        // Parquet files written with the Hadoop Lz4Codec use their own framing.
725        // The input buffer can contain an arbitrary number of "frames", each
726        // with the following structure:
727        // - bytes 0..3: big-endian uint32_t representing the frame decompressed size
728        // - bytes 4..7: big-endian uint32_t representing the frame compressed size
729        // - bytes 8...: frame compressed data
730        //
731        // The Hadoop Lz4Codec source code can be found here:
732        // https://github.com/apache/hadoop/blob/trunk/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-nativetask/src/main/native/src/codec/Lz4Codec.cc
733        let mut input_len = input_buf.len();
734        let mut input = input_buf;
735        let mut read_bytes = 0;
736        let mut output_len = output_buf.len();
737        let mut output: &mut [u8] = output_buf;
738        while input_len >= PREFIX_LEN {
739            let mut bytes = [0; SIZE_U32];
740            bytes.copy_from_slice(&input[0..4]);
741            let expected_decompressed_size = u32::from_be_bytes(bytes);
742            let mut bytes = [0; SIZE_U32];
743            bytes.copy_from_slice(&input[4..8]);
744            let expected_compressed_size = u32::from_be_bytes(bytes);
745            input = &input[PREFIX_LEN..];
746            input_len -= PREFIX_LEN;
747
748            if input_len < expected_compressed_size as usize {
749                return Err(io::Error::other("Not enough bytes for Hadoop frame"));
750            }
751
752            if output_len < expected_decompressed_size as usize {
753                return Err(io::Error::other(
754                    "Not enough bytes to hold advertised output",
755                ));
756            }
757            let decompressed_size =
758                lz4_flex::decompress_into(&input[..expected_compressed_size as usize], output)
759                    .map_err(|e| ParquetError::External(Box::new(e)))?;
760            if decompressed_size != expected_decompressed_size as usize {
761                return Err(io::Error::other("Unexpected decompressed size"));
762            }
763            input_len -= expected_compressed_size as usize;
764            output_len -= expected_decompressed_size as usize;
765            read_bytes += expected_decompressed_size as usize;
766            if input_len > expected_compressed_size as usize {
767                input = &input[expected_compressed_size as usize..];
768                output = &mut output[expected_decompressed_size as usize..];
769            } else {
770                break;
771            }
772        }
773        if input_len == 0 {
774            Ok(read_bytes)
775        } else {
776            Err(io::Error::other("Not all input are consumed"))
777        }
778    }
779
780    impl Codec for LZ4HadoopCodec {
781        fn decompress(
782            &mut self,
783            input_buf: &[u8],
784            output_buf: &mut Vec<u8>,
785            uncompress_size: Option<usize>,
786        ) -> Result<usize> {
787            let output_len = output_buf.len();
788            let Some(required_len) = uncompress_size else {
789                return Err(ParquetError::General(
790                    "LZ4HadoopCodec unsupported without uncompress_size".into(),
791                ));
792            };
793            output_buf.resize(output_len + required_len, 0);
794            match try_decompress_hadoop(input_buf, &mut output_buf[output_len..]) {
795                Ok(n) => {
796                    if n != required_len {
797                        return Err(ParquetError::General(
798                            "LZ4HadoopCodec uncompress_size is not the expected one".into(),
799                        ));
800                    }
801                    Ok(n)
802                }
803                Err(e) if !self.backward_compatible_lz4 => Err(e.into()),
804                // Fallback done to be backward compatible with older versions of this
805                // library and older versions of parquet-cpp.
806                Err(_) => {
807                    // Truncate any inserted element before tryingg next algorithm.
808                    output_buf.truncate(output_len);
809                    match LZ4Codec::new().decompress(input_buf, output_buf, uncompress_size) {
810                        Ok(n) => Ok(n),
811                        Err(_) => {
812                            // Truncate any inserted element before tryingg next algorithm.
813                            output_buf.truncate(output_len);
814                            LZ4RawCodec::new().decompress(input_buf, output_buf, uncompress_size)
815                        }
816                    }
817                }
818            }
819        }
820
821        fn compress(&mut self, input_buf: &[u8], output_buf: &mut Vec<u8>) -> Result<()> {
822            // Allocate memory to store the LZ4_HADOOP prefix.
823            let offset = output_buf.len();
824            output_buf.resize(offset + PREFIX_LEN, 0);
825
826            // Append LZ4_RAW compressed bytes after prefix.
827            LZ4RawCodec::new().compress(input_buf, output_buf)?;
828
829            // Prepend decompressed size and compressed size in big endian to be compatible
830            // with LZ4_HADOOP.
831            let output_buf = &mut output_buf[offset..];
832            let compressed_size = output_buf.len() - PREFIX_LEN;
833            let compressed_size = compressed_size as u32;
834            let uncompressed_size = input_buf.len() as u32;
835            output_buf[..SIZE_U32].copy_from_slice(&uncompressed_size.to_be_bytes());
836            output_buf[SIZE_U32..PREFIX_LEN].copy_from_slice(&compressed_size.to_be_bytes());
837
838            Ok(())
839        }
840    }
841}
842#[cfg(any(feature = "lz4", test))]
843pub use lz4_hadoop_codec::*;
844
845#[cfg(test)]
846mod tests {
847    use super::*;
848
849    use crate::util::test_common::rand_gen::random_bytes;
850
851    fn test_roundtrip(c: CodecType, data: &[u8], uncompress_size: Option<usize>) {
852        let codec_options = CodecOptionsBuilder::default()
853            .set_backward_compatible_lz4(false)
854            .build();
855        let mut c1 = create_codec(c, &codec_options).unwrap().unwrap();
856        let mut c2 = create_codec(c, &codec_options).unwrap().unwrap();
857
858        // Compress with c1
859        let mut compressed = Vec::new();
860        let mut decompressed = Vec::new();
861        c1.compress(data, &mut compressed)
862            .expect("Error when compressing");
863
864        // Decompress with c2
865        let decompressed_size = c2
866            .decompress(compressed.as_slice(), &mut decompressed, uncompress_size)
867            .expect("Error when decompressing");
868        assert_eq!(data.len(), decompressed_size);
869        assert_eq!(data, decompressed.as_slice());
870
871        decompressed.clear();
872        compressed.clear();
873
874        // Compress with c2
875        c2.compress(data, &mut compressed)
876            .expect("Error when compressing");
877
878        // Decompress with c1
879        let decompressed_size = c1
880            .decompress(compressed.as_slice(), &mut decompressed, uncompress_size)
881            .expect("Error when decompressing");
882        assert_eq!(data.len(), decompressed_size);
883        assert_eq!(data, decompressed.as_slice());
884
885        decompressed.clear();
886        compressed.clear();
887
888        // Test does not trample existing data in output buffers
889        let prefix = &[0xDE, 0xAD, 0xBE, 0xEF];
890        decompressed.extend_from_slice(prefix);
891        compressed.extend_from_slice(prefix);
892
893        c2.compress(data, &mut compressed)
894            .expect("Error when compressing");
895
896        assert_eq!(&compressed[..4], prefix);
897
898        let decompressed_size = c2
899            .decompress(&compressed[4..], &mut decompressed, uncompress_size)
900            .expect("Error when decompressing");
901
902        assert_eq!(data.len(), decompressed_size);
903        assert_eq!(data, &decompressed[4..]);
904        assert_eq!(&decompressed[..4], prefix);
905    }
906
907    fn test_codec_with_size(c: CodecType) {
908        let sizes = vec![100, 10000, 100000];
909        for size in sizes {
910            let data = random_bytes(size);
911            test_roundtrip(c, &data, Some(data.len()));
912        }
913    }
914
915    fn test_codec_without_size(c: CodecType) {
916        let sizes = vec![100, 10000, 100000];
917        for size in sizes {
918            let data = random_bytes(size);
919            test_roundtrip(c, &data, None);
920        }
921    }
922
923    #[test]
924    #[cfg_attr(miri, ignore)] // Takes too long
925    fn test_codec_snappy() {
926        test_codec_with_size(CodecType::SNAPPY);
927        test_codec_without_size(CodecType::SNAPPY);
928    }
929
930    #[test]
931    #[cfg_attr(miri, ignore)] // Takes too long
932    fn test_codec_gzip() {
933        for level in GzipLevel::MINIMUM_LEVEL..=GzipLevel::MAXIMUM_LEVEL {
934            let level = GzipLevel::try_new(level).unwrap();
935            test_codec_with_size(CodecType::GZIP(level));
936            test_codec_without_size(CodecType::GZIP(level));
937        }
938    }
939
940    #[test]
941    #[cfg_attr(miri, ignore)] // Takes too long
942    fn test_codec_brotli() {
943        for level in BrotliLevel::MINIMUM_LEVEL..=BrotliLevel::MAXIMUM_LEVEL {
944            let level = BrotliLevel::try_new(level).unwrap();
945            test_codec_with_size(CodecType::BROTLI(level));
946            test_codec_without_size(CodecType::BROTLI(level));
947        }
948    }
949
950    #[test]
951    #[cfg_attr(miri, ignore)] // Takes too long
952    fn test_codec_lz4() {
953        test_codec_with_size(CodecType::LZ4);
954    }
955
956    #[test]
957    #[cfg_attr(miri, ignore)] // Zstd calls native C functions unsupported by Miri
958    fn test_codec_zstd() {
959        // since ZstdLevel::MINIMUM_LEVEL is a large negative number, we test a smaller range
960        for level in
961            std::iter::once(ZstdLevel::MINIMUM_LEVEL).chain(-100..=ZstdLevel::MAXIMUM_LEVEL)
962        {
963            let level = ZstdLevel::try_new(level).unwrap();
964            test_codec_with_size(CodecType::ZSTD(level));
965            test_codec_without_size(CodecType::ZSTD(level));
966        }
967    }
968
969    #[test]
970    #[cfg_attr(miri, ignore)] // Takes too long
971    fn test_codec_lz4_raw() {
972        test_codec_with_size(CodecType::LZ4_RAW);
973    }
974}