use-mtree 0.1.0

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

//! mtree manifest labels and entry metadata for `RustUse`.

use core::fmt;

/// Common mtree manifest extension.
pub const MTREE_EXTENSION: &str = "mtree";
/// Common gzip-compressed mtree manifest extension.
pub const MTREE_GZIP_EXTENSION: &str = "mtree.gz";
/// Common mtree-related extensions.
pub const MTREE_EXTENSIONS: &[&str] = &["mtree", "mtree.gz"];

/// mtree dialect labels.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum MtreeFormat {
    /// BSD mtree label.
    Bsd,
    /// NetBSD mtree label.
    NetBsd,
    /// FreeBSD mtree label.
    FreeBsd,
    /// Unknown or intentionally unspecified mtree format.
    #[default]
    Unknown,
}

impl MtreeFormat {
    /// Returns a stable lowercase label.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Bsd => "bsd",
            Self::NetBsd => "netbsd",
            Self::FreeBsd => "freebsd",
            Self::Unknown => "unknown",
        }
    }
}

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

/// mtree entry kind labels.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum MtreeEntryKind {
    /// File entry.
    File,
    /// Directory entry.
    Directory,
    /// Link entry.
    Link,
    /// Device entry.
    Device,
    /// Unknown or unsupported entry kind.
    #[default]
    Unknown,
}

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

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

/// mtree keyword labels.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum MtreeKeyword {
    /// Entry type keyword.
    Type,
    /// User ID keyword.
    Uid,
    /// Group ID keyword.
    Gid,
    /// Mode keyword.
    Mode,
    /// Size keyword.
    Size,
    /// Time keyword.
    Time,
    /// SHA-256 digest keyword.
    Sha256Digest,
    /// Unknown or unsupported keyword.
    #[default]
    Unknown,
}

impl MtreeKeyword {
    /// Returns a stable lowercase label.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Type => "type",
            Self::Uid => "uid",
            Self::Gid => "gid",
            Self::Mode => "mode",
            Self::Size => "size",
            Self::Time => "time",
            Self::Sha256Digest => "sha256digest",
            Self::Unknown => "unknown",
        }
    }
}

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

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

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

    match parts.as_slice() {
        [.., last] if last == "mtree" => true,
        [.., previous, last] if previous == "mtree" && last == "gz" => 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::{
        MTREE_EXTENSIONS, MtreeEntryKind, MtreeFormat, MtreeKeyword, is_mtree_extension,
        is_mtree_filename,
    };

    #[test]
    fn detects_mtree_extensions() {
        assert!(is_mtree_extension(".mtree"));
        assert!(is_mtree_extension("mtree.gz"));
        assert_eq!(MTREE_EXTENSIONS[0], "mtree");
    }

    #[test]
    fn detects_mtree_filenames() {
        assert!(is_mtree_filename("manifest.mtree"));
        assert!(is_mtree_filename("manifest.MTREE.GZ"));
        assert!(!is_mtree_filename("bundle.zip"));
    }

    #[test]
    fn exposes_default_and_unknown_labels() {
        assert_eq!(MtreeFormat::default(), MtreeFormat::Unknown);
        assert_eq!(MtreeFormat::NetBsd.as_str(), "netbsd");
        assert_eq!(MtreeEntryKind::default(), MtreeEntryKind::Unknown);
        assert_eq!(MtreeEntryKind::Link.as_str(), "link");
        assert_eq!(MtreeKeyword::default(), MtreeKeyword::Unknown);
        assert_eq!(MtreeKeyword::Sha256Digest.as_str(), "sha256digest");
    }
}