use super::{
compression_goal::CompressionGoal,
methods::{compress_brotli, compress_gzip, compress_zstd, decompress_brotli, decompress_gzip, decompress_zstd},
target_compression::TargetCompression,
};
use crate::{Blob, TileCompression};
use anyhow::{Result, bail};
use versatiles_derive::context;
#[context("Optimizing compression for blob with input compression: {input_compression:?} and target: {target:?}")]
pub fn optimize_compression(
blob: Blob,
input_compression: TileCompression,
target: &TargetCompression,
) -> Result<(Blob, TileCompression)> {
if target.compressions.is_empty() {
bail!("At least one compression algorithm must be allowed");
}
if !target.compressions.contains(TileCompression::Uncompressed) {
bail!("'Uncompressed' must always be supported");
}
use CompressionGoal::{IsIncompressible, UseBestCompression};
if target.compression_goal != UseBestCompression && target.compressions.contains(input_compression) {
return Ok((blob, input_compression));
}
match input_compression {
TileCompression::Uncompressed => {
if target.compressions.contains(TileCompression::Zstd) {
return Ok((compress_zstd(&blob)?, TileCompression::Zstd));
}
if target.compression_goal != IsIncompressible {
if target.compressions.contains(TileCompression::Brotli) {
return Ok((compress_brotli(&blob)?, TileCompression::Brotli));
}
if target.compressions.contains(TileCompression::Gzip) {
return Ok((compress_gzip(&blob)?, TileCompression::Gzip));
}
}
Ok((blob, TileCompression::Uncompressed))
}
TileCompression::Gzip => {
if target.compression_goal != IsIncompressible && target.compressions.contains(TileCompression::Zstd) {
let decompressed = decompress_gzip(&blob)?;
let compressed_zstd = compress_zstd(&decompressed)?;
return Ok((compressed_zstd, TileCompression::Zstd));
}
if target.compression_goal != IsIncompressible && target.compressions.contains(TileCompression::Brotli) {
let decompressed = decompress_gzip(&blob)?;
let compressed_brotli = compress_brotli(&decompressed)?;
return Ok((compressed_brotli, TileCompression::Brotli));
}
if target.compressions.contains(TileCompression::Gzip) {
return Ok((blob, TileCompression::Gzip));
}
let decompressed = decompress_gzip(&blob)?;
Ok((decompressed, TileCompression::Uncompressed))
}
TileCompression::Brotli => {
if target.compressions.contains(TileCompression::Brotli) {
return Ok((blob, TileCompression::Brotli));
}
let decompressed = decompress_brotli(&blob)?;
if target.compression_goal != IsIncompressible && target.compressions.contains(TileCompression::Zstd) {
let compressed_zstd = compress_zstd(&decompressed)?;
return Ok((compressed_zstd, TileCompression::Zstd));
}
if target.compression_goal != IsIncompressible && target.compressions.contains(TileCompression::Gzip) {
let compressed_gzip = compress_gzip(&decompressed)?;
return Ok((compressed_gzip, TileCompression::Gzip));
}
Ok((decompressed, TileCompression::Uncompressed))
}
TileCompression::Zstd => {
if target.compressions.contains(TileCompression::Zstd) {
return Ok((blob, TileCompression::Zstd));
}
if target.compression_goal != IsIncompressible && target.compressions.contains(TileCompression::Brotli) {
let decompressed = decompress_zstd(&blob)?;
let compressed_brotli = compress_brotli(&decompressed)?;
return Ok((compressed_brotli, TileCompression::Brotli));
}
let decompressed = decompress_zstd(&blob)?;
if target.compression_goal != IsIncompressible && target.compressions.contains(TileCompression::Gzip) {
let compressed_gzip = compress_gzip(&decompressed)?;
return Ok((compressed_gzip, TileCompression::Gzip));
}
Ok((decompressed, TileCompression::Uncompressed))
}
}
}
#[context("Recompressing blob from {input_compression:?} to {output_compression:?}")]
pub fn recompress(blob: Blob, input_compression: TileCompression, output_compression: TileCompression) -> Result<Blob> {
if input_compression == output_compression {
return Ok(blob);
}
let decompressed = decompress(blob, input_compression)?;
let recompressed = compress(decompressed, output_compression)?;
Ok(recompressed)
}
#[context("Compressing blob with algorithm: {compression:?}")]
pub fn compress(blob: Blob, compression: TileCompression) -> Result<Blob> {
match compression {
TileCompression::Uncompressed => Ok(blob),
TileCompression::Gzip => compress_gzip(&blob),
TileCompression::Brotli => compress_brotli(&blob),
TileCompression::Zstd => compress_zstd(&blob),
}
}
#[context("Decompressing blob with algorithm: {compression:?}")]
pub fn decompress(blob: Blob, compression: TileCompression) -> Result<Blob> {
match compression {
TileCompression::Uncompressed => Ok(blob),
TileCompression::Gzip => decompress_gzip(&blob),
TileCompression::Brotli => decompress_brotli(&blob),
TileCompression::Zstd => decompress_zstd(&blob),
}
}
#[context("Decompressing blob ref with algorithm: {compression:?}")]
pub fn decompress_ref(blob: &Blob, compression: TileCompression) -> Result<Blob> {
match compression {
TileCompression::Uncompressed => Ok(blob.clone()),
TileCompression::Gzip => decompress_gzip(blob),
TileCompression::Brotli => decompress_brotli(blob),
TileCompression::Zstd => decompress_zstd(blob),
}
}
#[cfg(test)]
mod tests {
use super::super::test_utils::generate_test_data;
use super::*;
use enumset::{EnumSet, enum_set};
use rstest::{fixture, rstest};
use CompressionGoal::*;
use TileCompression::*;
struct TestBlobs {
original: Blob,
gzip: Blob,
brotli: Blob,
zstd: Blob,
}
impl TestBlobs {
fn get(&self, compression: TileCompression) -> Blob {
match compression {
Uncompressed => self.original.clone(),
Gzip => self.gzip.clone(),
Brotli => self.brotli.clone(),
Zstd => self.zstd.clone(),
}
}
}
#[fixture]
fn blobs() -> TestBlobs {
let original = generate_test_data(100);
TestBlobs {
gzip: compress_gzip(&original).unwrap(),
brotli: compress_brotli(&original).unwrap(),
zstd: compress_zstd(&original).unwrap(),
original,
}
}
fn allowed(types: &[TileCompression]) -> EnumSet<TileCompression> {
types.iter().copied().collect()
}
#[rstest]
#[case::best_from_uncompressed(Uncompressed, &[Uncompressed, Gzip, Brotli, Zstd], UseBestCompression, Zstd)]
#[case::best_from_gzip(Gzip, &[Uncompressed, Gzip, Brotli, Zstd], UseBestCompression, Zstd)]
#[case::best_from_brotli(Brotli, &[Uncompressed, Gzip, Brotli, Zstd], UseBestCompression, Brotli)]
#[case::best_from_zstd(Zstd, &[Uncompressed, Gzip, Brotli, Zstd], UseBestCompression, Zstd)]
#[case::fast_from_uncompressed(Uncompressed, &[Uncompressed, Gzip, Brotli, Zstd], UseFastCompression, Uncompressed)]
#[case::fast_gzip_to_gzip(Gzip, &[Uncompressed, Gzip], UseFastCompression, Gzip)]
#[case::fast_gzip_to_brotli(Gzip, &[Uncompressed, Brotli], UseFastCompression, Brotli)]
#[case::fast_gzip_to_zstd(Gzip, &[Uncompressed, Zstd], UseFastCompression, Zstd)]
#[case::fast_from_brotli(Brotli, &[Uncompressed, Gzip, Brotli, Zstd], UseFastCompression, Brotli)]
#[case::fast_from_zstd(Zstd, &[Uncompressed, Gzip, Brotli, Zstd], UseFastCompression, Zstd)]
#[case::incompressible_uncompressed(Uncompressed, &[Uncompressed], IsIncompressible, Uncompressed)]
#[case::incompressible_gzip(Gzip, &[Uncompressed, Gzip], IsIncompressible, Gzip)]
#[case::incompressible_brotli(Brotli, &[Uncompressed, Brotli], IsIncompressible, Brotli)]
#[case::incompressible_zstd(Zstd, &[Uncompressed, Zstd], IsIncompressible, Zstd)]
fn optimize_compression_scenarios(
blobs: TestBlobs,
#[case] input: TileCompression,
#[case] allowed_types: &[TileCompression],
#[case] goal: CompressionGoal,
#[case] expected: TileCompression,
) {
let target = TargetCompression {
compressions: allowed(allowed_types),
compression_goal: goal,
};
let (result_blob, result_compression) = optimize_compression(blobs.get(input), input, &target).unwrap();
assert_eq!(result_compression, expected);
assert_eq!(result_blob, blobs.get(expected));
}
#[test]
fn should_recompress_correctly() -> Result<()> {
let original_data = generate_test_data(1_000);
let gzip_data = compress_gzip(&original_data)?;
let brotli_data = compress_brotli(&original_data)?;
let recompressed = recompress(gzip_data.clone(), TileCompression::Gzip, TileCompression::Brotli)?;
let decompressed = decompress_brotli(&recompressed)?;
assert_eq!(original_data, decompressed);
let recompressed = recompress(brotli_data.clone(), TileCompression::Brotli, TileCompression::Gzip)?;
let decompressed = decompress_gzip(&recompressed)?;
assert_eq!(original_data, decompressed);
let recompressed = recompress(gzip_data.clone(), TileCompression::Gzip, TileCompression::Gzip)?;
assert_eq!(recompressed, gzip_data);
let recompressed = recompress(
original_data.clone(),
TileCompression::Uncompressed,
TileCompression::Gzip,
)?;
let decompressed = decompress_gzip(&recompressed)?;
assert_eq!(original_data, decompressed);
Ok(())
}
#[test]
fn should_handle_no_compression_correctly() -> Result<()> {
let data = generate_test_data(500);
let result = optimize_compression(
data.clone(),
TileCompression::Uncompressed,
&TargetCompression::from(TileCompression::Uncompressed),
)?;
assert_eq!(result.0, data);
assert_eq!(result.1, TileCompression::Uncompressed);
Ok(())
}
#[test]
fn should_fail_when_no_compressions_allowed() {
let data = generate_test_data(100);
let target = TargetCompression {
compressions: EnumSet::empty(),
compression_goal: CompressionGoal::UseBestCompression,
};
let result = optimize_compression(data, TileCompression::Uncompressed, &target);
assert!(result.is_err());
}
#[test]
fn should_fail_when_uncompressed_not_allowed() {
let data = generate_test_data(100);
let target = TargetCompression {
compressions: enum_set!(TileCompression::Gzip | TileCompression::Brotli),
compression_goal: CompressionGoal::UseBestCompression,
};
let result = optimize_compression(data, TileCompression::Uncompressed, &target);
assert!(result.is_err());
}
#[test]
fn should_handle_empty_compression_set_in_recompress() -> Result<()> {
let original_data = generate_test_data(100);
let recompressed = recompress(
original_data.clone(),
TileCompression::Uncompressed,
TileCompression::Uncompressed,
)?;
assert_eq!(recompressed, original_data);
Ok(())
}
#[test]
fn test_generic_compress_dispatch() -> Result<()> {
let data = generate_test_data(1024);
let result = compress(data.clone(), TileCompression::Uncompressed)?;
assert_eq!(result, data);
let gzip = compress(data.clone(), TileCompression::Gzip)?;
assert_eq!(gzip, compress_gzip(&data)?);
let brotli = compress(data.clone(), TileCompression::Brotli)?;
assert_eq!(brotli, compress_brotli(&data)?);
Ok(())
}
#[test]
fn test_generic_decompress_dispatch() -> Result<()> {
let data = generate_test_data(512);
let gzip = compress_gzip(&data)?;
let brotli = compress_brotli(&data)?;
let res_u = decompress(data.clone(), TileCompression::Uncompressed)?;
assert_eq!(res_u, data);
let res_g = decompress(gzip.clone(), TileCompression::Gzip)?;
assert_eq!(res_g, decompress_gzip(&gzip)?);
let res_b = decompress(brotli.clone(), TileCompression::Brotli)?;
assert_eq!(res_b, decompress_brotli(&brotli)?);
Ok(())
}
#[test]
fn test_optimize_compression_decompress_when_only_uncompressed_allowed() -> Result<()> {
let original = generate_test_data(256);
let gzip_blob = compress_gzip(&original)?;
let target = TargetCompression::from_none(); let (out_blob, out_comp) = optimize_compression(gzip_blob.clone(), TileCompression::Gzip, &target)?;
assert_eq!(out_comp, TileCompression::Uncompressed);
assert_eq!(out_blob, original);
let brotli_blob = compress_brotli(&original)?;
let (out_blob2, out_comp2) = optimize_compression(brotli_blob.clone(), TileCompression::Brotli, &target)?;
assert_eq!(out_comp2, TileCompression::Uncompressed);
assert_eq!(out_blob2, original);
Ok(())
}
}