exarch-core 0.2.9

Memory-safe archive extraction library with security validation
Documentation
//! Archive manifest types for listing.

use std::path::PathBuf;
use std::time::SystemTime;

use crate::formats::detect::ArchiveType;

/// Complete manifest of archive contents.
///
/// Generated by `list_archive()`, contains metadata about all entries
/// without extracting them to disk.
///
/// # Examples
///
/// ```no_run
/// use exarch_core::SecurityConfig;
/// use exarch_core::list_archive;
///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let config = SecurityConfig::default();
/// let manifest = list_archive("archive.tar.gz", &config)?;
///
/// for entry in &manifest.entries {
///     println!("{}: {} bytes", entry.path.display(), entry.size);
/// }
/// # Ok(())
/// # }
/// ```
#[derive(Debug, Clone)]
pub struct ArchiveManifest {
    /// All entries in the archive (files, dirs, symlinks, hardlinks)
    pub entries: Vec<ArchiveEntry>,

    /// Total number of entries
    pub total_entries: usize,

    /// Total uncompressed size in bytes
    pub total_size: u64,

    /// Archive format
    pub format: ArchiveType,
}

impl ArchiveManifest {
    /// Creates a new empty manifest.
    #[must_use]
    pub fn new(format: ArchiveType) -> Self {
        Self {
            entries: Vec::new(),
            total_entries: 0,
            total_size: 0,
            format,
        }
    }

    /// Adds an entry to the manifest.
    pub fn add_entry(&mut self, entry: ArchiveEntry) {
        self.total_size += entry.size;
        self.total_entries += 1;
        self.entries.push(entry);
    }
}

/// Single entry in archive manifest.
///
/// Contains metadata about a file, directory, symlink, or hardlink
/// without extracting it to disk.
#[derive(Debug, Clone)]
pub struct ArchiveEntry {
    /// Entry path (relative, as stored in archive)
    pub path: PathBuf,

    /// Entry type (File, Directory, Symlink, Hardlink)
    pub entry_type: ManifestEntryType,

    /// Uncompressed size in bytes (0 for directories)
    pub size: u64,

    /// Compressed size in bytes (if available, ZIP only)
    pub compressed_size: Option<u64>,

    /// File permissions (Unix mode)
    pub mode: Option<u32>,

    /// Modification time
    pub modified: Option<SystemTime>,

    /// Symlink target (if `entry_type` is Symlink)
    pub symlink_target: Option<PathBuf>,

    /// Hardlink target (if `entry_type` is Hardlink)
    pub hardlink_target: Option<PathBuf>,
}

impl ArchiveEntry {
    /// Returns the compression ratio if compressed size is available.
    #[must_use]
    pub fn compression_ratio(&self) -> Option<f64> {
        self.compressed_size
            .filter(|&c| c > 0)
            .map(|c| self.size as f64 / c as f64)
    }
}

/// Entry type in manifest (does NOT require validation).
///
/// Unlike `EntryType`, this enum is used for read-only listing
/// and does not enforce security constraints.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ManifestEntryType {
    /// Regular file
    File,

    /// Directory
    Directory,

    /// Symbolic link
    Symlink,

    /// Hard link
    Hardlink,
}

impl std::fmt::Display for ManifestEntryType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::File => write!(f, "File"),
            Self::Directory => write!(f, "Directory"),
            Self::Symlink => write!(f, "Symlink"),
            Self::Hardlink => write!(f, "Hardlink"),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_manifest_new() {
        let manifest = ArchiveManifest::new(ArchiveType::TarGz);
        assert_eq!(manifest.total_entries, 0);
        assert_eq!(manifest.total_size, 0);
        assert_eq!(manifest.entries.len(), 0);
    }

    #[test]
    fn test_manifest_add_entry() {
        let mut manifest = ArchiveManifest::new(ArchiveType::TarGz);

        let entry = ArchiveEntry {
            path: PathBuf::from("file.txt"),
            entry_type: ManifestEntryType::File,
            size: 1024,
            compressed_size: Some(512),
            mode: Some(0o644),
            modified: None,
            symlink_target: None,
            hardlink_target: None,
        };

        manifest.add_entry(entry);

        assert_eq!(manifest.total_entries, 1);
        assert_eq!(manifest.total_size, 1024);
        assert_eq!(manifest.entries.len(), 1);
    }

    #[test]
    fn test_compression_ratio() {
        let entry = ArchiveEntry {
            path: PathBuf::from("file.txt"),
            entry_type: ManifestEntryType::File,
            size: 1000,
            compressed_size: Some(100),
            mode: None,
            modified: None,
            symlink_target: None,
            hardlink_target: None,
        };

        assert_eq!(entry.compression_ratio(), Some(10.0));
    }

    #[test]
    fn test_compression_ratio_no_compressed() {
        let entry = ArchiveEntry {
            path: PathBuf::from("file.txt"),
            entry_type: ManifestEntryType::File,
            size: 1000,
            compressed_size: None,
            mode: None,
            modified: None,
            symlink_target: None,
            hardlink_target: None,
        };

        assert_eq!(entry.compression_ratio(), None);
    }

    #[test]
    fn test_manifest_entry_type_display() {
        assert_eq!(ManifestEntryType::File.to_string(), "File");
        assert_eq!(ManifestEntryType::Directory.to_string(), "Directory");
        assert_eq!(ManifestEntryType::Symlink.to_string(), "Symlink");
        assert_eq!(ManifestEntryType::Hardlink.to_string(), "Hardlink");
    }
}