//! Construction of `once` argument vectors.
//!
//! This module is the entire write surface of `twice`. Every command the
//! server can ever run is produced by [`argv`], and [`argv`] is a total match
//! over [`JobKind`] — which has no destructive variant. There is no code path
//! that reaches `once remove`, `once keys`, `once exec`, `once restore`, or
//! `once teardown`, and no string from a client is ever concatenated into a
//! shell.
use std::ffi::OsString;
use twice_core::JobKind;
/// Subcommands this binary is permitted to invoke. Kept next to [`argv`] so a
/// reviewer can check the two against each other at a glance.
#[allow(
dead_code,
reason = "reviewer-facing allowlist, asserted by the tests below"
)]
pub(crate) const ALLOWED_SUBCOMMANDS: [&str; 4] = ["start", "stop", "update", "deploy"];
/// Subcommands that must never be reachable. Asserted by tests against the
/// output of [`argv`] for every job kind.
#[allow(
dead_code,
reason = "reviewer-facing denylist, asserted by the tests below"
)]
pub(crate) const FORBIDDEN_SUBCOMMANDS: [&str; 6] =
["remove", "rm", "keys", "exec", "restore", "teardown"];
/// Builds the argument vector for a job, excluding the program name itself.
pub(crate) fn argv(kind: &JobKind) -> Vec<OsString> {
match kind {
JobKind::Start { host } => vec!["start".into(), host.as_str().into()],
JobKind::Stop { host } => vec!["stop".into(), host.as_str().into()],
JobKind::Deploy { host, image } => vec![
"deploy".into(),
image.as_str().into(),
"--host".into(),
host.as_str().into(),
],
JobKind::UpdateImage {
host,
image,
auto_update,
} => {
let mut args: Vec<OsString> = vec![
"update".into(),
host.as_str().into(),
"--image".into(),
image.as_str().into(),
];
if let Some(enabled) = *auto_update {
args.push(format!("--auto-update={enabled}").into());
}
args
}
}
}
#[cfg(test)]
mod tests {
use twice_core::{Host, ImageRef};
use super::*;
fn host() -> Host {
Host::parse("writebook.example.com").unwrap()
}
fn image() -> ImageRef {
ImageRef::parse("ghcr.io/basecamp/writebook:1.4.0").unwrap()
}
fn every_kind() -> Vec<JobKind> {
vec![
JobKind::Start { host: host() },
JobKind::Stop { host: host() },
JobKind::Deploy {
host: host(),
image: image(),
},
JobKind::UpdateImage {
host: host(),
image: image(),
auto_update: None,
},
JobKind::UpdateImage {
host: host(),
image: image(),
auto_update: Some(true),
},
JobKind::UpdateImage {
host: host(),
image: image(),
auto_update: Some(false),
},
]
}
#[test]
fn every_job_starts_with_an_allowlisted_subcommand() {
for kind in every_kind() {
let args = argv(&kind);
let first = args.first().and_then(|a| a.to_str()).map(str::to_owned);
assert!(
first
.as_deref()
.is_some_and(|s| ALLOWED_SUBCOMMANDS.contains(&s)),
"job {kind:?} produced non-allowlisted subcommand {first:?}"
);
}
}
#[test]
fn no_job_can_reach_a_destructive_subcommand() {
for kind in every_kind() {
for arg in argv(&kind) {
let arg = arg.to_str().unwrap().to_owned();
assert!(
!FORBIDDEN_SUBCOMMANDS.contains(&arg.as_str()),
"job {kind:?} produced forbidden argument {arg:?}"
);
assert!(
!arg.contains("--remove-data"),
"job {kind:?} produced a data-destroying flag"
);
}
}
}
#[test]
fn update_passes_image_as_a_separate_argv_entry() {
let args = argv(&JobKind::UpdateImage {
host: host(),
image: image(),
auto_update: None,
});
assert_eq!(
args,
vec![
OsString::from("update"),
OsString::from("writebook.example.com"),
OsString::from("--image"),
OsString::from("ghcr.io/basecamp/writebook:1.4.0"),
]
);
}
#[test]
fn deploy_passes_the_pinned_image_and_host_as_separate_arguments() {
let args = argv(&JobKind::Deploy {
host: host(),
image: image(),
});
assert_eq!(
args,
vec![
OsString::from("deploy"),
OsString::from("ghcr.io/basecamp/writebook:1.4.0"),
OsString::from("--host"),
OsString::from("writebook.example.com"),
]
);
}
#[test]
fn auto_update_is_omitted_unless_the_client_asked() {
let without = argv(&JobKind::UpdateImage {
host: host(),
image: image(),
auto_update: None,
});
assert!(
!without
.iter()
.any(|a| a.to_string_lossy().starts_with("--auto-update"))
);
let with = argv(&JobKind::UpdateImage {
host: host(),
image: image(),
auto_update: Some(false),
});
assert!(with.contains(&OsString::from("--auto-update=false")));
}
}