use-archive-entry 0.1.0

Archive entry metadata primitives for RustUse
Documentation
#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]

//! Archive entry metadata primitives for `RustUse`.

use core::fmt;

/// Generic archive entry kinds.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum ArchiveEntryKind {
    /// Regular file entry.
    #[default]
    File,
    /// Directory entry.
    Directory,
    /// Symbolic link entry.
    Symlink,
    /// Hard link entry.
    Hardlink,
    /// Device entry.
    Device,
    /// FIFO entry.
    Fifo,
    /// Unknown or intentionally unspecified entry kind.
    Unknown,
}

impl ArchiveEntryKind {
    /// 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::Hardlink => "hardlink",
            Self::Device => "device",
            Self::Fifo => "fifo",
            Self::Unknown => "unknown",
        }
    }

    /// Returns whether the entry kind is link-like.
    #[must_use]
    pub const fn is_link(self) -> bool {
        matches!(self, Self::Symlink | Self::Hardlink)
    }
}

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

/// Generic archive entry metadata.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct ArchiveEntry {
    /// Archive-internal path.
    pub path: String,
    /// Archive entry kind.
    pub kind: ArchiveEntryKind,
    /// Entry payload size in bytes, when known.
    pub size: Option<u64>,
    /// Entry mode or permission bits, when known.
    pub mode: Option<u32>,
    /// Unix timestamp seconds for the modified time, when known.
    pub modified_unix_seconds: Option<i64>,
}

impl ArchiveEntry {
    /// Creates archive entry metadata from a path and kind.
    #[must_use]
    pub fn new(path: impl Into<String>, kind: ArchiveEntryKind) -> Self {
        Self {
            path: path.into(),
            kind,
            size: None,
            mode: None,
            modified_unix_seconds: None,
        }
    }

    /// Creates a regular file entry.
    #[must_use]
    pub fn file(path: impl Into<String>) -> Self {
        Self::new(path, ArchiveEntryKind::File)
    }

    /// Creates a directory entry.
    #[must_use]
    pub fn directory(path: impl Into<String>) -> Self {
        Self::new(path, ArchiveEntryKind::Directory)
    }

    /// Creates a symbolic link entry.
    #[must_use]
    pub fn symlink(path: impl Into<String>) -> Self {
        Self::new(path, ArchiveEntryKind::Symlink)
    }

    /// Adds known entry size metadata.
    #[must_use]
    pub const fn with_size(mut self, size: u64) -> Self {
        self.size = Some(size);
        self
    }

    /// Adds known entry mode metadata.
    #[must_use]
    pub const fn with_mode(mut self, mode: u32) -> Self {
        self.mode = Some(mode);
        self
    }

    /// Adds known modified timestamp metadata.
    #[must_use]
    pub const fn with_modified_unix_seconds(mut self, seconds: i64) -> Self {
        self.modified_unix_seconds = Some(seconds);
        self
    }

    /// Returns the archive-internal path.
    #[must_use]
    pub fn path(&self) -> &str {
        &self.path
    }

    /// Returns the archive entry kind.
    #[must_use]
    pub const fn kind(&self) -> ArchiveEntryKind {
        self.kind
    }

    /// Returns the known entry size.
    #[must_use]
    pub const fn size(&self) -> Option<u64> {
        self.size
    }

    /// Returns the known entry mode.
    #[must_use]
    pub const fn mode(&self) -> Option<u32> {
        self.mode
    }

    /// Returns the known modified timestamp.
    #[must_use]
    pub const fn modified_unix_seconds(&self) -> Option<i64> {
        self.modified_unix_seconds
    }

    /// Returns whether this is a regular file entry.
    #[must_use]
    pub const fn is_file(&self) -> bool {
        matches!(self.kind, ArchiveEntryKind::File)
    }

    /// Returns whether this is a directory entry.
    #[must_use]
    pub const fn is_directory(&self) -> bool {
        matches!(self.kind, ArchiveEntryKind::Directory)
    }

    /// Returns whether this is a symbolic link entry.
    #[must_use]
    pub const fn is_symlink(&self) -> bool {
        matches!(self.kind, ArchiveEntryKind::Symlink)
    }
}

#[cfg(test)]
mod tests {
    use super::{ArchiveEntry, ArchiveEntryKind};

    #[test]
    fn creates_file_entry_metadata() {
        let entry = ArchiveEntry::file("docs/readme.md")
            .with_size(128)
            .with_mode(0o644)
            .with_modified_unix_seconds(1_700_000_000);

        assert_eq!(entry.path(), "docs/readme.md");
        assert_eq!(entry.kind(), ArchiveEntryKind::File);
        assert_eq!(entry.size(), Some(128));
        assert_eq!(entry.mode(), Some(0o644));
        assert!(entry.is_file());
    }

    #[test]
    fn identifies_link_like_kinds() {
        assert!(ArchiveEntryKind::Symlink.is_link());
        assert!(ArchiveEntryKind::Hardlink.is_link());
        assert!(!ArchiveEntryKind::Directory.is_link());
    }
}