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