mod github;
mod install;
mod verify;
pub use github::{GitHubSource, REPO};
use crate::ComposeError;
#[derive(Debug, Clone, Copy, Default)]
pub struct UpdateOptions {
pub check_only: bool,
pub force: bool,
}
pub trait ReleaseSource {
fn latest_version(&self) -> crate::Result<String>;
fn fetch(&self, asset: &str) -> crate::Result<Vec<u8>>;
}
pub fn run(opts: UpdateOptions) -> crate::Result<()> {
let source = GitHubSource::default();
run_with(&source, env!("CARGO_PKG_VERSION"), opts)
}
pub fn run_with(
source: &dyn ReleaseSource,
current: &str,
opts: UpdateOptions,
) -> crate::Result<()> {
let current_v = verify::parse_version(current)?;
let latest_tag = source.latest_version()?;
let latest_v = verify::parse_version(&latest_tag)?;
if latest_v <= current_v && !opts.force {
println!("podup is up to date (v{current})");
return Ok(());
}
if latest_v > current_v {
println!("update available: v{current} -> {latest_tag}");
} else {
println!("reinstalling {latest_tag} (--force)");
}
if opts.check_only {
println!("run `podup update` to install it");
return Ok(());
}
if let Some(pm) = install::managing_package_manager() {
return Err(install::package_managed_error(pm));
}
let asset = install::require_platform_asset()?;
println!("downloading {asset} ({latest_tag}) ...");
let binary = source.fetch(asset)?;
let sha256sums = source.fetch("SHA256SUMS")?;
let signature = source.fetch("SHA256SUMS.sig")?;
verify::verify_signature(&sha256sums, &signature)?;
let expected = verify::expected_digest(&sha256sums, asset)?;
verify::verify_digest(&binary, &expected)?;
println!("signature and checksum verified");
let path = install::install_binary(&binary)?;
println!("updated to {latest_tag}: {}", path.display());
Ok(())
}
pub fn exit_code(_err: &ComposeError) -> i32 {
2
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
use std::collections::HashMap;
struct MockSource {
latest: String,
assets: HashMap<String, Vec<u8>>,
fetched: RefCell<Vec<String>>,
}
impl ReleaseSource for MockSource {
fn latest_version(&self) -> crate::Result<String> {
Ok(self.latest.clone())
}
fn fetch(&self, asset: &str) -> crate::Result<Vec<u8>> {
self.fetched.borrow_mut().push(asset.to_string());
self.assets
.get(asset)
.cloned()
.ok_or_else(|| ComposeError::Update(format!("missing asset {asset}")))
}
}
#[test]
fn up_to_date_skips_download() {
let src = MockSource {
latest: "v0.6.0".into(),
assets: HashMap::new(),
fetched: RefCell::new(Vec::new()),
};
run_with(&src, "0.6.0", UpdateOptions::default()).unwrap();
assert!(
src.fetched.borrow().is_empty(),
"must not fetch when current"
);
}
#[test]
fn newer_release_check_only_does_not_install() {
let src = MockSource {
latest: "v0.7.0".into(),
assets: HashMap::new(),
fetched: RefCell::new(Vec::new()),
};
let opts = UpdateOptions {
check_only: true,
force: false,
};
run_with(&src, "0.6.0", opts).unwrap();
assert!(
src.fetched.borrow().is_empty(),
"check-only must not download"
);
}
#[test]
fn bad_version_from_source_errors() {
let src = MockSource {
latest: "not-a-version".into(),
assets: HashMap::new(),
fetched: RefCell::new(Vec::new()),
};
assert!(run_with(&src, "0.6.0", UpdateOptions::default()).is_err());
}
#[test]
fn newer_release_with_real_install_path() {
let Some(asset) = install::platform_asset() else {
return;
};
use ed25519_dalek::{Signer, SigningKey};
let sk = SigningKey::from_bytes(&[42u8; 32]);
let binary = b"the new podup binary".to_vec();
let digest = verify::sha256_hex(&binary);
let sums = format!("{digest} {asset}\n");
let sig = sk.sign(sums.as_bytes()).to_bytes().to_vec();
let mut assets = HashMap::new();
assets.insert(asset.to_string(), binary.clone());
assets.insert("SHA256SUMS".to_string(), sums.into_bytes());
assets.insert("SHA256SUMS.sig".to_string(), sig);
let src = MockSource {
latest: "v9.9.9".into(),
assets,
fetched: RefCell::new(Vec::new()),
};
let err = run_with(&src, "0.6.0", UpdateOptions::default()).unwrap_err();
assert!(matches!(err, ComposeError::Update(_)));
assert!(src.fetched.borrow().contains(&"SHA256SUMS.sig".to_string()));
}
#[test]
fn exit_code_is_two() {
assert_eq!(exit_code(&ComposeError::Update("x".into())), 2);
}
}