1use std::marker::PhantomData;
6use std::path::{Path, PathBuf};
7use std::sync::Arc;
8
9use rtb_app::app::App;
10use rtb_forge::ReleaseProvider;
11
12use crate::asset;
13use crate::error::{Result, UpdateError};
14use crate::flow;
15use crate::options::{CheckOutcome, ProgressEvent, RunOptions, RunOutcome};
16use crate::verify;
17
18pub struct NoApp;
20pub struct HasApp;
22pub struct NoProvider;
24pub struct HasProvider;
26
27pub struct Updater {
29 app: App,
30 provider: Arc<dyn ReleaseProvider>,
31 swap_fn: flow::SwapFn,
32 self_test_fn: flow::SelfTestFn,
33 cache_dir: Option<PathBuf>,
39 include_framework_builtin: bool,
40}
41
42impl Updater {
43 #[must_use]
46 pub fn builder() -> UpdaterBuilder<NoApp, NoProvider> {
47 UpdaterBuilder {
48 app: None,
49 provider: None,
50 swap_fn: None,
51 self_test_fn: None,
52 cache_dir: None,
53 _markers: PhantomData,
54 }
55 }
56
57 #[must_use]
59 pub fn current_version(&self) -> &semver::Version {
60 &self.app.version.version
61 }
62
63 pub async fn check(&self) -> Result<CheckOutcome> {
70 let release = self.provider.latest_release().await?;
71 let Some(latest) = flow::parse_release_tag(&release.tag) else {
72 return Err(UpdateError::Pattern(format!(
73 "release tag `{}` is not a semver",
74 release.tag
75 )));
76 };
77 let current = self.current_version().clone();
78 Ok(match latest.cmp(¤t) {
79 std::cmp::Ordering::Equal => CheckOutcome::UpToDate { current },
80 std::cmp::Ordering::Greater => CheckOutcome::Newer { current, latest, release },
81 std::cmp::Ordering::Less => CheckOutcome::Older { current, latest },
82 })
83 }
84
85 pub async fn run(&self, options: RunOptions) -> Result<RunOutcome> {
93 self.preflight_required_fields()?;
94
95 emit(&options, ProgressEvent::Checking);
96
97 let release = match &options.target {
98 Some(version) => {
99 self.check_target_is_not_downgrade(version, options.force)?;
100 let tag = format!("v{version}");
101 self.provider.release_by_tag(&tag).await?
102 }
103 None => self.provider.latest_release().await?,
104 };
105
106 let latest = flow::parse_release_tag(&release.tag).ok_or_else(|| {
107 UpdateError::Pattern(format!("release tag `{}` is not a semver", release.tag))
108 })?;
109 let current = self.current_version().clone();
110
111 if latest == current && !options.force {
112 return Ok(RunOutcome {
113 from_version: current.clone(),
114 to_version: current,
115 bytes: 0,
116 swapped: false,
117 staged_at: None,
118 });
119 }
120
121 let expected_name = self.expected_asset_name(&release.tag);
122 let asset = asset::pick_asset(&release, &expected_name)?;
123 let signature = asset::pick_signature(&release, asset)
124 .ok_or_else(|| UpdateError::MissingSignature { asset: asset.name.clone() })?;
125
126 let cache_dir = self
127 .cache_dir
128 .clone()
129 .unwrap_or_else(|| flow::cache_dir_for(&self.app.metadata.name, &release.tag));
130 std::fs::create_dir_all(&cache_dir)?;
131 let staged_archive = cache_dir.join(&asset.name);
132
133 let bytes = flow::download_to_file(
134 &*self.provider,
135 asset,
136 &staged_archive,
137 options.progress.as_ref(),
138 )
139 .await?;
140
141 emit(&options, ProgressEvent::Verifying);
142
143 let sig_bytes = flow::fetch_small_asset(&*self.provider, signature).await?;
144 let archive_bytes = std::fs::read(&staged_archive)?;
145 verify::minisign(
146 &asset.name,
147 &archive_bytes,
148 &sig_bytes,
149 &self.app.metadata.update_public_keys,
150 )?;
151
152 if let Some(checksums_name) = self.app.metadata.update_checksums_asset {
153 let checksums_asset =
154 release.assets.iter().find(|a| a.name == checksums_name).ok_or_else(|| {
155 UpdateError::BadChecksum { asset: checksums_name.to_string() }
156 })?;
157 let checksums_bytes = flow::fetch_small_asset(&*self.provider, checksums_asset).await?;
158 let checksums_text = String::from_utf8_lossy(&checksums_bytes);
159 verify::checksums(&asset.name, &archive_bytes, &checksums_text)?;
160 }
161
162 let bin_dir = cache_dir.join("bin");
163 let staged_binary =
164 flow::extract_binary(&staged_archive, &bin_dir, &self.app.metadata.name)?;
165
166 emit(&options, ProgressEvent::SelfTesting);
167
168 self.self_test_staged(&staged_binary, &release.tag)?;
169 flow::mark_executable(&staged_binary)?;
170
171 if options.dry_run {
172 let outcome = flow::dry_run_outcome(current, latest.clone(), bytes, staged_binary);
173 emit(&options, ProgressEvent::Done { version: latest });
174 return Ok(outcome);
175 }
176
177 emit(&options, ProgressEvent::Swapping);
178
179 (self.swap_fn)(&staged_binary).map_err(|e| UpdateError::SwapFailed(e.to_string()))?;
180
181 emit(&options, ProgressEvent::Done { version: latest.clone() });
182
183 Ok(flow::swap_outcome(current, latest, bytes))
184 }
185
186 pub async fn run_from_file(
194 &self,
195 asset_path: &Path,
196 signature_path: &Path,
197 options: RunOptions,
198 ) -> Result<RunOutcome> {
199 self.preflight_required_fields()?;
200
201 emit(&options, ProgressEvent::Verifying);
202
203 let asset_bytes = tokio::fs::read(asset_path).await?;
204 let sig_bytes = tokio::fs::read(signature_path).await?;
205 let asset_name =
206 asset_path.file_name().and_then(|n| n.to_str()).unwrap_or("asset").to_string();
207
208 verify::minisign(
211 &asset_name,
212 &asset_bytes,
213 &sig_bytes,
214 &self.app.metadata.update_public_keys,
215 )?;
216
217 let current = self.current_version().clone();
218 let cache_dir = self
219 .cache_dir
220 .clone()
221 .unwrap_or_else(|| flow::cache_dir_for(&self.app.metadata.name, "offline"));
222 let bin_dir = cache_dir.join("bin");
223 let staged_binary = flow::extract_binary(asset_path, &bin_dir, &self.app.metadata.name)?;
224
225 emit(&options, ProgressEvent::SelfTesting);
226
227 let staged_version = self.self_test_version(&staged_binary)?;
228 flow::mark_executable(&staged_binary)?;
229
230 if options.dry_run {
231 return Ok(flow::dry_run_outcome(
232 current,
233 staged_version,
234 asset_bytes.len() as u64,
235 staged_binary,
236 ));
237 }
238
239 emit(&options, ProgressEvent::Swapping);
240
241 (self.swap_fn)(&staged_binary).map_err(|e| UpdateError::SwapFailed(e.to_string()))?;
242
243 emit(&options, ProgressEvent::Done { version: staged_version.clone() });
244
245 Ok(flow::swap_outcome(current, staged_version, asset_bytes.len() as u64))
246 }
247
248 fn preflight_required_fields(&self) -> Result<()> {
251 if self.app.metadata.release_source.is_none() {
252 return Err(UpdateError::NoReleaseSource);
253 }
254 if self.app.metadata.update_public_keys.is_empty() {
255 return Err(UpdateError::NoPublicKey);
256 }
257 let _ = self.include_framework_builtin;
260 Ok(())
261 }
262
263 fn check_target_is_not_downgrade(&self, target: &semver::Version, force: bool) -> Result<()> {
264 if force {
265 return Ok(());
266 }
267 let current = self.current_version();
268 if target < current {
269 return Err(UpdateError::DowngradeRefused {
270 target: target.clone(),
271 current: current.clone(),
272 });
273 }
274 Ok(())
275 }
276
277 fn expected_asset_name(&self, tag: &str) -> String {
278 let pattern = self.app.metadata.update_asset_pattern.unwrap_or(asset::DEFAULT_PATTERN);
279 asset::render_pattern(pattern, &self.app.metadata.name, tag)
280 }
281
282 fn self_test_staged(&self, binary: &Path, expected_tag: &str) -> Result<()> {
286 let Ok(stdout) = (self.self_test_fn)(binary) else {
287 return Err(UpdateError::SelfTestFailed);
288 };
289 let tag_stripped = expected_tag.trim_start_matches(['v', 'V']);
290 if stdout.contains(expected_tag) || stdout.contains(tag_stripped) {
291 Ok(())
292 } else {
293 Err(UpdateError::SelfTestFailed)
294 }
295 }
296
297 fn self_test_version(&self, binary: &Path) -> Result<semver::Version> {
301 let Ok(stdout) = (self.self_test_fn)(binary) else {
302 return Err(UpdateError::SelfTestFailed);
303 };
304 for token in stdout.split_whitespace() {
306 let candidate = token.trim_start_matches(['v', 'V']);
307 if let Ok(v) = semver::Version::parse(candidate) {
308 return Ok(v);
309 }
310 }
311 Err(UpdateError::SelfTestFailed)
312 }
313}
314
315fn emit(options: &RunOptions, event: ProgressEvent) {
316 if let Some(sink) = &options.progress {
317 sink(event);
318 }
319}
320
321pub struct UpdaterBuilder<AppMarker, ProviderMarker> {
327 app: Option<App>,
328 provider: Option<Arc<dyn ReleaseProvider>>,
329 swap_fn: Option<flow::SwapFn>,
330 self_test_fn: Option<flow::SelfTestFn>,
331 cache_dir: Option<PathBuf>,
332 _markers: PhantomData<(AppMarker, ProviderMarker)>,
333}
334
335impl<P> UpdaterBuilder<NoApp, P> {
336 #[must_use]
339 pub fn app(self, app: &App) -> UpdaterBuilder<HasApp, P> {
340 UpdaterBuilder {
341 app: Some(app.clone()),
342 provider: self.provider,
343 swap_fn: self.swap_fn,
344 self_test_fn: self.self_test_fn,
345 cache_dir: self.cache_dir,
346 _markers: PhantomData,
347 }
348 }
349}
350
351impl<A> UpdaterBuilder<A, NoProvider> {
352 #[must_use]
356 pub fn provider(self, provider: Arc<dyn ReleaseProvider>) -> UpdaterBuilder<A, HasProvider> {
357 UpdaterBuilder {
358 app: self.app,
359 provider: Some(provider),
360 swap_fn: self.swap_fn,
361 self_test_fn: self.self_test_fn,
362 cache_dir: self.cache_dir,
363 _markers: PhantomData,
364 }
365 }
366}
367
368impl<A, P> UpdaterBuilder<A, P> {
369 #[must_use]
372 pub fn swap_fn(mut self, swap_fn: flow::SwapFn) -> Self {
373 self.swap_fn = Some(swap_fn);
374 self
375 }
376
377 #[must_use]
380 pub fn self_test_fn(mut self, self_test_fn: flow::SelfTestFn) -> Self {
381 self.self_test_fn = Some(self_test_fn);
382 self
383 }
384
385 #[must_use]
394 pub fn cache_dir(mut self, cache_dir: impl Into<PathBuf>) -> Self {
395 self.cache_dir = Some(cache_dir.into());
396 self
397 }
398}
399
400impl UpdaterBuilder<HasApp, HasProvider> {
401 #[must_use]
404 pub fn build(self) -> Updater {
405 Updater {
406 app: self.app.expect("HasApp"),
407 provider: self.provider.expect("HasProvider"),
408 swap_fn: self.swap_fn.unwrap_or_else(flow::default_swap_fn),
409 self_test_fn: self.self_test_fn.unwrap_or_else(flow::default_self_test_fn),
410 cache_dir: self.cache_dir,
411 include_framework_builtin: true,
412 }
413 }
414}