use-cpio 0.1.0

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

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

use core::fmt;

/// Common CPIO file extension.
pub const CPIO_EXTENSION: &str = "cpio";
/// Common gzip-compressed CPIO extension.
pub const CPIO_GZIP_EXTENSION: &str = "cpio.gz";
/// Common xz-compressed CPIO extension.
pub const CPIO_XZ_EXTENSION: &str = "cpio.xz";
/// Common zstd-compressed CPIO extension.
pub const CPIO_ZSTD_EXTENSION: &str = "cpio.zst";
/// Common CPIO-related extensions.
pub const CPIO_EXTENSIONS: &[&str] = &["cpio", "cpio.gz", "cpio.xz", "cpio.zst"];

/// CPIO format variant labels.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum CpioFormat {
    /// Binary CPIO format.
    Binary,
    /// Old ASCII CPIO format.
    OldAscii,
    /// New ASCII CPIO format.
    NewAscii,
    /// New ASCII CPIO format with CRC metadata.
    CrcAscii,
    /// Unknown or intentionally unspecified CPIO format.
    #[default]
    Unknown,
}

impl CpioFormat {
    /// Returns a stable lowercase label.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Binary => "binary",
            Self::OldAscii => "old-ascii",
            Self::NewAscii => "new-ascii",
            Self::CrcAscii => "crc-ascii",
            Self::Unknown => "unknown",
        }
    }
}

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

/// CPIO entry kind labels.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum CpioEntryKind {
    /// File entry.
    File,
    /// Directory entry.
    Directory,
    /// Symbolic link entry.
    Symlink,
    /// Device entry.
    Device,
    /// FIFO entry.
    Fifo,
    /// Socket entry.
    Socket,
    /// Unknown or unsupported CPIO entry kind.
    #[default]
    Unknown,
}

impl CpioEntryKind {
    /// Returns a stable lowercase label.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::File => "file",
            Self::Directory => "directory",
            Self::Symlink => "symlink",
            Self::Device => "device",
            Self::Fifo => "fifo",
            Self::Socket => "socket",
            Self::Unknown => "unknown",
        }
    }
}

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

/// Returns whether `extension` is a known CPIO extension label.
#[must_use]
pub fn is_cpio_extension(extension: &str) -> bool {
    matches!(
        normalize_extension(extension).as_str(),
        "cpio" | "cpio.gz" | "cpio.xz" | "cpio.zst" | "cpio.zstd"
    )
}

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

    match parts.as_slice() {
        [.., last] if last == "cpio" => true,
        [.., previous, last]
            if previous == "cpio" && matches!(last.as_str(), "gz" | "xz" | "zst" | "zstd") =>
        {
            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::{CPIO_EXTENSIONS, CpioEntryKind, CpioFormat, is_cpio_extension, is_cpio_filename};

    #[test]
    fn detects_cpio_extensions() {
        assert!(is_cpio_extension(".cpio"));
        assert!(is_cpio_extension("cpio.gz"));
        assert!(is_cpio_extension("cpio.zst"));
        assert_eq!(CPIO_EXTENSIONS[0], "cpio");
    }

    #[test]
    fn detects_cpio_filenames() {
        assert!(is_cpio_filename("initramfs.cpio"));
        assert!(is_cpio_filename("initramfs.CPIO.XZ"));
        assert!(!is_cpio_filename("bundle.tar"));
    }

    #[test]
    fn exposes_default_and_unknown_labels() {
        assert_eq!(CpioFormat::default(), CpioFormat::Unknown);
        assert_eq!(CpioFormat::CrcAscii.as_str(), "crc-ascii");
        assert_eq!(CpioEntryKind::default(), CpioEntryKind::Unknown);
        assert_eq!(CpioEntryKind::Socket.as_str(), "socket");
    }
}