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::ed25519(
146 &asset.name,
147 &signature.name,
148 &archive_bytes,
149 &sig_bytes,
150 &self.app.metadata.update_public_keys,
151 )?;
152
153 if let Some(checksums_name) = self.app.metadata.update_checksums_asset {
154 let checksums_asset =
155 release.assets.iter().find(|a| a.name == checksums_name).ok_or_else(|| {
156 UpdateError::BadChecksum { asset: checksums_name.to_string() }
157 })?;
158 let checksums_bytes = flow::fetch_small_asset(&*self.provider, checksums_asset).await?;
159 let checksums_text = String::from_utf8_lossy(&checksums_bytes);
160 verify::checksums(&asset.name, &archive_bytes, &checksums_text)?;
161 }
162
163 let bin_dir = cache_dir.join("bin");
164 let staged_binary =
165 flow::extract_binary(&staged_archive, &bin_dir, &self.app.metadata.name)?;
166
167 emit(&options, ProgressEvent::SelfTesting);
168
169 self.self_test_staged(&staged_binary, &release.tag)?;
170 flow::mark_executable(&staged_binary)?;
171
172 if options.dry_run {
173 let outcome = flow::dry_run_outcome(current, latest.clone(), bytes, staged_binary);
174 emit(&options, ProgressEvent::Done { version: latest });
175 return Ok(outcome);
176 }
177
178 emit(&options, ProgressEvent::Swapping);
179
180 (self.swap_fn)(&staged_binary).map_err(|e| UpdateError::SwapFailed(e.to_string()))?;
181
182 emit(&options, ProgressEvent::Done { version: latest.clone() });
183
184 Ok(flow::swap_outcome(current, latest, bytes))
185 }
186
187 pub async fn run_from_file(
195 &self,
196 asset_path: &Path,
197 signature_path: &Path,
198 options: RunOptions,
199 ) -> Result<RunOutcome> {
200 self.preflight_required_fields()?;
201
202 emit(&options, ProgressEvent::Verifying);
203
204 let asset_bytes = tokio::fs::read(asset_path).await?;
205 let sig_bytes = tokio::fs::read(signature_path).await?;
206 let asset_name =
207 asset_path.file_name().and_then(|n| n.to_str()).unwrap_or("asset").to_string();
208 let sig_name =
209 signature_path.file_name().and_then(|n| n.to_str()).unwrap_or("asset.sig").to_string();
210
211 verify::ed25519(
212 &asset_name,
213 &sig_name,
214 &asset_bytes,
215 &sig_bytes,
216 &self.app.metadata.update_public_keys,
217 )?;
218
219 let current = self.current_version().clone();
220 let cache_dir = self
221 .cache_dir
222 .clone()
223 .unwrap_or_else(|| flow::cache_dir_for(&self.app.metadata.name, "offline"));
224 let bin_dir = cache_dir.join("bin");
225 let staged_binary = flow::extract_binary(asset_path, &bin_dir, &self.app.metadata.name)?;
226
227 emit(&options, ProgressEvent::SelfTesting);
228
229 let staged_version = self.self_test_version(&staged_binary)?;
230 flow::mark_executable(&staged_binary)?;
231
232 if options.dry_run {
233 return Ok(flow::dry_run_outcome(
234 current,
235 staged_version,
236 asset_bytes.len() as u64,
237 staged_binary,
238 ));
239 }
240
241 emit(&options, ProgressEvent::Swapping);
242
243 (self.swap_fn)(&staged_binary).map_err(|e| UpdateError::SwapFailed(e.to_string()))?;
244
245 emit(&options, ProgressEvent::Done { version: staged_version.clone() });
246
247 Ok(flow::swap_outcome(current, staged_version, asset_bytes.len() as u64))
248 }
249
250 fn preflight_required_fields(&self) -> Result<()> {
253 if self.app.metadata.release_source.is_none() {
254 return Err(UpdateError::NoReleaseSource);
255 }
256 if self.app.metadata.update_public_keys.is_empty() {
257 return Err(UpdateError::NoPublicKey);
258 }
259 let _ = self.include_framework_builtin;
262 Ok(())
263 }
264
265 fn check_target_is_not_downgrade(&self, target: &semver::Version, force: bool) -> Result<()> {
266 if force {
267 return Ok(());
268 }
269 let current = self.current_version();
270 if target < current {
271 return Err(UpdateError::DowngradeRefused {
272 target: target.clone(),
273 current: current.clone(),
274 });
275 }
276 Ok(())
277 }
278
279 fn expected_asset_name(&self, tag: &str) -> String {
280 let pattern = self.app.metadata.update_asset_pattern.unwrap_or(asset::DEFAULT_PATTERN);
281 asset::render_pattern(pattern, &self.app.metadata.name, tag)
282 }
283
284 fn self_test_staged(&self, binary: &Path, expected_tag: &str) -> Result<()> {
288 let Ok(stdout) = (self.self_test_fn)(binary) else {
289 return Err(UpdateError::SelfTestFailed);
290 };
291 let tag_stripped = expected_tag.trim_start_matches(['v', 'V']);
292 if stdout.contains(expected_tag) || stdout.contains(tag_stripped) {
293 Ok(())
294 } else {
295 Err(UpdateError::SelfTestFailed)
296 }
297 }
298
299 fn self_test_version(&self, binary: &Path) -> Result<semver::Version> {
303 let Ok(stdout) = (self.self_test_fn)(binary) else {
304 return Err(UpdateError::SelfTestFailed);
305 };
306 for token in stdout.split_whitespace() {
308 let candidate = token.trim_start_matches(['v', 'V']);
309 if let Ok(v) = semver::Version::parse(candidate) {
310 return Ok(v);
311 }
312 }
313 Err(UpdateError::SelfTestFailed)
314 }
315}
316
317fn emit(options: &RunOptions, event: ProgressEvent) {
318 if let Some(sink) = &options.progress {
319 sink(event);
320 }
321}
322
323pub struct UpdaterBuilder<AppMarker, ProviderMarker> {
329 app: Option<App>,
330 provider: Option<Arc<dyn ReleaseProvider>>,
331 swap_fn: Option<flow::SwapFn>,
332 self_test_fn: Option<flow::SelfTestFn>,
333 cache_dir: Option<PathBuf>,
334 _markers: PhantomData<(AppMarker, ProviderMarker)>,
335}
336
337impl<P> UpdaterBuilder<NoApp, P> {
338 #[must_use]
341 pub fn app(self, app: &App) -> UpdaterBuilder<HasApp, P> {
342 UpdaterBuilder {
343 app: Some(app.clone()),
344 provider: self.provider,
345 swap_fn: self.swap_fn,
346 self_test_fn: self.self_test_fn,
347 cache_dir: self.cache_dir,
348 _markers: PhantomData,
349 }
350 }
351}
352
353impl<A> UpdaterBuilder<A, NoProvider> {
354 #[must_use]
358 pub fn provider(self, provider: Arc<dyn ReleaseProvider>) -> UpdaterBuilder<A, HasProvider> {
359 UpdaterBuilder {
360 app: self.app,
361 provider: Some(provider),
362 swap_fn: self.swap_fn,
363 self_test_fn: self.self_test_fn,
364 cache_dir: self.cache_dir,
365 _markers: PhantomData,
366 }
367 }
368}
369
370impl<A, P> UpdaterBuilder<A, P> {
371 #[must_use]
374 pub fn swap_fn(mut self, swap_fn: flow::SwapFn) -> Self {
375 self.swap_fn = Some(swap_fn);
376 self
377 }
378
379 #[must_use]
382 pub fn self_test_fn(mut self, self_test_fn: flow::SelfTestFn) -> Self {
383 self.self_test_fn = Some(self_test_fn);
384 self
385 }
386
387 #[must_use]
396 pub fn cache_dir(mut self, cache_dir: impl Into<PathBuf>) -> Self {
397 self.cache_dir = Some(cache_dir.into());
398 self
399 }
400}
401
402impl UpdaterBuilder<HasApp, HasProvider> {
403 #[must_use]
406 pub fn build(self) -> Updater {
407 Updater {
408 app: self.app.expect("HasApp"),
409 provider: self.provider.expect("HasProvider"),
410 swap_fn: self.swap_fn.unwrap_or_else(flow::default_swap_fn),
411 self_test_fn: self.self_test_fn.unwrap_or_else(flow::default_self_test_fn),
412 cache_dir: self.cache_dir,
413 include_framework_builtin: true,
414 }
415 }
416}