diff options
| author | JJ Liebig <jonathan.liebig@gmail.com> | 2026-08-14 21:48:59 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-14 22:48:59 +0300 |
| commit | cb727d055545b5b2fc366f778e4c57874d0aa8c1 (patch) | |
| tree | 112a6b2ca98c807eeb376c76764e34fb2acef5c9 /src | |
| parent | d76657f2c7fc18dcce3b9af43842c8afaba1646b (diff) | |
fix: use verified local packages for windows updates (#2816)
refs #2751
Diffstat (limited to 'src')
| -rw-r--r-- | src/update.rs | 120 |
1 files changed, 107 insertions, 13 deletions
diff --git a/src/update.rs b/src/update.rs index abc9663f..161386f9 100644 --- a/src/update.rs +++ b/src/update.rs @@ -8,7 +8,6 @@ use std::collections::BTreeMap; use std::env; -#[cfg(not(windows))] use std::fs; #[cfg(not(windows))] use std::io; @@ -125,6 +124,8 @@ impl UpdateChannel { struct AssetRef { url: String, sha256: Option<String>, + #[cfg(windows)] + format: Option<String>, } impl<'de> Deserialize<'de> for AssetRef { @@ -137,6 +138,8 @@ impl<'de> Deserialize<'de> for AssetRef { serde_json::Value::String(url) if !url.trim().is_empty() => Ok(Self { url: url.trim().to_string(), sha256: None, + #[cfg(windows)] + format: None, }), serde_json::Value::Object(mut object) => { let url = object @@ -146,12 +149,18 @@ impl<'de> Deserialize<'de> for AssetRef { let sha256 = object .remove("sha256") .and_then(|value| value.as_str().map(str::to_string)); + #[cfg(windows)] + let format = object + .remove("format") + .and_then(|value| value.as_str().map(str::to_string)); if url.trim().is_empty() { return Err(serde::de::Error::custom("asset url must not be empty")); } Ok(Self { url: url.trim().to_string(), sha256: sha256.filter(|value| !value.trim().is_empty()), + #[cfg(windows)] + format: format.filter(|value| !value.trim().is_empty()), }) } _ => Err(serde::de::Error::custom( @@ -161,6 +170,30 @@ impl<'de> Deserialize<'de> for AssetRef { } } +#[cfg(windows)] +impl AssetRef { + fn package_format(&self) -> Result<String, String> { + let format = self + .format + .clone() + .unwrap_or_else(|| { + if self.url.to_ascii_lowercase().ends_with(".zip") { + "zip" + } else { + "exe" + } + .into() + }) + .to_ascii_lowercase(); + match format.as_str() { + "zip" | "exe" => Ok(format), + _ => Err(format!( + "update manifest asset has unsupported format '{format}'" + )), + } + } +} + #[derive(Deserialize)] struct UpdateManifest { version: String, @@ -275,6 +308,8 @@ struct ReleaseInfo { target_protocol: Option<u32>, download_url: String, sha256: Option<String>, + #[cfg(windows)] + package_format: String, notes_body: String, } @@ -382,6 +417,8 @@ fn release_info_from_manifest(manifest: &UpdateManifest) -> Result<Option<Releas target_protocol: manifest.protocol, download_url, sha256: Some(sha256), + #[cfg(windows)] + package_format: asset.package_format()?, notes_body, })) } @@ -467,6 +504,8 @@ fn release_info_from_preview_manifest( target_protocol: Some(manifest.protocol), download_url, sha256: asset.sha256.clone(), + #[cfg(windows)] + package_format: asset.package_format()?, notes_body, })) } @@ -637,29 +676,81 @@ fn install_downloaded_update(mut update: DownloadedUpdate) -> Result<(), String> } #[cfg(windows)] +const WINDOWS_INSTALLER: &str = include_str!("../website/install.ps1"); + +#[cfg(windows)] +struct DownloadedWindowsUpdate { + package_path: PathBuf, + installer_path: PathBuf, +} + +#[cfg(windows)] +impl Drop for DownloadedWindowsUpdate { + fn drop(&mut self) { + let _ = fs::remove_file(&self.package_path); + let _ = fs::remove_file(&self.installer_path); + } +} + +#[cfg(windows)] +fn download_windows_update(release: &ReleaseInfo) -> Result<DownloadedWindowsUpdate, String> { + let expected_sha256 = release + .sha256 + .as_deref() + .ok_or("Windows update asset is missing a SHA-256 checksum")?; + let stem = format!("herdr-update-{}", std::process::id()); + let update = DownloadedWindowsUpdate { + package_path: env::temp_dir().join(format!("{stem}.{}", release.package_format)), + installer_path: env::temp_dir().join(format!("{stem}.ps1")), + }; + fs::write(&update.installer_path, WINDOWS_INSTALLER) + .map_err(|err| format!("failed to prepare Windows installer: {err}"))?; + + let status = crate::noninteractive_process::curl_command() + .args(["-sfL", "--max-time", "120", "-o"]) + .arg(&update.package_path) + .arg(&release.download_url) + .status() + .map_err(|err| format!("download failed: {err}"))?; + if !status.success() { + return Err("download failed".into()); + } + crate::checksum::verify_sha256(&update.package_path, expected_sha256) + .map_err(|err| format!("downloaded update checksum verification failed: {err}"))?; + tracing::info!(sha256 = %expected_sha256, "downloaded update checksum verified"); + + Ok(update) +} + +#[cfg(windows)] fn install_windows_update_with_installer( - channel: UpdateChannel, - expected_build_id: Option<&str>, + release: &ReleaseInfo, + update: &DownloadedWindowsUpdate, ) -> Result<(), String> { + let expected_sha256 = release + .sha256 + .as_deref() + .ok_or("Windows update asset is missing a SHA-256 checksum")?; let mut command = Command::new("powershell"); command + .args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-File"]) + .arg(&update.installer_path) + .args(["-Channel", release.channel.as_str(), "-LocalPackagePath"]) + .arg(&update.package_path) .args([ - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-Command", - "irm https://herdr.dev/install.ps1 | iex", + "-LocalPackageFormat", + &release.package_format, + "-LocalPackageIdentity", + release.label(), + "-LocalPackageSha256", + expected_sha256, ]) - .env("HERDR_CHANNEL", channel.as_str()) // Drop any inherited PSModulePath. When herdr is launched from // PowerShell 7, its Core module paths come first and Windows // PowerShell 5.1 (this `powershell`) fails to autoload cmdlets like // Get-FileHash. Removing it lets 5.1 compute its own default path. // See PowerShell/PowerShell#8635. .env_remove("PSModulePath"); - if let Some(build_id) = expected_build_id { - command.env("HERDR_EXPECTED_BUILD_ID", build_id); - } let status = command .status() .map_err(|err| format!("failed to run Windows installer: {err}"))?; @@ -2041,7 +2132,10 @@ pub fn self_update(options: SelfUpdateOptions) -> Result<Version, String> { if let Some(sha256) = &release.sha256 { tracing::debug!(sha256 = %sha256, "selected Windows update asset has checksum"); } - install_windows_update_with_installer(channel, release.build_id.as_deref())?; + eprintln!("downloading {}...", release.label()); + let downloaded_update = download_windows_update(&release)?; + eprintln!("downloaded {}", release.label()); + install_windows_update_with_installer(&release, &downloaded_update)?; let updated_exe = windows_installed_herdr_exe_path()?; eprintln!("installed {}", release.label()); print_outdated_integration_notice_with_updated_binary(&updated_exe); |
