parx-rs 0.1.0

Parx format Rust library
Documentation
/*
 * Copyright 2026 PARX Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
//! Compression and decompression for PARX footer payloads.

use crate::error::{ParxError, Result};
use crate::format::Compression;
use std::io::{Read, Write};

/// Compress data using the specified algorithm.
///
/// # Errors
/// Returns error if compression fails.
pub fn compress(data: &[u8], algorithm: Compression) -> Result<Vec<u8>> {
    match algorithm {
        Compression::Zstd => compress_zstd(data),
        Compression::Lz4 => compress_lz4(data),
        Compression::Gzip => compress_gzip(data),
    }
}

/// Decompress data using the specified algorithm.
///
/// The `size_hint` parameter is used for pre-allocation when known.
///
/// # Errors
/// Returns error if decompression fails or data is corrupted.
pub fn decompress(data: &[u8], algorithm: Compression, size_hint: usize) -> Result<Vec<u8>> {
    match algorithm {
        Compression::Zstd => decompress_zstd(data, size_hint),
        Compression::Lz4 => decompress_lz4(data),
        Compression::Gzip => decompress_gzip(data, size_hint),
    }
}

/// Default Zstd compression level
const ZSTD_LEVEL: i32 = 3;

/// Compress data using Zstandard.
fn compress_zstd(data: &[u8]) -> Result<Vec<u8>> {
    zstd::encode_all(std::io::Cursor::new(data), ZSTD_LEVEL)
        .map_err(|e| ParxError::CompressionError(format!("zstd compression failed: {e}")))
}

/// Decompress data using Zstandard.
fn decompress_zstd(data: &[u8], size_hint: usize) -> Result<Vec<u8>> {
    let mut output = Vec::with_capacity(size_hint);
    let mut decoder = zstd::Decoder::new(std::io::Cursor::new(data))
        .map_err(|e| ParxError::CompressionError(format!("zstd decoder init failed: {e}")))?;
    decoder
        .read_to_end(&mut output)
        .map_err(|e| ParxError::CompressionError(format!("zstd decompression failed: {e}")))?;
    Ok(output)
}

/// Compress data using LZ4.
fn compress_lz4(data: &[u8]) -> Result<Vec<u8>> {
    let compressed = lz4_flex::compress_prepend_size(data);
    if compressed.is_empty() && !data.is_empty() {
        return Err(ParxError::CompressionError(
            "lz4 compression produced empty output".to_string(),
        ));
    }
    Ok(compressed)
}

/// Decompress data using LZ4.
fn decompress_lz4(data: &[u8]) -> Result<Vec<u8>> {
    lz4_flex::decompress_size_prepended(data)
        .map_err(|e| ParxError::CompressionError(format!("lz4 decompression failed: {e}")))
}

/// Compress data using Gzip.
fn compress_gzip(data: &[u8]) -> Result<Vec<u8>> {
    use flate2::write::GzEncoder;
    use flate2::Compression;

    let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
    encoder
        .write_all(data)
        .map_err(|e| ParxError::CompressionError(format!("gzip compression failed: {e}")))?;
    encoder
        .finish()
        .map_err(|e| ParxError::CompressionError(format!("gzip finish failed: {e}")))
}

/// Decompress data using Gzip.
fn decompress_gzip(data: &[u8], size_hint: usize) -> Result<Vec<u8>> {
    use flate2::read::GzDecoder;

    let mut decoder = GzDecoder::new(data);
    let mut output = Vec::with_capacity(size_hint);
    decoder
        .read_to_end(&mut output)
        .map_err(|e| ParxError::CompressionError(format!("gzip decompression failed: {e}")))?;
    Ok(output)
}

/// Threshold in bytes above which auto-compression is enabled.
pub const AUTO_COMPRESS_THRESHOLD: usize = 10_000;

/// Determine if data should be auto-compressed based on size.
#[inline]
pub const fn should_auto_compress(data_len: usize) -> bool {
    data_len > AUTO_COMPRESS_THRESHOLD
}

#[cfg(test)]
mod tests {
    use super::*;

    fn test_roundtrip(algorithm: Compression) {
        let data = b"The quick brown fox jumps over the lazy dog. ".repeat(100);
        let compressed = compress(&data, algorithm).expect("compression failed");
        let decompressed =
            decompress(&compressed, algorithm, data.len()).expect("decompression failed");
        assert_eq!(data.as_slice(), decompressed.as_slice());
    }

    #[test]
    fn test_zstd_roundtrip() {
        test_roundtrip(Compression::Zstd);
    }

    #[test]
    fn test_lz4_roundtrip() {
        test_roundtrip(Compression::Lz4);
    }

    #[test]
    fn test_gzip_roundtrip() {
        test_roundtrip(Compression::Gzip);
    }

    #[test]
    fn test_compression_reduces_size() {
        let data = b"AAAAAAAAAA".repeat(1000);
        for algorithm in [Compression::Zstd, Compression::Lz4, Compression::Gzip] {
            let compressed = compress(&data, algorithm).expect("compression failed");
            assert!(
                compressed.len() < data.len(),
                "{} did not reduce size: {} vs {}",
                algorithm,
                compressed.len(),
                data.len()
            );
        }
    }

    #[test]
    fn test_empty_data() {
        let data = b"";
        for algorithm in [Compression::Zstd, Compression::Lz4, Compression::Gzip] {
            let compressed = compress(data, algorithm).expect("compression failed");
            let decompressed = decompress(&compressed, algorithm, 0).expect("decompression failed");
            assert_eq!(data.as_slice(), decompressed.as_slice());
        }
    }

    #[test]
    fn test_auto_compress_threshold() {
        assert!(!should_auto_compress(1000));
        assert!(!should_auto_compress(10_000));
        assert!(should_auto_compress(10_001));
        assert!(should_auto_compress(100_000));
    }
}