rpgcpf 0.1.1

GCPF archive compression and decompression library
Documentation
//! Types and methods for working with [`Gcpf`] compression modes

use std::fmt;

use num_enum::{FromPrimitive, IntoPrimitive};

use crate::compressor::{self, Compressor};
use crate::decompressor::{self, Decompressor};
use crate::error::Error;

#[cfg(feature = "brotli")]
const HAS_BROTLI: bool = true;
#[cfg(not(feature = "brotli"))]
const HAS_BROTLI: bool = false;

#[cfg(feature = "deflate")]
const HAS_DEFLATE: bool = true;
#[cfg(not(feature = "deflate"))]
const HAS_DEFLATE: bool = false;

#[cfg(feature = "fastlz")]
const HAS_FASTLZ: bool = true;
#[cfg(not(feature = "fastlz"))]
const HAS_FASTLZ: bool = false;

#[cfg(feature = "gzip")]
const HAS_GZIP: bool = true;
#[cfg(not(feature = "gzip"))]
const HAS_GZIP: bool = false;

#[cfg(feature = "zstd")]
const HAS_ZSTD: bool = true;
#[cfg(not(feature = "zstd"))]
const HAS_ZSTD: bool = false;

/// Compression modes supported by Godot's GCPF
/// <https://github.com/godotengine/godot/blob/240f510fa786ee4af6a34ca9712a5d5d9745d637/core/io/compression.h#L46>
#[derive(Clone, Copy, Debug, Eq, FromPrimitive, Hash, IntoPrimitive, PartialEq)]
#[repr(u32)]
pub enum CompressionMode {
    #[allow(missing_docs)]
    FastLZ = 0,
    #[allow(missing_docs)]
    Deflate,
    #[allow(missing_docs)]
    Zstd,
    #[allow(missing_docs)]
    Gzip,
    #[allow(missing_docs)]
    Brotli,

    /// Unknown or unsupported compression modes
    #[num_enum(catch_all)]
    Unknown(u32),
}

impl fmt::Display for CompressionMode {
    #[cfg_attr(test, coverage(off))]
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{self:#?}")
    }
}

#[cfg(feature = "cli")]
impl clap::ValueEnum for CompressionMode {
    fn value_variants<'a>() -> &'a [Self] {
        &[
            CompressionMode::Brotli,
            CompressionMode::Deflate,
            CompressionMode::FastLZ,
            CompressionMode::Gzip,
            CompressionMode::Zstd,
        ]
    }

    fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
        match self {
            CompressionMode::Brotli => Some("brotli".into()),
            CompressionMode::Deflate => Some("deflate".into()),
            CompressionMode::FastLZ => Some("fastlz".into()),
            CompressionMode::Gzip => Some("gzip".into()),
            CompressionMode::Zstd => Some("zstd".into()),
            CompressionMode::Unknown(_) => None,
        }
    }
}

impl CompressionMode {
    /// Check if a compression mode is enabled at runtime
    #[cfg_attr(test, coverage(off))]
    #[must_use]
    pub fn is_enabled(&self) -> bool {
        match self {
            Self::FastLZ => HAS_FASTLZ,
            Self::Deflate => HAS_DEFLATE,
            Self::Zstd => HAS_ZSTD,
            Self::Gzip => HAS_GZIP,
            Self::Brotli => HAS_BROTLI,
            Self::Unknown(_) => false,
        }
    }

    /// Get an appropriate [`Compressor`] implementation for the mode
    ///
    /// # Errors
    ///
    /// Will return `Err` if:
    ///
    /// - `self` is [`Compressor::Unknown`] ([`Error::UnknownMode`])
    /// - `self` is disabled in the crate features ([`Error:NoCompressor`])
    #[cfg_attr(test, coverage(off))]
    pub fn get_compressor(&self) -> Result<Box<dyn Compressor>, Error> {
        match self {
            #[cfg(feature = "fastlz")]
            Self::FastLZ => Ok(Box::new(compressor::fastlz::FastLZCompressor::default())),

            #[cfg(feature = "deflate")]
            Self::Deflate => Ok(Box::new(compressor::deflate::DeflateCompressor::default())),

            #[cfg(feature = "zstd")]
            Self::Zstd => Ok(Box::new(compressor::zstd::ZstdCompressor::default())),

            #[cfg(feature = "gzip")]
            Self::Gzip => Ok(Box::new(compressor::gzip::GzipCompressor::default())),

            #[cfg(feature = "brotli")]
            Self::Brotli => Ok(Box::new(compressor::brotli::BrotliCompressor::default())),

            Self::Unknown(c) => Err(Error::UnknownMode(*c)),

            #[allow(unreachable_patterns)]
            mode => Err(Error::NoCompressor(*mode)),
        }
    }

    /// Get an appropriate [`Decompressor`] implementation for the mode
    ///
    /// # Errors
    ///
    /// Will return `Err` if:
    ///
    /// - `self` is [`Decompressor::Unknown`] ([`Error::UnknownMode`])
    /// - `self` is disabled in the crate features ([`Error:NoDecompressor`])
    #[cfg_attr(test, coverage(off))]
    pub fn get_decompressor(&self) -> Result<Box<dyn Decompressor>, Error> {
        match self {
            #[cfg(feature = "fastlz")]
            // TODO: use the `capacity_hint` argument
            Self::FastLZ => Ok(Box::new(decompressor::fastlz::FastLZDecompressor::default())),

            #[cfg(feature = "deflate")]
            Self::Deflate => Ok(Box::new(
                decompressor::deflate::DeflateDecompressor::default(),
            )),

            #[cfg(feature = "zstd")]
            Self::Zstd => Ok(Box::new(decompressor::zstd::ZstdDecompressor::default())),

            #[cfg(feature = "gzip")]
            Self::Gzip => Ok(Box::new(decompressor::gzip::GzipDecompressor::default())),

            #[cfg(feature = "brotli")]
            Self::Brotli => Ok(Box::new(decompressor::brotli::BrotliDecompressor::default())),

            Self::Unknown(c) => Err(Error::UnknownMode(*c)),

            #[allow(unreachable_patterns)]
            mode => Err(Error::NoDecompressor(*mode)),
        }
    }
}