cargo-reclaim 0.2.1

Safe Cargo cleanup for target directories, stale artifacts, and Cargo home caches
Documentation
use std::fmt;
use std::path::{Path, PathBuf};

use crate::PolicyKind;

pub const DEFAULT_SCHEDULER_INSTANCE_NAME: &str = "cargo-reclaim";

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SchedulerPlatform {
    SystemdUser,
    Launchd,
    TaskScheduler,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SchedulerMode {
    Observe,
    Cleanup,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Schedule {
    pub hour: u8,
    pub minute: u8,
}

impl Schedule {
    pub fn parse(value: &str) -> Result<Self, SchedulerError> {
        let Some((hour, minute)) = value.split_once(':') else {
            return Err(SchedulerError::InvalidSchedule(value.to_string()));
        };
        if hour.len() != 2 || minute.len() != 2 {
            return Err(SchedulerError::InvalidSchedule(value.to_string()));
        }
        let hour = hour
            .parse::<u8>()
            .map_err(|_| SchedulerError::InvalidSchedule(value.to_string()))?;
        let minute = minute
            .parse::<u8>()
            .map_err(|_| SchedulerError::InvalidSchedule(value.to_string()))?;
        if hour > 23 || minute > 59 {
            return Err(SchedulerError::InvalidSchedule(value.to_string()));
        }
        Ok(Self { hour, minute })
    }

    pub fn as_hh_mm(self) -> String {
        format!("{:02}:{:02}", self.hour, self.minute)
    }
}

impl Default for Schedule {
    fn default() -> Self {
        Self { hour: 3, minute: 0 }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SchedulerRequest {
    pub platform: SchedulerPlatform,
    pub instance_name: String,
    pub config_path: PathBuf,
    pub cargo_reclaim_bin: PathBuf,
    pub schedule: Schedule,
    pub mode: SchedulerMode,
    pub policy: Option<PolicyKind>,
    pub allow_unattended_cleanup: bool,
    pub allow_unattended_high_policy: bool,
    pub state_dir: Option<PathBuf>,
    pub log_dir: Option<PathBuf>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GeneratedArtifactKind {
    SystemdService,
    SystemdTimer,
    LaunchdPlist,
    TaskSchedulerXml,
    RunnerScript,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GeneratedArtifact {
    pub kind: GeneratedArtifactKind,
    pub intended_install_path: PathBuf,
    pub contents: String,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SchedulerReport {
    pub command: &'static str,
    pub dry_run: bool,
    pub platform: SchedulerPlatform,
    pub mode: SchedulerMode,
    pub schedule: Schedule,
    pub effective_policy: PolicyKind,
    pub artifacts: Vec<GeneratedArtifact>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SchedulerOperation {
    Install,
    Uninstall,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SchedulerOperationPlan {
    pub command: &'static str,
    pub operation: SchedulerOperation,
    pub dry_run: bool,
    pub platform: SchedulerPlatform,
    pub artifacts: Vec<GeneratedArtifact>,
    pub steps: Vec<SchedulerPlanStep>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SchedulerPlanStep {
    EnsureDir {
        path: PathBuf,
    },
    WriteFile {
        path: PathBuf,
        artifact_kind: GeneratedArtifactKind,
    },
    SetExecutable {
        path: PathBuf,
    },
    RemoveFile {
        path: PathBuf,
    },
    RunCommand {
        argv: Vec<String>,
    },
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SchedulerExecutionReport {
    pub command: &'static str,
    pub operation: SchedulerOperation,
    pub dry_run: bool,
    pub platform: SchedulerPlatform,
    pub artifacts: Vec<GeneratedArtifact>,
    pub steps: Vec<SchedulerExecutionStep>,
    pub totals: SchedulerExecutionTotals,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SchedulerExecutionStep {
    pub step: SchedulerPlanStep,
    pub status: SchedulerExecutionStatus,
    pub message: Option<String>,
    pub command_output: Option<SchedulerCommandOutput>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SchedulerExecutionStatus {
    Applied,
    Skipped,
    Failed,
    Blocked,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct SchedulerExecutionTotals {
    pub applied: usize,
    pub skipped: usize,
    pub failed: usize,
    pub blocked: usize,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SchedulerCommandOutput {
    pub exit_code: Option<i32>,
    pub stdout: String,
    pub stderr: String,
}

impl SchedulerExecutionReport {
    pub fn succeeded(&self) -> bool {
        self.totals.failed == 0 && self.totals.blocked == 0
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SchedulerError {
    InvalidSchedule(String),
    InvalidInstanceName(String),
    CleanupNotAllowed,
    HighPolicyNotAllowed(PolicyKind),
}

impl fmt::Display for SchedulerError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::InvalidSchedule(value) => write!(
                formatter,
                "invalid scheduler time `{value}`; expected HH:MM in 24-hour time"
            ),
            Self::InvalidInstanceName(value) => write!(
                formatter,
                "invalid scheduler name `{value}`; use only ASCII letters, digits, '-', '_', or '.', and do not use path separators"
            ),
            Self::CleanupNotAllowed => formatter.write_str(
                "scheduler cleanup preview requires --allow-unattended-cleanup or [scheduler].allow_unattended_cleanup = true",
            ),
            Self::HighPolicyNotAllowed(policy) => write!(
                formatter,
                "scheduler cleanup with {} policy requires an explicit policy and --allow-unattended-high-policy or [scheduler].allow_unattended_high_policy = true",
                policy_label(*policy)
            ),
        }
    }
}

impl std::error::Error for SchedulerError {}

pub fn scheduler_instance_name_from_config(
    explicit_name: Option<&str>,
    _config_path: &Path,
) -> Result<String, SchedulerError> {
    if let Some(name) = explicit_name {
        return validate_scheduler_instance_name(name).map(ToOwned::to_owned);
    }

    Ok(DEFAULT_SCHEDULER_INSTANCE_NAME.to_string())
}

pub(crate) fn validate_scheduler_instance_name(name: &str) -> Result<&str, SchedulerError> {
    if name.is_empty()
        || name == "."
        || name == ".."
        || name.contains('/')
        || name.contains('\\')
        || !name.chars().all(is_safe_instance_character)
    {
        return Err(SchedulerError::InvalidInstanceName(name.to_string()));
    }
    Ok(name)
}

fn is_safe_instance_character(character: char) -> bool {
    character.is_ascii_alphanumeric() || matches!(character, '-' | '_' | '.')
}

pub fn policy_label(policy: PolicyKind) -> &'static str {
    match policy {
        PolicyKind::Observe => "observe",
        PolicyKind::Conservative => "conservative",
        PolicyKind::Balanced => "balanced",
        PolicyKind::Aggressive => "aggressive",
        PolicyKind::Custom => "custom",
    }
}

pub fn platform_label(platform: SchedulerPlatform) -> &'static str {
    match platform {
        SchedulerPlatform::SystemdUser => "systemd-user",
        SchedulerPlatform::Launchd => "launchd",
        SchedulerPlatform::TaskScheduler => "task-scheduler",
    }
}

pub fn mode_label(mode: SchedulerMode) -> &'static str {
    match mode {
        SchedulerMode::Observe => "observe",
        SchedulerMode::Cleanup => "cleanup",
    }
}

pub fn operation_label(operation: SchedulerOperation) -> &'static str {
    match operation {
        SchedulerOperation::Install => "install",
        SchedulerOperation::Uninstall => "uninstall",
    }
}

pub fn execution_status_label(status: SchedulerExecutionStatus) -> &'static str {
    match status {
        SchedulerExecutionStatus::Applied => "applied",
        SchedulerExecutionStatus::Skipped => "skipped",
        SchedulerExecutionStatus::Failed => "failed",
        SchedulerExecutionStatus::Blocked => "blocked",
    }
}

pub fn artifact_kind_label(kind: GeneratedArtifactKind) -> &'static str {
    match kind {
        GeneratedArtifactKind::SystemdService => "systemd-service",
        GeneratedArtifactKind::SystemdTimer => "systemd-timer",
        GeneratedArtifactKind::LaunchdPlist => "launchd-plist",
        GeneratedArtifactKind::TaskSchedulerXml => "task-scheduler-xml",
        GeneratedArtifactKind::RunnerScript => "runner-script",
    }
}