1use std::{
2 path::{Path, PathBuf},
3 time::{SystemTime, UNIX_EPOCH},
4};
5
6use serde::{Deserialize, Serialize};
7use url::Url;
8
9use crate::{
10 apply_patch, default_platform, download_to_file, read_url_to_string, sha256_file,
11 verify_manifest_signature, verify_sha256, ApplyPatchOptions, Artifact, DeltaArtifact,
12 DownloadEvent, Error, FileDigest, HttpHeader, Result, SelectedArtifact, UpdateManifest,
13 DEFAULT_ALGORITHM,
14};
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
17#[serde(rename_all = "camelCase")]
18pub struct PrepareUpdateOptions {
19 pub manifest_url: String,
20 #[serde(default, skip_serializing_if = "Option::is_none")]
21 pub platform: Option<String>,
22 #[serde(default, skip_serializing_if = "Option::is_none")]
23 pub current_version: Option<String>,
24 #[serde(default, skip_serializing_if = "Option::is_none")]
25 pub current_artifact_path: Option<PathBuf>,
26 #[serde(default, skip_serializing_if = "Option::is_none")]
27 pub output_path: Option<PathBuf>,
28 #[serde(default, skip_serializing_if = "Option::is_none")]
29 pub patch_path: Option<PathBuf>,
30 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub cache_dir: Option<PathBuf>,
32 #[serde(default, skip_serializing_if = "Option::is_none")]
33 pub hpatchz_path: Option<PathBuf>,
34 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub signature_public_key: Option<String>,
36 #[serde(default = "default_true")]
37 pub require_signature: bool,
38 #[serde(default)]
39 pub force: bool,
40 #[serde(default)]
41 pub headers: Vec<HttpHeader>,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub timeout_secs: Option<u64>,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
47#[serde(rename_all = "camelCase")]
48pub enum PreparedUpdateKind {
49 Delta,
50 Full,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
54#[serde(rename_all = "camelCase")]
55pub struct PreparedUpdate {
56 pub version: String,
57 pub platform: String,
58 pub kind: PreparedUpdateKind,
59 pub artifact_path: PathBuf,
60 pub artifact: FileDigest,
61 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub patch_path: Option<PathBuf>,
63 pub bytes_downloaded: u64,
64 pub manifest: UpdateManifest,
65}
66
67pub async fn prepare_update<F>(
68 options: PrepareUpdateOptions,
69 mut on_event: F,
70) -> Result<PreparedUpdate>
71where
72 F: FnMut(DownloadEvent),
73{
74 let platform = options.platform.clone().unwrap_or_else(default_platform);
75 let manifest_text = read_url_to_string(
76 &options.manifest_url,
77 &options.headers,
78 options.timeout_secs,
79 )
80 .await?;
81 let manifest: UpdateManifest = serde_json::from_str(&manifest_text)?;
82 match options.signature_public_key.as_deref() {
83 Some(public_key) => verify_manifest_signature(&manifest, public_key)?,
84 None if options.require_signature => return Err(Error::SignaturePublicKeyMissing),
85 None => {}
86 }
87 let platform_release = manifest.platform(&platform)?;
88 let current_digest = match &options.current_artifact_path {
89 Some(path) => Some(sha256_file(path)?),
90 None => None,
91 };
92 let selected = manifest.select_artifact(
93 &platform,
94 options.current_version.as_deref(),
95 current_digest.as_ref().map(|digest| digest.sha256.as_str()),
96 )?;
97 let cache_dir = options.cache_dir.clone().unwrap_or_else(default_cache_dir);
98
99 match selected {
100 SelectedArtifact::Delta { artifact } => {
101 let current_path = options.current_artifact_path.as_ref().ok_or_else(|| {
102 Error::NoMatchingArtifact {
103 platform: platform.clone(),
104 current_version: options.current_version.clone(),
105 current_sha256: None,
106 }
107 })?;
108 prepare_delta(
109 &options,
110 &manifest,
111 platform_release.full.clone(),
112 artifact,
113 &platform,
114 current_path,
115 &cache_dir,
116 &mut on_event,
117 )
118 .await
119 }
120 SelectedArtifact::Full { artifact } => {
121 prepare_full(
122 &options,
123 &manifest,
124 artifact,
125 &platform,
126 &cache_dir,
127 &mut on_event,
128 )
129 .await
130 }
131 }
132}
133
134async fn prepare_delta<F>(
135 options: &PrepareUpdateOptions,
136 manifest: &UpdateManifest,
137 full_artifact: Artifact,
138 delta: DeltaArtifact,
139 platform: &str,
140 current_path: &Path,
141 cache_dir: &Path,
142 on_event: &mut F,
143) -> Result<PreparedUpdate>
144where
145 F: FnMut(DownloadEvent),
146{
147 if delta.algorithm != DEFAULT_ALGORITHM {
148 return Err(Error::UnsupportedAlgorithm(delta.algorithm));
149 }
150
151 let patch_path = options.patch_path.clone().unwrap_or_else(|| {
152 cache_dir.join(url_file_name(&delta.url).unwrap_or_else(|| {
153 format!(
154 "{}-to-{}.hpatch",
155 delta
156 .from_version
157 .clone()
158 .unwrap_or_else(|| "current".to_string()),
159 manifest.version
160 )
161 }))
162 });
163 let patch_url = resolve_artifact_url(&options.manifest_url, &delta.url);
164 let stats = download_to_file(
165 &patch_url,
166 &patch_path,
167 &options.headers,
168 options.timeout_secs,
169 on_event,
170 )
171 .await?;
172 verify_size(&patch_path, delta.size, stats.bytes_written)?;
173 verify_sha256(&patch_path, &delta.sha256)?;
174
175 let output_path = options.output_path.clone().unwrap_or_else(|| {
176 cache_dir.join(
177 url_file_name(&full_artifact.url)
178 .unwrap_or_else(|| format!("{}-{}.installer", manifest.version, platform)),
179 )
180 });
181 let expected = delta
182 .target_sha256
183 .clone()
184 .unwrap_or_else(|| full_artifact.sha256.clone());
185
186 let apply = apply_patch(&ApplyPatchOptions {
187 old_path: current_path.to_path_buf(),
188 patch_path: patch_path.clone(),
189 output_path: output_path.clone(),
190 hpatchz_path: options.hpatchz_path.clone(),
191 force: options.force,
192 expected_sha256: Some(expected),
193 cache_size: None,
194 parallel_threads: Some(4),
195 verify_checksums: true,
196 })?;
197 if let Some(target_size) = delta.target_size {
198 verify_size(&apply.output.path, target_size, apply.output.size)?;
199 }
200
201 Ok(PreparedUpdate {
202 version: manifest.version.clone(),
203 platform: platform.to_string(),
204 kind: PreparedUpdateKind::Delta,
205 artifact_path: output_path,
206 artifact: apply.output,
207 patch_path: Some(patch_path),
208 bytes_downloaded: stats.bytes_written,
209 manifest: manifest.clone(),
210 })
211}
212
213async fn prepare_full<F>(
214 options: &PrepareUpdateOptions,
215 manifest: &UpdateManifest,
216 artifact: Artifact,
217 platform: &str,
218 cache_dir: &Path,
219 on_event: &mut F,
220) -> Result<PreparedUpdate>
221where
222 F: FnMut(DownloadEvent),
223{
224 let output_path = options.output_path.clone().unwrap_or_else(|| {
225 cache_dir.join(
226 url_file_name(&artifact.url)
227 .unwrap_or_else(|| format!("{}-{}.installer", manifest.version, platform)),
228 )
229 });
230 let artifact_url = resolve_artifact_url(&options.manifest_url, &artifact.url);
231 let stats = download_to_file(
232 &artifact_url,
233 &output_path,
234 &options.headers,
235 options.timeout_secs,
236 on_event,
237 )
238 .await?;
239 verify_size(&output_path, artifact.size, stats.bytes_written)?;
240 let digest = verify_sha256(&output_path, &artifact.sha256)?;
241
242 Ok(PreparedUpdate {
243 version: manifest.version.clone(),
244 platform: platform.to_string(),
245 kind: PreparedUpdateKind::Full,
246 artifact_path: output_path,
247 artifact: digest,
248 patch_path: None,
249 bytes_downloaded: stats.bytes_written,
250 manifest: manifest.clone(),
251 })
252}
253
254fn resolve_artifact_url(manifest_url: &str, artifact_url: &str) -> String {
255 if Url::parse(artifact_url).is_ok() || Path::new(artifact_url).is_absolute() {
256 return artifact_url.to_string();
257 }
258
259 if let Ok(base_url) = Url::parse(manifest_url) {
260 if let Ok(joined) = base_url.join(artifact_url) {
261 return joined.to_string();
262 }
263 }
264
265 let base = Path::new(manifest_url)
266 .parent()
267 .unwrap_or_else(|| Path::new("."));
268 base.join(artifact_url).to_string_lossy().to_string()
269}
270
271fn url_file_name(url_or_path: &str) -> Option<String> {
272 if let Ok(url) = Url::parse(url_or_path) {
273 return url
274 .path_segments()
275 .and_then(|mut segments| segments.next_back())
276 .filter(|name| !name.is_empty())
277 .map(ToOwned::to_owned);
278 }
279 Path::new(url_or_path)
280 .file_name()
281 .map(|name| name.to_string_lossy().to_string())
282}
283
284fn default_cache_dir() -> PathBuf {
285 let millis = SystemTime::now()
286 .duration_since(UNIX_EPOCH)
287 .map(|duration| duration.as_millis())
288 .unwrap_or_default();
289 std::env::temp_dir()
290 .join("tauri-hdiff-update")
291 .join(millis.to_string())
292}
293
294fn verify_size(path: &Path, expected: u64, actual: u64) -> Result<()> {
295 if expected != actual {
296 return Err(Error::SizeMismatch {
297 path: path.to_path_buf(),
298 expected,
299 actual,
300 });
301 }
302 Ok(())
303}
304
305fn default_true() -> bool {
306 true
307}