use-ar 0.1.0

Unix ar archive labels, extensions, and entry metadata primitives for RustUse
Documentation
#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]

//! Unix `ar` archive labels and entry kind metadata for `RustUse`.

use core::fmt;

/// Common static-library archive extension.
pub const AR_STATIC_LIBRARY_EXTENSION: &str = "a";
/// Common Unix ar extension.
pub const AR_EXTENSION: &str = "ar";
/// Common Debian package extension backed by ar containers.
pub const DEBIAN_PACKAGE_EXTENSION: &str = "deb";
/// Common ar-related extensions.
pub const AR_EXTENSIONS: &[&str] = &["a", "ar", "deb"];

/// Unix `ar` format labels.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum ArFormat {
    /// GNU ar variant.
    Gnu,
    /// BSD ar variant.
    Bsd,
    /// Common ar container label.
    Common,
    /// Unknown or intentionally unspecified ar format.
    #[default]
    Unknown,
}

impl ArFormat {
    /// Returns a stable lowercase label.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Gnu => "gnu",
            Self::Bsd => "bsd",
            Self::Common => "common",
            Self::Unknown => "unknown",
        }
    }
}

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

/// Unix `ar` entry kind labels.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum ArEntryKind {
    /// Regular archive member.
    Member,
    /// Symbol table member.
    SymbolTable,
    /// String table member.
    StringTable,
    /// Unknown or unsupported ar entry kind.
    #[default]
    Unknown,
}

impl ArEntryKind {
    /// Returns a stable lowercase label.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Member => "member",
            Self::SymbolTable => "symbol-table",
            Self::StringTable => "string-table",
            Self::Unknown => "unknown",
        }
    }
}

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

/// Returns whether `extension` is a known ar extension label.
#[must_use]
pub fn is_ar_extension(extension: &str) -> bool {
    matches!(normalize_extension(extension).as_str(), "a" | "ar" | "deb")
}

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

    matches!(parts.as_slice(), [.., last] if matches!(last.as_str(), "a" | "ar" | "deb"))
}

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::{AR_EXTENSIONS, ArEntryKind, ArFormat, is_ar_extension, is_ar_filename};

    #[test]
    fn detects_ar_extensions() {
        assert!(is_ar_extension(".a"));
        assert!(is_ar_extension("ar"));
        assert!(is_ar_extension("deb"));
        assert_eq!(AR_EXTENSIONS[0], "a");
    }

    #[test]
    fn detects_ar_filenames() {
        assert!(is_ar_filename("libexample.a"));
        assert!(is_ar_filename("package.DEB"));
        assert!(!is_ar_filename("bundle.zip"));
    }

    #[test]
    fn exposes_default_and_unknown_labels() {
        assert_eq!(ArFormat::default(), ArFormat::Unknown);
        assert_eq!(ArFormat::Bsd.as_str(), "bsd");
        assert_eq!(ArEntryKind::default(), ArEntryKind::Unknown);
        assert_eq!(ArEntryKind::StringTable.as_str(), "string-table");
    }
}