use crate::updater::{self, UpdateOutcome};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct UpdateFlags {
pub check_update: bool,
pub self_update: bool,
pub no_self_update: bool,
}
impl UpdateFlags {
#[must_use]
pub const fn is_terminal(self) -> bool {
self.check_update || self.self_update
}
#[must_use]
pub fn is_opted_out(self) -> bool {
self.is_opted_out_with(updater::env_opt_out())
}
#[must_use]
pub const fn is_opted_out_with(self, env_says_opt_out: bool) -> bool {
self.no_self_update || env_says_opt_out
}
}
#[must_use]
pub fn extract_update_flags<I, S>(args: I) -> (UpdateFlags, Vec<String>)
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let mut flags = UpdateFlags::default();
let mut rest = Vec::new();
for arg in args {
let arg = arg.as_ref();
match arg {
"--check-update" => flags.check_update = true,
"--self-update" => flags.self_update = true,
"--no-self-update" => flags.no_self_update = true,
_ => rest.push(arg.to_owned()),
}
}
(flags, rest)
}
#[must_use]
pub fn dispatch(
flags: UpdateFlags,
bin_name: &str,
current_version: &str,
) -> Option<(String, i32)> {
if !flags.is_terminal() {
return None;
}
let triple = updater::target_triple();
if flags.self_update {
return Some(run_self_update(flags, bin_name, current_version, triple));
}
let outcome = updater::check(current_version);
Some((outcome.message(bin_name, triple), outcome.exit_code()))
}
fn run_self_update(
flags: UpdateFlags,
bin_name: &str,
current_version: &str,
triple: &str,
) -> (String, i32) {
if flags.is_opted_out() {
let outcome = UpdateOutcome::DisabledByPolicy;
return (outcome.message(bin_name, triple), outcome.exit_code());
}
match updater::perform(bin_name, current_version) {
Ok(outcome) => (outcome.message(bin_name, triple), outcome.exit_code()),
Err(e) => (format!("self-update failed: {e}"), 1),
}
}
#[must_use]
pub const fn help_fragment() -> &'static str {
"UPDATE OPTIONS:\n \
--check-update Report whether a newer release exists, then exit\n \
--self-update Download, verify and install the latest release, then exit\n \
--no-self-update Refuse --self-update (env: CSAF_NO_SELF_UPDATE)\n\n\
EXIT CODES:\n \
0 Up to date, update installed, update host unreachable, --help, --version\n \
1 --self-update failed (no asset for this triple, checksum mismatch, install error)\n \
2 Argument parse error\n \
3 --self-update refused by policy\n \
10 --check-update found a newer release\n\n\
TRUST MODEL:\n \
Downloads are verified against the SHA256SUMS manifest published in the same\n \
release. That is integrity, not authenticity: anyone able to rewrite the release\n \
archive can rewrite the manifest too. The artifacts are not signed."
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extracts_each_flag_and_preserves_everything_else() {
let (flags, rest) = extract_update_flags([
"--config",
"a.toml",
"--check-update",
"--no-self-update",
"positional",
]);
assert!(flags.check_update);
assert!(flags.no_self_update);
assert!(!flags.self_update);
assert_eq!(rest, ["--config", "a.toml", "positional"]);
}
#[test]
fn every_flag_is_recognised_on_its_own() {
let cases = [
(
"--check-update",
UpdateFlags {
check_update: true,
..UpdateFlags::default()
},
),
(
"--self-update",
UpdateFlags {
self_update: true,
..UpdateFlags::default()
},
),
(
"--no-self-update",
UpdateFlags {
no_self_update: true,
..UpdateFlags::default()
},
),
];
for (arg, expected) in cases {
let (flags, rest) = extract_update_flags([arg]);
assert_eq!(flags, expected, "{arg} was not parsed correctly");
assert!(rest.is_empty(), "{arg} leaked into the leftover arguments");
}
}
#[test]
fn all_three_flags_can_be_combined() {
let (flags, rest) =
extract_update_flags(["--check-update", "--self-update", "--no-self-update"]);
assert_eq!(
flags,
UpdateFlags {
check_update: true,
self_update: true,
no_self_update: true,
},
);
assert!(rest.is_empty());
}
#[test]
fn self_update_alone_is_terminal() {
let (flags, _) = extract_update_flags(["--self-update"]);
assert!(flags.self_update, "--self-update did not set its flag");
assert!(
flags.is_terminal(),
"--self-update must be a terminal action"
);
}
#[test]
fn no_update_flags_means_run_the_program() {
let (flags, _) = extract_update_flags(["--config", "a.toml"]);
assert!(!flags.is_terminal());
assert!(dispatch(flags, "csaf-crud", "1.3.7").is_none());
}
#[test]
fn policy_opt_out_refuses_self_update_with_exit_3() {
let flags = UpdateFlags {
self_update: true,
no_self_update: true,
..UpdateFlags::default()
};
let (msg, code) = dispatch(flags, "csaf-crud", "1.3.7").expect("terminal");
assert_eq!(code, 3);
assert!(msg.contains("disabled by policy"), "{msg}");
}
#[test]
fn opt_out_truth_table_is_exhaustive() {
for (flag, env, expected) in [
(false, false, false), (true, false, true), (false, true, true), (true, true, true), ] {
let flags = UpdateFlags {
no_self_update: flag,
..UpdateFlags::default()
};
assert_eq!(
flags.is_opted_out_with(env),
expected,
"flag={flag} env={env} should give {expected}",
);
}
}
#[test]
fn env_wrapper_agrees_with_the_pure_decision() {
let allowed = UpdateFlags::default();
assert!(
!allowed.is_opted_out(),
"self-update reported as disabled with no flag set — is \
{} set in this environment?",
updater::NO_SELF_UPDATE_ENV,
);
let refused = UpdateFlags {
no_self_update: true,
..UpdateFlags::default()
};
assert!(
refused.is_opted_out(),
"--no-self-update must always refuse"
);
}
#[test]
fn policy_opt_out_does_not_disable_checking() {
let flags = UpdateFlags {
check_update: true,
no_self_update: true,
..UpdateFlags::default()
};
assert!(flags.is_terminal());
assert!(flags.is_opted_out());
}
#[test]
fn help_fragment_documents_every_flag_and_exit_code() {
let help = help_fragment();
for needle in [
"--check-update",
"--self-update",
"--no-self-update",
"CSAF_NO_SELF_UPDATE",
"10",
"integrity, not authenticity",
] {
assert!(help.contains(needle), "help text is missing {needle:?}");
}
}
}