use-bzip2 0.1.0

Bzip2 compression labels, extensions, and option metadata for RustUse
Documentation
#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]

//! Bzip2 labels and option metadata for `RustUse`.

use core::fmt;

/// Common bzip2 file extension.
pub const BZIP2_EXTENSION: &str = "bz2";
/// Common abbreviated bzip2-compressed tar extension.
pub const TBZ_EXTENSION: &str = "tbz";
/// Common abbreviated bzip2-compressed tar extension.
pub const TBZ2_EXTENSION: &str = "tbz2";
/// Common bzip2-compressed tar extension.
pub const TAR_BZIP2_EXTENSION: &str = "tar.bz2";
/// Common bzip2-related extensions.
pub const BZIP2_EXTENSIONS: &[&str] = &["bz2", "bzip2", "tbz", "tbz2", "tar.bz2"];

/// Bzip2 extension labels.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum Bzip2Extension {
    /// `.bz2` payload.
    Bz2,
    /// `.tbz` tar shorthand.
    Tbz,
    /// `.tbz2` tar shorthand.
    Tbz2,
    /// `.tar.bz2` tar plus bzip2 extension.
    TarBz2,
    /// Unknown or unsupported bzip2 extension label.
    #[default]
    Unknown,
}

impl Bzip2Extension {
    /// Returns a stable lowercase label.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Bz2 => "bz2",
            Self::Tbz => "tbz",
            Self::Tbz2 => "tbz2",
            Self::TarBz2 => "tar.bz2",
            Self::Unknown => "unknown",
        }
    }

    /// Detects a bzip2 extension label.
    #[must_use]
    pub fn from_extension(extension: &str) -> Self {
        match normalize_extension(extension).as_str() {
            "bz2" | "bzip2" => Self::Bz2,
            "tbz" => Self::Tbz,
            "tbz2" => Self::Tbz2,
            "tar.bz2" | "tar.bzip2" => Self::TarBz2,
            _ => Self::Unknown,
        }
    }
}

impl fmt::Display for Bzip2Extension {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(self.as_str())
    }
}

/// Bzip2 compression level labels.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum Bzip2Level {
    /// Prefer fastest compression.
    Fastest,
    /// Balance speed and compressed size.
    #[default]
    Balanced,
    /// Prefer best compressed size.
    Best,
    /// Caller-specified numeric level.
    Numeric(u32),
    /// Unknown or intentionally unspecified level.
    Unknown,
}

impl Bzip2Level {
    /// Returns a stable lowercase label.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Fastest => "fastest",
            Self::Balanced => "balanced",
            Self::Best => "best",
            Self::Numeric(_) => "numeric",
            Self::Unknown => "unknown",
        }
    }

    /// Returns the numeric level when this is [`Bzip2Level::Numeric`].
    #[must_use]
    pub const fn numeric(self) -> Option<u32> {
        match self {
            Self::Numeric(level) => Some(level),
            Self::Fastest | Self::Balanced | Self::Best | Self::Unknown => None,
        }
    }
}

/// High-level bzip2 usage profile.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum Bzip2Profile {
    /// Prefer compression speed.
    Speed,
    /// Prefer compressed size.
    Size,
    /// Balance speed and size.
    #[default]
    Balanced,
    /// Prefer compatibility with broad tooling.
    Compatibility,
    /// Unknown or intentionally unspecified profile.
    Unknown,
}

impl Bzip2Profile {
    /// Returns a stable lowercase label.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Speed => "speed",
            Self::Size => "size",
            Self::Balanced => "balanced",
            Self::Compatibility => "compatibility",
            Self::Unknown => "unknown",
        }
    }
}

/// Returns whether `extension` is a known bzip2 extension label.
#[must_use]
pub fn is_bzip2_extension(extension: &str) -> bool {
    !matches!(
        Bzip2Extension::from_extension(extension),
        Bzip2Extension::Unknown
    )
}

/// Returns whether `name` has a known bzip2 filename encoding.
#[must_use]
pub fn is_bzip2_filename(name: &str) -> bool {
    let parts = filename_parts(name);

    match parts.as_slice() {
        [.., last] if matches!(last.as_str(), "bz2" | "bzip2" | "tbz" | "tbz2") => true,
        [.., previous, last] if previous == "tar" && matches!(last.as_str(), "bz2" | "bzip2") => {
            true
        },
        _ => false,
    }
}

fn normalize_extension(extension: &str) -> String {
    extension
        .trim()
        .trim_start_matches('.')
        .to_ascii_lowercase()
}

fn filename_parts(name: &str) -> Vec<String> {
    name.trim()
        .to_ascii_lowercase()
        .rsplit(['/', '\\'])
        .next()
        .unwrap_or_default()
        .trim_start_matches('.')
        .split('.')
        .filter(|part| !part.is_empty())
        .map(str::to_owned)
        .collect()
}

#[cfg(test)]
mod tests {
    use super::{
        BZIP2_EXTENSIONS, Bzip2Extension, Bzip2Level, Bzip2Profile, is_bzip2_extension,
        is_bzip2_filename,
    };

    #[test]
    fn detects_bzip2_extensions() {
        assert!(is_bzip2_extension(".bz2"));
        assert!(is_bzip2_extension("tar.bz2"));
        assert_eq!(Bzip2Extension::from_extension("tbz2"), Bzip2Extension::Tbz2);
        assert_eq!(BZIP2_EXTENSIONS[0], "bz2");
    }

    #[test]
    fn detects_bzip2_filenames() {
        assert!(is_bzip2_filename("release.tar.bz2"));
        assert!(is_bzip2_filename("bundle.TBZ2"));
        assert!(!is_bzip2_filename("bundle.zip"));
    }

    #[test]
    fn exposes_default_and_unknown_labels() {
        assert_eq!(Bzip2Extension::default(), Bzip2Extension::Unknown);
        assert_eq!(Bzip2Level::default().as_str(), "balanced");
        assert_eq!(Bzip2Level::Numeric(9).numeric(), Some(9));
        assert_eq!(Bzip2Profile::Unknown.as_str(), "unknown");
    }
}