Skip to main content

orc_rust/
compression.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// Modified from https://github.com/DataEngineeringLabs/orc-format/blob/416490db0214fc51d53289253c0ee91f7fc9bc17/src/read/decompress/mod.rs
19//! Related code for handling compression and decompression of ORC files.
20
21use std::{
22    borrow::Cow,
23    io::{Read, Write},
24};
25
26use bytes::{Bytes, BytesMut};
27use fallible_streaming_iterator::FallibleStreamingIterator;
28use snafu::ResultExt;
29
30use crate::error::{self, OrcError, Result};
31use crate::proto::{self, CompressionKind};
32
33// Spec states default is 256K
34const DEFAULT_COMPRESSION_BLOCK_SIZE: u64 = 256 * 1024;
35
36/// Bits 1..23 of the 3-byte header store the payload length.
37const MAX_COMPRESSION_BLOCK_SIZE: u64 = 1 << 23;
38
39#[derive(Clone, Copy, Debug)]
40pub struct Compression {
41    compression_type: CompressionType,
42    /// No compression chunk will decompress to larger than this size.
43    /// Use to size the scratch buffer appropriately.
44    max_decompressed_block_size: usize,
45}
46impl std::fmt::Display for Compression {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        write!(
49            f,
50            "{} ({} byte max block size)",
51            self.compression_type, self.max_decompressed_block_size
52        )
53    }
54}
55
56impl Compression {
57    pub fn compression_type(&self) -> CompressionType {
58        self.compression_type
59    }
60
61    pub(crate) fn from_proto(
62        kind: proto::CompressionKind,
63        compression_block_size: Option<u64>,
64    ) -> Option<Self> {
65        let max_decompressed_block_size =
66            compression_block_size.unwrap_or(DEFAULT_COMPRESSION_BLOCK_SIZE) as usize;
67        match kind {
68            CompressionKind::None => None,
69            CompressionKind::Zlib => Some(Self {
70                compression_type: CompressionType::Zlib,
71                max_decompressed_block_size,
72            }),
73            CompressionKind::Snappy => Some(Self {
74                compression_type: CompressionType::Snappy,
75                max_decompressed_block_size,
76            }),
77            CompressionKind::Lzo => Some(Self {
78                compression_type: CompressionType::Lzo,
79                max_decompressed_block_size,
80            }),
81            CompressionKind::Lz4 => Some(Self {
82                compression_type: CompressionType::Lz4,
83                max_decompressed_block_size,
84            }),
85            CompressionKind::Zstd => Some(Self {
86                compression_type: CompressionType::Zstd,
87                max_decompressed_block_size,
88            }),
89        }
90    }
91}
92
93#[derive(Clone, Copy, Debug)]
94pub enum CompressionType {
95    Zlib,
96    Snappy,
97    Lzo,
98    Lz4,
99    Zstd,
100}
101
102impl std::fmt::Display for CompressionType {
103    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104        write!(f, "{self:?}")
105    }
106}
107
108impl CompressionType {
109    pub(crate) fn to_proto(self) -> proto::CompressionKind {
110        match self {
111            Self::Zlib => proto::CompressionKind::Zlib,
112            Self::Snappy => proto::CompressionKind::Snappy,
113            Self::Lzo => proto::CompressionKind::Lzo,
114            Self::Lz4 => proto::CompressionKind::Lz4,
115            Self::Zstd => proto::CompressionKind::Zstd,
116        }
117    }
118}
119
120/// Indicates length of block and whether it's compressed or not.
121#[derive(Debug, PartialEq, Eq)]
122enum CompressionHeader {
123    Original(u32),
124    Compressed(u32),
125}
126
127/// ORC files are compressed in blocks, with a 3 byte header at the start
128/// of these blocks indicating the length of the block and whether it's
129/// compressed or not.
130impl CompressionHeader {
131    fn decode(bytes: [u8; 3]) -> Self {
132        let bytes = [bytes[0], bytes[1], bytes[2], 0];
133        let length_and_flag = u32::from_le_bytes(bytes);
134        let is_original = length_and_flag & 1 == 1;
135        let length = length_and_flag >> 1;
136        if is_original {
137            Self::Original(length)
138        } else {
139            Self::Compressed(length)
140        }
141    }
142
143    fn encode(self) -> [u8; 3] {
144        let (length, is_original) = match self {
145            Self::Original(length) => (length, 1),
146            Self::Compressed(length) => (length, 0),
147        };
148        debug_assert!((length as u64) < MAX_COMPRESSION_BLOCK_SIZE);
149        let encoded = (length << 1) | is_original;
150        let bytes = encoded.to_le_bytes();
151        [bytes[0], bytes[1], bytes[2]]
152    }
153}
154
155trait DecompressorVariant: Send {
156    fn decompress_block(&self, compressed_bytes: &[u8], scratch: &mut Vec<u8>) -> Result<()>;
157}
158
159#[derive(Debug, Clone, Copy)]
160struct Zlib;
161#[derive(Debug, Clone, Copy)]
162struct Zstd;
163#[derive(Debug, Clone, Copy)]
164struct Snappy;
165#[derive(Debug, Clone, Copy)]
166struct Lzo;
167#[derive(Debug, Clone, Copy)]
168struct Lz4 {
169    max_decompressed_block_size: usize,
170}
171
172impl DecompressorVariant for Zlib {
173    fn decompress_block(&self, compressed_bytes: &[u8], scratch: &mut Vec<u8>) -> Result<()> {
174        let mut gz = flate2::read::DeflateDecoder::new(compressed_bytes);
175        scratch.clear();
176        gz.read_to_end(scratch).context(error::IoSnafu)?;
177        Ok(())
178    }
179}
180
181impl DecompressorVariant for Zstd {
182    fn decompress_block(&self, compressed_bytes: &[u8], scratch: &mut Vec<u8>) -> Result<()> {
183        let mut reader =
184            zstd::Decoder::new(compressed_bytes).context(error::BuildZstdDecoderSnafu)?;
185        scratch.clear();
186        reader.read_to_end(scratch).context(error::IoSnafu)?;
187        Ok(())
188    }
189}
190
191impl DecompressorVariant for Snappy {
192    fn decompress_block(&self, compressed_bytes: &[u8], scratch: &mut Vec<u8>) -> Result<()> {
193        let len =
194            snap::raw::decompress_len(compressed_bytes).context(error::BuildSnappyDecoderSnafu)?;
195        scratch.resize(len, 0);
196        let mut decoder = snap::raw::Decoder::new();
197        decoder
198            .decompress(compressed_bytes, scratch)
199            .context(error::BuildSnappyDecoderSnafu)?;
200        Ok(())
201    }
202}
203
204impl DecompressorVariant for Lzo {
205    fn decompress_block(&self, compressed_bytes: &[u8], scratch: &mut Vec<u8>) -> Result<()> {
206        let decompressed = lzokay_native::decompress_all(compressed_bytes, None)
207            .context(error::BuildLzoDecoderSnafu)?;
208        // TODO: better way to utilize scratch here
209        scratch.clear();
210        scratch.extend(decompressed);
211        Ok(())
212    }
213}
214
215impl DecompressorVariant for Lz4 {
216    fn decompress_block(&self, compressed_bytes: &[u8], scratch: &mut Vec<u8>) -> Result<()> {
217        let decompressed =
218            lz4_flex::block::decompress(compressed_bytes, self.max_decompressed_block_size)
219                .context(error::BuildLz4DecoderSnafu)?;
220        // TODO: better way to utilize scratch here
221        scratch.clear();
222        scratch.extend(decompressed);
223        Ok(())
224    }
225}
226
227// TODO: push this earlier so we don't check this variant each time
228fn get_decompressor_variant(
229    Compression {
230        compression_type,
231        max_decompressed_block_size,
232    }: Compression,
233) -> Box<dyn DecompressorVariant> {
234    match compression_type {
235        CompressionType::Zlib => Box::new(Zlib),
236        CompressionType::Snappy => Box::new(Snappy),
237        CompressionType::Lzo => Box::new(Lzo),
238        CompressionType::Lz4 => Box::new(Lz4 {
239            max_decompressed_block_size,
240        }),
241        CompressionType::Zstd => Box::new(Zstd),
242    }
243}
244
245enum State {
246    Original(Bytes),
247    Compressed(Vec<u8>),
248}
249
250struct DecompressorIter {
251    stream: BytesMut,
252    current: Option<State>, // when we have compression but the value is original
253    compression: Option<Box<dyn DecompressorVariant>>,
254    scratch: Vec<u8>,
255}
256
257impl DecompressorIter {
258    fn new(stream: Bytes, compression: Option<Compression>, scratch: Vec<u8>) -> Self {
259        Self {
260            stream: BytesMut::from(stream.as_ref()),
261            current: None,
262            compression: compression.map(get_decompressor_variant),
263            scratch,
264        }
265    }
266}
267
268impl FallibleStreamingIterator for DecompressorIter {
269    type Item = [u8];
270
271    type Error = OrcError;
272
273    #[inline]
274    fn advance(&mut self) -> Result<(), Self::Error> {
275        if self.stream.is_empty() {
276            self.current = None;
277            return Ok(());
278        }
279
280        match &self.compression {
281            Some(compression) => {
282                // TODO: take stratch from current State::Compressed for re-use
283                let header = self.stream.split_to(3);
284                let header = [header[0], header[1], header[2]];
285                match CompressionHeader::decode(header) {
286                    CompressionHeader::Original(length) => {
287                        let original = self.stream.split_to(length as usize);
288                        self.current = Some(State::Original(original.into()));
289                    }
290                    CompressionHeader::Compressed(length) => {
291                        let compressed = self.stream.split_to(length as usize);
292                        compression.decompress_block(&compressed, &mut self.scratch)?;
293                        self.current = Some(State::Compressed(std::mem::take(&mut self.scratch)));
294                    }
295                };
296                Ok(())
297            }
298            None => {
299                // TODO: take stratch from current State::Compressed for re-use
300                self.current = Some(State::Original(self.stream.clone().into()));
301                self.stream.clear();
302                Ok(())
303            }
304        }
305    }
306
307    #[inline]
308    fn get(&self) -> Option<&Self::Item> {
309        self.current.as_ref().map(|x| match x {
310            State::Original(x) => x.as_ref(),
311            State::Compressed(x) => x.as_ref(),
312        })
313    }
314}
315
316/// A [`Read`]er fulfilling the ORC specification of reading compressed data.
317pub struct Decompressor {
318    decompressor: DecompressorIter,
319    offset: usize,
320    is_first: bool,
321}
322
323impl Decompressor {
324    /// Creates a new [`Decompressor`] that will use `scratch` as a temporary region.
325    pub fn new(stream: Bytes, compression: Option<Compression>, scratch: Vec<u8>) -> Self {
326        Self {
327            decompressor: DecompressorIter::new(stream, compression, scratch),
328            offset: 0,
329            is_first: true,
330        }
331    }
332
333    // TODO: remove need for this upstream
334    pub fn empty() -> Self {
335        Self {
336            decompressor: DecompressorIter::new(Bytes::new(), None, vec![]),
337            offset: 0,
338            is_first: true,
339        }
340    }
341}
342
343impl std::io::Read for Decompressor {
344    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
345        if self.is_first {
346            self.is_first = false;
347            self.decompressor.advance().unwrap();
348        }
349        let current = self.decompressor.get();
350        let current = if let Some(current) = current {
351            if current.len() == self.offset {
352                self.decompressor.advance().unwrap();
353                self.offset = 0;
354                let current = self.decompressor.get();
355                if let Some(current) = current {
356                    current
357                } else {
358                    return Ok(0);
359                }
360            } else {
361                &current[self.offset..]
362            }
363        } else {
364            return Ok(0);
365        };
366
367        if current.len() >= buf.len() {
368            buf.copy_from_slice(&current[..buf.len()]);
369            self.offset += buf.len();
370            Ok(buf.len())
371        } else {
372            buf[..current.len()].copy_from_slice(current);
373            self.offset += current.len();
374            Ok(current.len())
375        }
376    }
377}
378
379trait CompressorVariant: Send {
380    fn compress_block(&mut self, input: &[u8], output: &mut Vec<u8>) -> Result<()>;
381}
382
383struct ZlibCompressor;
384
385impl CompressorVariant for ZlibCompressor {
386    fn compress_block(&mut self, input: &[u8], output: &mut Vec<u8>) -> Result<()> {
387        let mut encoder =
388            flate2::write::DeflateEncoder::new(output, flate2::Compression::default());
389        encoder.write_all(input).context(error::IoSnafu)?;
390        encoder.finish().context(error::IoSnafu)?;
391        Ok(())
392    }
393}
394
395struct SnappyCompressor(snap::raw::Encoder);
396
397impl CompressorVariant for SnappyCompressor {
398    fn compress_block(&mut self, input: &[u8], output: &mut Vec<u8>) -> Result<()> {
399        output.resize(snap::raw::max_compress_len(input.len()), 0);
400        let written = self
401            .0
402            .compress(input, output)
403            .context(error::CompressSnappySnafu)?;
404        output.truncate(written);
405        Ok(())
406    }
407}
408
409struct Lz4Compressor;
410
411impl CompressorVariant for Lz4Compressor {
412    fn compress_block(&mut self, input: &[u8], output: &mut Vec<u8>) -> Result<()> {
413        output.resize(lz4_flex::block::get_maximum_output_size(input.len()), 0);
414        let written =
415            lz4_flex::block::compress_into(input, output).context(error::CompressLz4Snafu)?;
416        output.truncate(written);
417        Ok(())
418    }
419}
420
421struct ZstdCompressor(zstd::bulk::Compressor<'static>);
422
423impl CompressorVariant for ZstdCompressor {
424    fn compress_block(&mut self, input: &[u8], output: &mut Vec<u8>) -> Result<()> {
425        output.reserve(zstd::zstd_safe::compress_bound(input.len()));
426        self.0
427            .compress_to_buffer(input, output)
428            .context(error::IoSnafu)?;
429        Ok(())
430    }
431}
432
433fn get_compressor_variant(compression: CompressionType) -> Result<Box<dyn CompressorVariant>> {
434    match compression {
435        CompressionType::Zlib => Ok(Box::new(ZlibCompressor)),
436        CompressionType::Snappy => Ok(Box::new(SnappyCompressor(snap::raw::Encoder::new()))),
437        CompressionType::Lz4 => Ok(Box::new(Lz4Compressor)),
438        CompressionType::Zstd => Ok(Box::new(ZstdCompressor(
439            zstd::bulk::Compressor::new(0).context(error::IoSnafu)?,
440        ))),
441        CompressionType::Lzo => error::UnexpectedSnafu {
442            msg: "LZO compression is not supported by the ORC writer",
443        }
444        .fail(),
445    }
446}
447
448pub(crate) struct Compressor {
449    compression: Option<CompressionType>,
450    block_size: Option<usize>,
451    compressor: Option<Box<dyn CompressorVariant>>,
452    scratch: Vec<u8>,
453}
454
455impl Compressor {
456    pub(crate) fn new(
457        compression: Option<CompressionType>,
458        block_size: Option<usize>,
459    ) -> Result<Self> {
460        if let Some(block_size) = block_size {
461            if block_size == 0 || (block_size as u64) >= MAX_COMPRESSION_BLOCK_SIZE {
462                return error::UnexpectedSnafu {
463                    msg: format!(
464                        "compression block size must be in 1..{}, got {}",
465                        MAX_COMPRESSION_BLOCK_SIZE, block_size
466                    ),
467                }
468                .fail();
469            }
470        }
471
472        let (block_size, compressor) = match compression {
473            Some(compression) => {
474                let block_size = block_size.unwrap_or(DEFAULT_COMPRESSION_BLOCK_SIZE as usize);
475                (Some(block_size), Some(get_compressor_variant(compression)?))
476            }
477            None => (None, None),
478        };
479        Ok(Self {
480            compression,
481            block_size,
482            compressor,
483            scratch: Vec::new(),
484        })
485    }
486
487    pub(crate) fn compress<'a>(&mut self, input: &'a [u8]) -> Result<Cow<'a, [u8]>> {
488        let Some(compressor) = self.compressor.as_mut() else {
489            return Ok(Cow::Borrowed(input));
490        };
491        let block_size = self.block_size.expect("enabled compressor has block size");
492        let mut output = Vec::with_capacity(input.len());
493        for block in input.chunks(block_size) {
494            self.scratch.clear();
495            compressor.compress_block(block, &mut self.scratch)?;
496
497            let (payload, header) = if self.scratch.len() < block.len() {
498                (
499                    self.scratch.as_slice(),
500                    CompressionHeader::Compressed(self.scratch.len() as u32),
501                )
502            } else {
503                (block, CompressionHeader::Original(block.len() as u32))
504            };
505            output.extend_from_slice(&header.encode());
506            output.extend_from_slice(payload);
507        }
508        Ok(Cow::Owned(output))
509    }
510
511    pub(crate) fn compression(&self) -> Option<CompressionType> {
512        self.compression
513    }
514
515    pub(crate) fn block_size(&self) -> Option<usize> {
516        self.block_size
517    }
518}
519
520#[cfg(test)]
521mod tests {
522    use super::*;
523
524    #[test]
525    fn compression_header_roundtrip() {
526        for (expected, bytes) in [
527            (CompressionHeader::Original(5), [0x0b, 0x00, 0x00]),
528            (CompressionHeader::Compressed(100_000), [0x40, 0x0d, 0x03]),
529            (
530                CompressionHeader::Original((MAX_COMPRESSION_BLOCK_SIZE - 1) as u32),
531                [0xff, 0xff, 0xff],
532            ),
533        ] {
534            let decoded = CompressionHeader::decode(bytes);
535            assert_eq!(decoded, expected);
536            assert_eq!(decoded.encode(), bytes);
537        }
538    }
539
540    #[test]
541    fn rejects_invalid_writer_compression() {
542        assert!(Compressor::new(Some(CompressionType::Zstd), Some(0)).is_err());
543        assert!(Compressor::new(
544            Some(CompressionType::Zstd),
545            Some(MAX_COMPRESSION_BLOCK_SIZE as usize),
546        )
547        .is_err());
548        assert!(Compressor::new(Some(CompressionType::Lzo), None).is_err());
549    }
550
551    #[test]
552    fn handles_empty_original_and_chunked_blocks() {
553        let mut disabled = Compressor::new(None, None).unwrap();
554        let encoded = disabled.compress(b"uncompressed").unwrap();
555        assert!(matches!(encoded, Cow::Borrowed(b"uncompressed")));
556        assert!(disabled.compression().is_none());
557        assert!(disabled.block_size().is_none());
558
559        let mut compressor = Compressor::new(Some(CompressionType::Zstd), Some(4)).unwrap();
560        assert!(compressor.compress(&[]).unwrap().is_empty());
561
562        for compression_type in [
563            CompressionType::Zlib,
564            CompressionType::Snappy,
565            CompressionType::Lz4,
566            CompressionType::Zstd,
567        ] {
568            let mut compressor = Compressor::new(Some(compression_type), Some(64)).unwrap();
569            let encoded = compressor.compress(b"x").unwrap();
570            assert_eq!(encoded.as_ref(), [3, 0, 0, b'x']);
571        }
572
573        let encoded = compressor.compress(b"abcdefghi").unwrap();
574        assert_eq!(
575            encoded.as_ref(),
576            [9, 0, 0, b'a', b'b', b'c', b'd', 9, 0, 0, b'e', b'f', b'g', b'h', 3, 0, 0, b'i',]
577        );
578    }
579
580    #[test]
581    fn roundtrips_all_writer_codecs() {
582        let input = vec![0; 1024];
583        for compression_type in [
584            CompressionType::Zlib,
585            CompressionType::Snappy,
586            CompressionType::Lz4,
587            CompressionType::Zstd,
588        ] {
589            let mut compressor = Compressor::new(Some(compression_type), Some(64)).unwrap();
590            let encoded = compressor.compress(&input).unwrap();
591            let header = CompressionHeader::decode([encoded[0], encoded[1], encoded[2]]);
592            assert!(
593                matches!(header, CompressionHeader::Compressed(length) if length < 64),
594                "{compression_type} unexpectedly stored a compressible block as original"
595            );
596
597            let compression = Compression::from_proto(compression_type.to_proto(), Some(64));
598            let mut decoded = Vec::new();
599            Decompressor::new(Bytes::from(encoded.into_owned()), compression, Vec::new())
600                .read_to_end(&mut decoded)
601                .unwrap();
602            assert_eq!(decoded, input);
603        }
604    }
605}