mod launchd;
mod period;
use anyhow::{Result, bail};
use serde::Serialize;
pub use period::{Every, parse_every};
pub const JOB_SCHEMA: i64 = 1;
pub const LABEL_PREFIX: &str = "com.maclean.job.";
pub(crate) const MANAGED_KEY: &str = "MacleanManaged";
pub(crate) const SCHEMA_KEY: &str = "MacleanSchema";
pub(crate) const ITEM_KEY: &str = "MacleanItemId";
pub(crate) const COMMENT: &str =
"Managed by maclean. Do not edit this file. Use the maclean TUI or: maclean schedule";
#[derive(Debug, Clone, Serialize)]
pub struct ScheduledJob {
pub item_id: String,
pub every: Every,
pub command: Vec<String>,
pub schema: i64,
}
pub trait Scheduler {
fn list(&self) -> Result<Vec<ScheduledJob>>;
fn add(&self, job: &ScheduledJob) -> Result<()>;
fn remove(&self, item_id: &str) -> Result<()>;
fn purge(&self) -> Result<Vec<ScheduledJob>>;
}
pub fn current() -> Box<dyn Scheduler> {
Box::new(launchd::LaunchdScheduler)
}
pub fn maclean_command(item_id: &str) -> Result<Vec<String>> {
if !valid_item_id(item_id) {
bail!("invalid item id '{item_id}'");
}
let exe = std::env::current_exe()?;
Ok(vec![
exe.display().to_string(),
"reclaim".into(),
item_id.into(),
"--yes".into(),
])
}
pub fn valid_item_id(id: &str) -> bool {
!id.is_empty()
&& !id.starts_with('-')
&& id
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, ':' | '.' | '-' | '_' | '/' | '+'))
}
pub fn is_maclean_job(
filename: &str,
label: Option<&str>,
comment: Option<&str>,
schema: Option<i64>,
managed: Option<bool>,
) -> bool {
if !filename.starts_with(LABEL_PREFIX) || !filename.ends_with(".plist") {
return false;
}
let Some(label) = label else {
return false;
};
if !label.starts_with(LABEL_PREFIX) {
return false;
}
if managed == Some(true) || schema.is_some_and(|s| s >= 1) {
return true;
}
comment.is_some_and(|c| c.starts_with("Managed by maclean"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn item_ids_reject_flags_and_junk() {
assert!(valid_item_id("spotify:cache"));
assert!(valid_item_id("cargo:/Users/a/proj"));
assert!(valid_item_id("docker:image:sha256:abc"));
assert!(!valid_item_id(""));
assert!(!valid_item_id("--yes"));
assert!(!valid_item_id("foo;rm"));
assert!(!valid_item_id("foo bar"));
}
#[test]
fn foreign_plists_are_not_ours_even_with_our_filename() {
assert!(!is_maclean_job(
"com.maclean.job.evil.plist",
Some("com.apple.something"),
Some("Managed by maclean. Do not edit this file."),
None,
None,
));
assert!(!is_maclean_job(
"com.maclean.job.evil.plist",
Some("com.maclean.job.evil"),
Some("a random agent"),
None,
None,
));
assert!(is_maclean_job(
"com.maclean.job.spotify-cache.plist",
Some("com.maclean.job.spotify-cache"),
Some("Managed by maclean. Do not edit this file."),
None,
None,
));
assert!(is_maclean_job(
"com.maclean.job.spotify-cache.plist",
Some("com.maclean.job.spotify-cache"),
None,
Some(1),
Some(true),
));
}
}