Skip to main content

hdiff_update_core/
update.rs

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}