use std::marker::PhantomData;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use rtb_app::app::App;
use rtb_vcs::ReleaseProvider;
use crate::asset;
use crate::error::{Result, UpdateError};
use crate::flow;
use crate::options::{CheckOutcome, ProgressEvent, RunOptions, RunOutcome};
use crate::verify;
pub struct NoApp;
pub struct HasApp;
pub struct NoProvider;
pub struct HasProvider;
pub struct Updater {
app: App,
provider: Arc<dyn ReleaseProvider>,
swap_fn: flow::SwapFn,
self_test_fn: flow::SelfTestFn,
cache_dir: Option<PathBuf>,
include_framework_builtin: bool,
}
impl Updater {
#[must_use]
pub fn builder() -> UpdaterBuilder<NoApp, NoProvider> {
UpdaterBuilder {
app: None,
provider: None,
swap_fn: None,
self_test_fn: None,
cache_dir: None,
_markers: PhantomData,
}
}
#[must_use]
pub fn current_version(&self) -> &semver::Version {
&self.app.version.version
}
pub async fn check(&self) -> Result<CheckOutcome> {
let release = self.provider.latest_release().await?;
let Some(latest) = flow::parse_release_tag(&release.tag) else {
return Err(UpdateError::Pattern(format!(
"release tag `{}` is not a semver",
release.tag
)));
};
let current = self.current_version().clone();
Ok(match latest.cmp(¤t) {
std::cmp::Ordering::Equal => CheckOutcome::UpToDate { current },
std::cmp::Ordering::Greater => CheckOutcome::Newer { current, latest, release },
std::cmp::Ordering::Less => CheckOutcome::Older { current, latest },
})
}
pub async fn run(&self, options: RunOptions) -> Result<RunOutcome> {
self.preflight_required_fields()?;
emit(&options, ProgressEvent::Checking);
let release = match &options.target {
Some(version) => {
self.check_target_is_not_downgrade(version, options.force)?;
let tag = format!("v{version}");
self.provider.release_by_tag(&tag).await?
}
None => self.provider.latest_release().await?,
};
let latest = flow::parse_release_tag(&release.tag).ok_or_else(|| {
UpdateError::Pattern(format!("release tag `{}` is not a semver", release.tag))
})?;
let current = self.current_version().clone();
if latest == current && !options.force {
return Ok(RunOutcome {
from_version: current.clone(),
to_version: current,
bytes: 0,
swapped: false,
staged_at: None,
});
}
let expected_name = self.expected_asset_name(&release.tag);
let asset = asset::pick_asset(&release, &expected_name)?;
let signature = asset::pick_signature(&release, asset)
.ok_or_else(|| UpdateError::MissingSignature { asset: asset.name.clone() })?;
let cache_dir = self
.cache_dir
.clone()
.unwrap_or_else(|| flow::cache_dir_for(&self.app.metadata.name, &release.tag));
std::fs::create_dir_all(&cache_dir)?;
let staged_archive = cache_dir.join(&asset.name);
let bytes = flow::download_to_file(
&*self.provider,
asset,
&staged_archive,
options.progress.as_ref(),
)
.await?;
emit(&options, ProgressEvent::Verifying);
let sig_bytes = flow::fetch_small_asset(&*self.provider, signature).await?;
let archive_bytes = std::fs::read(&staged_archive)?;
verify::ed25519(
&asset.name,
&signature.name,
&archive_bytes,
&sig_bytes,
&self.app.metadata.update_public_keys,
)?;
if let Some(checksums_name) = self.app.metadata.update_checksums_asset {
let checksums_asset =
release.assets.iter().find(|a| a.name == checksums_name).ok_or_else(|| {
UpdateError::BadChecksum { asset: checksums_name.to_string() }
})?;
let checksums_bytes = flow::fetch_small_asset(&*self.provider, checksums_asset).await?;
let checksums_text = String::from_utf8_lossy(&checksums_bytes);
verify::checksums(&asset.name, &archive_bytes, &checksums_text)?;
}
let bin_dir = cache_dir.join("bin");
let staged_binary =
flow::extract_binary(&staged_archive, &bin_dir, &self.app.metadata.name)?;
emit(&options, ProgressEvent::SelfTesting);
self.self_test_staged(&staged_binary, &release.tag)?;
flow::mark_executable(&staged_binary)?;
if options.dry_run {
let outcome = flow::dry_run_outcome(current, latest.clone(), bytes, staged_binary);
emit(&options, ProgressEvent::Done { version: latest });
return Ok(outcome);
}
emit(&options, ProgressEvent::Swapping);
(self.swap_fn)(&staged_binary).map_err(|e| UpdateError::SwapFailed(e.to_string()))?;
emit(&options, ProgressEvent::Done { version: latest.clone() });
Ok(flow::swap_outcome(current, latest, bytes))
}
pub async fn run_from_file(
&self,
asset_path: &Path,
signature_path: &Path,
options: RunOptions,
) -> Result<RunOutcome> {
self.preflight_required_fields()?;
emit(&options, ProgressEvent::Verifying);
let asset_bytes = tokio::fs::read(asset_path).await?;
let sig_bytes = tokio::fs::read(signature_path).await?;
let asset_name =
asset_path.file_name().and_then(|n| n.to_str()).unwrap_or("asset").to_string();
let sig_name =
signature_path.file_name().and_then(|n| n.to_str()).unwrap_or("asset.sig").to_string();
verify::ed25519(
&asset_name,
&sig_name,
&asset_bytes,
&sig_bytes,
&self.app.metadata.update_public_keys,
)?;
let current = self.current_version().clone();
let cache_dir = self
.cache_dir
.clone()
.unwrap_or_else(|| flow::cache_dir_for(&self.app.metadata.name, "offline"));
let bin_dir = cache_dir.join("bin");
let staged_binary = flow::extract_binary(asset_path, &bin_dir, &self.app.metadata.name)?;
emit(&options, ProgressEvent::SelfTesting);
let staged_version = self.self_test_version(&staged_binary)?;
flow::mark_executable(&staged_binary)?;
if options.dry_run {
return Ok(flow::dry_run_outcome(
current,
staged_version,
asset_bytes.len() as u64,
staged_binary,
));
}
emit(&options, ProgressEvent::Swapping);
(self.swap_fn)(&staged_binary).map_err(|e| UpdateError::SwapFailed(e.to_string()))?;
emit(&options, ProgressEvent::Done { version: staged_version.clone() });
Ok(flow::swap_outcome(current, staged_version, asset_bytes.len() as u64))
}
fn preflight_required_fields(&self) -> Result<()> {
if self.app.metadata.release_source.is_none() {
return Err(UpdateError::NoReleaseSource);
}
if self.app.metadata.update_public_keys.is_empty() {
return Err(UpdateError::NoPublicKey);
}
let _ = self.include_framework_builtin;
Ok(())
}
fn check_target_is_not_downgrade(&self, target: &semver::Version, force: bool) -> Result<()> {
if force {
return Ok(());
}
let current = self.current_version();
if target < current {
return Err(UpdateError::DowngradeRefused {
target: target.clone(),
current: current.clone(),
});
}
Ok(())
}
fn expected_asset_name(&self, tag: &str) -> String {
let pattern = self.app.metadata.update_asset_pattern.unwrap_or(asset::DEFAULT_PATTERN);
asset::render_pattern(pattern, &self.app.metadata.name, tag)
}
fn self_test_staged(&self, binary: &Path, expected_tag: &str) -> Result<()> {
let Ok(stdout) = (self.self_test_fn)(binary) else {
return Err(UpdateError::SelfTestFailed);
};
let tag_stripped = expected_tag.trim_start_matches(['v', 'V']);
if stdout.contains(expected_tag) || stdout.contains(tag_stripped) {
Ok(())
} else {
Err(UpdateError::SelfTestFailed)
}
}
fn self_test_version(&self, binary: &Path) -> Result<semver::Version> {
let Ok(stdout) = (self.self_test_fn)(binary) else {
return Err(UpdateError::SelfTestFailed);
};
for token in stdout.split_whitespace() {
let candidate = token.trim_start_matches(['v', 'V']);
if let Ok(v) = semver::Version::parse(candidate) {
return Ok(v);
}
}
Err(UpdateError::SelfTestFailed)
}
}
fn emit(options: &RunOptions, event: ProgressEvent) {
if let Some(sink) = &options.progress {
sink(event);
}
}
pub struct UpdaterBuilder<AppMarker, ProviderMarker> {
app: Option<App>,
provider: Option<Arc<dyn ReleaseProvider>>,
swap_fn: Option<flow::SwapFn>,
self_test_fn: Option<flow::SelfTestFn>,
cache_dir: Option<PathBuf>,
_markers: PhantomData<(AppMarker, ProviderMarker)>,
}
impl<P> UpdaterBuilder<NoApp, P> {
#[must_use]
pub fn app(self, app: &App) -> UpdaterBuilder<HasApp, P> {
UpdaterBuilder {
app: Some(app.clone()),
provider: self.provider,
swap_fn: self.swap_fn,
self_test_fn: self.self_test_fn,
cache_dir: self.cache_dir,
_markers: PhantomData,
}
}
}
impl<A> UpdaterBuilder<A, NoProvider> {
#[must_use]
pub fn provider(self, provider: Arc<dyn ReleaseProvider>) -> UpdaterBuilder<A, HasProvider> {
UpdaterBuilder {
app: self.app,
provider: Some(provider),
swap_fn: self.swap_fn,
self_test_fn: self.self_test_fn,
cache_dir: self.cache_dir,
_markers: PhantomData,
}
}
}
impl<A, P> UpdaterBuilder<A, P> {
#[must_use]
pub fn swap_fn(mut self, swap_fn: flow::SwapFn) -> Self {
self.swap_fn = Some(swap_fn);
self
}
#[must_use]
pub fn self_test_fn(mut self, self_test_fn: flow::SelfTestFn) -> Self {
self.self_test_fn = Some(self_test_fn);
self
}
#[must_use]
pub fn cache_dir(mut self, cache_dir: impl Into<PathBuf>) -> Self {
self.cache_dir = Some(cache_dir.into());
self
}
}
impl UpdaterBuilder<HasApp, HasProvider> {
#[must_use]
pub fn build(self) -> Updater {
Updater {
app: self.app.expect("HasApp"),
provider: self.provider.expect("HasProvider"),
swap_fn: self.swap_fn.unwrap_or_else(flow::default_swap_fn),
self_test_fn: self.self_test_fn.unwrap_or_else(flow::default_self_test_fn),
cache_dir: self.cache_dir,
include_framework_builtin: true,
}
}
}