aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJJ Liebig <jonathan.liebig@gmail.com>2026-08-14 21:48:59 +0200
committerGitHub <noreply@github.com>2026-08-14 22:48:59 +0300
commitcb727d055545b5b2fc366f778e4c57874d0aa8c1 (patch)
tree112a6b2ca98c807eeb376c76764e34fb2acef5c9 /src
parentd76657f2c7fc18dcce3b9af43842c8afaba1646b (diff)
fix: use verified local packages for windows updates (#2816)
refs #2751
Diffstat (limited to 'src')
-rw-r--r--src/update.rs120
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);