unpackr 0.1.0

Production-quality, low-disk-space archive extraction engine with progressive in-place storage reclamation and native GUI
pub mod bench;
pub mod cancel;
pub mod completions;
pub mod inspect;
pub mod resume;
pub mod status;
pub mod verify;

use clap::{Parser, Subcommand};
use std::path::PathBuf;

#[derive(Parser, Debug)]
#[command(
    name = "unpackr",
    author = "Unpackr Authors",
    version = "0.1.0",
    about = "Production-quality, low-disk-space archive extraction engine",
    long_about = "Unpackr extracts large ZIP archives while minimizing peak disk space usage through incremental verified extraction and storage reclamation."
)]
pub struct Cli {
    #[command(subcommand)]
    pub command: Option<Commands>,

    /// Verbose logging output
    #[arg(short, long, global = true)]
    pub verbose: bool,

    /// Suppress progress bar output
    #[arg(short, long, global = true)]
    pub quiet: bool,
}

#[derive(Subcommand, Debug)]
pub enum Commands {
    /// Launch the modern minimalist native desktop GUI (default when no subcommand is given)
    #[command(visible_alias = "gui")]
    Ui {
        /// Optional path to a target archive (.zip) to load on startup
        archive: Option<PathBuf>,
    },

    /// Inspect an archive and display detailed entry metadata and offsets without extracting
    #[command(visible_alias = "i")]
    Inspect {
        /// Path to the target archive (.zip)
        archive: PathBuf,

        /// Output inspection details in JSON format
        #[arg(long)]
        json: bool,

        /// Limit the number of entries displayed in the table (default: 50)
        #[arg(long, default_value = "50")]
        limit: usize,
    },

    /// Extract an archive with streaming verification and low disk space usage
    #[command(visible_alias = "x")]
    Extract {
        /// Path to the archive
        archive: PathBuf,

        /// Destination directory
        destination: PathBuf,

        /// Collision policy if destination files exist: fail, skip, overwrite, rename (default: fail)
        #[arg(long, default_value = "fail", value_enum)]
        collision: crate::extraction::CollisionPolicy,

        /// Disable sparse file hole detection
        #[arg(long)]
        no_sparse: bool,

        /// Maximum allowed compression ratio before aborting (zip bomb protection, default: 100.0)
        #[arg(long, default_value = "100.0")]
        max_ratio: f64,

        /// Reclaim archive storage in-place during extraction (experimental/destructive mode)
        #[arg(long)]
        reclaim_archive: bool,

        /// Maximum allowed total uncompressed bytes across all entries (DoS defense)
        #[arg(long)]
        max_total_size: Option<u64>,

        /// Maximum allowed uncompressed bytes for any single entry (DoS defense)
        #[arg(long)]
        max_file_size: Option<u64>,

        /// Maximum allowed entry count in archive (DoS defense, default: 100,000)
        #[arg(long)]
        max_entries: Option<usize>,

        /// Custom state directory for crash recovery journals (default: <destination>/.unpackr/)
        #[arg(long)]
        state_dir: Option<PathBuf>,

        /// Output extraction summary in JSON format
        #[arg(long)]
        json: bool,
    },

    /// Resume an interrupted extraction job
    #[command(visible_alias = "r")]
    Resume {
        /// Job ID, destination directory, archive path, or manifest path
        target: String,

        /// Optional destination directory (if first argument is an archive)
        destination: Option<PathBuf>,

        /// Explicit path to archive if moved or not found in manifest
        #[arg(short, long)]
        archive: Option<PathBuf>,

        /// Retry entries that previously failed
        #[arg(long)]
        retry_failed: bool,

        /// Fully verify already extracted files before resuming
        #[arg(long)]
        verify: bool,

        /// Collision policy: fail, skip, overwrite, rename
        #[arg(long, value_enum)]
        collision: Option<crate::extraction::CollisionPolicy>,

        /// Reclaim archive storage in-place during resume
        #[arg(long)]
        reclaim_archive: bool,

        /// Maximum allowed total uncompressed bytes across all entries (DoS defense)
        #[arg(long)]
        max_total_size: Option<u64>,

        /// Maximum allowed uncompressed bytes for any single entry (DoS defense)
        #[arg(long)]
        max_file_size: Option<u64>,

        /// Maximum allowed entry count in archive (DoS defense)
        #[arg(long)]
        max_entries: Option<usize>,

        /// Output resume summary in JSON format
        #[arg(long)]
        json: bool,
    },

    /// Show current status and disk savings for a job
    #[command(visible_alias = "s")]
    Status {
        /// Job ID, destination directory, or manifest path to inspect
        job_id: String,

        /// Output status in JSON format
        #[arg(long)]
        json: bool,
    },

    /// Verify an extracted destination against archive metadata
    #[command(visible_alias = "v")]
    Verify {
        /// Job ID, destination directory, or manifest path to verify
        job_id: String,

        /// Output verification results in JSON format
        #[arg(long)]
        json: bool,
    },

    /// Cancel an interrupted or incomplete extraction job
    #[command(visible_alias = "c")]
    Cancel {
        /// Job ID, destination directory, or manifest path to cancel
        job_id: String,

        /// Clean up and remove extracted files and destination directory
        #[arg(long)]
        clean: bool,

        /// Output cancellation result in JSON format
        #[arg(long)]
        json: bool,
    },

    /// Benchmark extraction performance and peak storage comparison on an archive
    #[command(visible_alias = "b")]
    Bench {
        /// Optional path to an archive to benchmark (or omit to generate an automated test workload)
        archive: Option<PathBuf>,

        /// Number of entries if generating a synthetic benchmark workload (default: 10)
        #[arg(long, default_value = "10")]
        entries: usize,

        /// Entry size in megabytes if generating a synthetic workload (default: 4)
        #[arg(long, default_value = "4")]
        size_mb: usize,

        /// Output benchmark results in JSON format
        #[arg(long)]
        json: bool,
    },

    /// Generate shell auto-completion scripts (bash, zsh, fish, elvish, powershell)
    Completions {
        /// Shell to generate completions for
        #[arg(value_enum)]
        shell: clap_complete::Shell,
    },
}