/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
cli/tools/upgrade.rs
3 559 строк
104 KB
Bartek Iwańczuk
fix(upgrade): stream zstd delta patch to avoid 2GiB OOM (#35284)
17 июн 2026, 11:14
Не верифицирован
17 июн 2026, 11:14
e19228d
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. //! This module provides feature to upgrade deno executable use std::borrow::Cow; use std::env; use std::fs; use std::io::IsTerminal; use std::ops::Sub; use std::path::Path; use std::path::PathBuf; use std::process::Command; use std::sync::Arc; use std::time::Duration; use async_trait::async_trait; use deno_core::anyhow::Context; use deno_core::anyhow::bail; use deno_core::error::AnyError; use deno_core::unsync::spawn; use deno_core::url::Url; use deno_lib::shared::ReleaseChannel; use deno_lib::version; use deno_semver::SmallStackString; use deno_semver::Version; use once_cell::sync::Lazy; use sha2::Digest; use sys_traits::FsDirEntry; use sys_traits::FsMetadataValue; use crate::args::Flags; use crate::args::UPGRADE_USAGE; use crate::args::UpgradeFlags; use crate::colors; use crate::factory::CliFactory; use crate::http_util::HttpClient; use crate::http_util::HttpClientProvider; use crate::util::archive; use crate::util::progress_bar::ProgressBar; use crate::util::progress_bar::ProgressBarStyle; static RELEASE_URL: &str = "https://github.com/denoland/deno/releases"; static CANARY_URL: &str = "https://dl.deno.land/canary"; static DL_RELEASE_URL: &str = "https://dl.deno.land/release"; pub static ARCHIVE_NAME: Lazy<String> = Lazy::new(|| format!("deno-{}.zip", env!("TARGET"))); static DELTA_TARGET_NAME: Lazy<String> = Lazy::new(|| format!("deno-{}", env!("TARGET"))); // How often query server for new version. In hours. const UPGRADE_CHECK_INTERVAL: i64 = 24; const UPGRADE_CHECK_FETCH_DELAY: Duration = Duration::from_millis(500); /// Environment necessary for doing the update checker. /// An alternate trait implementation can be provided for testing purposes. trait UpdateCheckerEnvironment: Clone { fn read_check_file(&self) -> String; fn write_check_file(&self, text: &str); fn current_time(&self) -> chrono::DateTime<chrono::Utc>; } #[derive(Clone)] struct RealUpdateCheckerEnvironment { cache_file_path: PathBuf, current_time: chrono::DateTime<chrono::Utc>, } impl RealUpdateCheckerEnvironment { pub fn new(cache_file_path: PathBuf) -> Self { Self { cache_file_path, // cache the current time current_time: chrono::Utc::now(), } } } impl UpdateCheckerEnvironment for RealUpdateCheckerEnvironment { fn read_check_file(&self) -> String { std::fs::read_to_string(&self.cache_file_path).unwrap_or_default() } fn write_check_file(&self, text: &str) { let _ = std::fs::write(&self.cache_file_path, text); } fn current_time(&self) -> chrono::DateTime<chrono::Utc> { self.current_time } } #[derive(Debug, Copy, Clone)] enum UpgradeCheckKind { Execution, Lsp, } #[async_trait(?Send)] trait VersionProvider: Clone { /// Fetch latest available version for the given release channel async fn latest_version( &self, release_channel: ReleaseChannel, ) -> Result<AvailableVersion, AnyError>; /// Returns either a semver or git hash. It's up to implementor to /// decide which one is appropriate, but in general only "stable" /// and "lts" versions use semver. fn current_version(&self) -> Cow<'_, str>; fn get_current_exe_release_channel(&self) -> ReleaseChannel; } #[derive(Clone)] struct RealVersionProvider { http_client_provider: Arc<HttpClientProvider>, check_kind: UpgradeCheckKind, } impl RealVersionProvider { pub fn new( http_client_provider: Arc<HttpClientProvider>, check_kind: UpgradeCheckKind, ) -> Self { Self { http_client_provider, check_kind, } } } #[async_trait(?Send)] impl VersionProvider for RealVersionProvider { async fn latest_version( &self, release_channel: ReleaseChannel, ) -> Result<AvailableVersion, AnyError> { fetch_latest_version( &self.http_client_provider.get_or_create()?, release_channel, self.check_kind, ) .await } fn current_version(&self) -> Cow<'_, str> { Cow::Borrowed(version::DENO_VERSION_INFO.version_or_git_hash()) } fn get_current_exe_release_channel(&self) -> ReleaseChannel { version::DENO_VERSION_INFO.release_channel } } struct UpdateChecker< TEnvironment: UpdateCheckerEnvironment, TVersionProvider: VersionProvider, > { env: TEnvironment, version_provider: TVersionProvider, maybe_file: Option<CheckVersionFile>, } impl<TEnvironment: UpdateCheckerEnvironment, TVersionProvider: VersionProvider> UpdateChecker<TEnvironment, TVersionProvider> { pub fn new(env: TEnvironment, version_provider: TVersionProvider) -> Self { let maybe_file = CheckVersionFile::parse(env.read_check_file()); Self { env, version_provider, maybe_file, } } pub fn should_check_for_new_version(&self) -> bool { let Some(file) = &self.maybe_file else { return true; }; let last_check_age = self .env .current_time() .signed_duration_since(file.last_checked); last_check_age > chrono::Duration::hours(UPGRADE_CHECK_INTERVAL) } /// Returns the current exe release channel and a version if a new one is available and it should be prompted about. pub fn should_prompt(&self) -> Option<(ReleaseChannel, String)> { let file = self.maybe_file.as_ref()?; // If the current version saved is not the actually current version of the binary // It means // - We already check for a new version today // - The user have probably upgraded today // So we should not prompt and wait for tomorrow for the latest version to be updated again let current_version = self.version_provider.current_version(); if file.current_version != current_version { return None; } if file.latest_version == current_version { return None; } if let Ok(current) = Version::parse_standard(¤t_version) && let Ok(latest) = Version::parse_standard(&file.latest_version) && current >= latest { return None; } let last_prompt_age = self .env .current_time() .signed_duration_since(file.last_prompt); if last_prompt_age > chrono::Duration::hours(UPGRADE_CHECK_INTERVAL) { Some((file.current_release_channel, file.latest_version.clone())) } else { None } } /// Store that we showed the update message to the user. pub fn store_prompted(self) { if let Some(file) = self.maybe_file { self.env.write_check_file( &file.with_last_prompt(self.env.current_time()).serialize(), ); } } } fn get_minor_version_blog_post_url(semver: &Version) -> String { format!("https://deno.com/blog/v{}.{}", semver.major, semver.minor) } fn get_rc_version_blog_post_url(semver: &Version) -> String { format!( "https://deno.com/blog/v{}.{}-rc-{}", semver.major, semver.minor, semver.pre[1] ) } async fn print_release_notes( current_version: &str, new_version: &str, client: &HttpClient, ) { let Ok(current_semver) = Version::parse_standard(current_version) else { return; }; let Ok(new_semver) = Version::parse_standard(new_version) else { return; }; let is_switching_from_deno1_to_deno2 = new_semver.major == 2 && current_semver.major == 1; let is_deno_2_rc = new_semver.major == 2 && new_semver.minor == 0 && new_semver.patch == 0 && new_semver.pre.first().map(|s| s.as_str()) == Some("rc"); if is_deno_2_rc || is_switching_from_deno1_to_deno2 { log::info!( "{}\n\n {}\n", colors::gray("Migration guide:"), colors::bold( "https://docs.deno.com/runtime/manual/advanced/migrate_deprecations" ) ); } if is_deno_2_rc { log::info!( "{}\n\n {}\n", colors::gray("If you find a bug, please report to:"), colors::bold("https://github.com/denoland/deno/issues/new") ); // Check if there's blog post entry for this release let blog_url_str = get_rc_version_blog_post_url(&new_semver); let blog_url = Url::parse(&blog_url_str).unwrap(); if client.download(blog_url).await.is_ok() { log::info!( "{}\n\n {}\n", colors::gray("Blog post:"), colors::bold(blog_url_str) ); } return; } let should_print = current_semver.major != new_semver.major || current_semver.minor != new_semver.minor; if !should_print { return; } log::info!( "{}\n\n {}\n", colors::gray("Release notes:"), colors::bold(format!( "https://github.com/denoland/deno/releases/tag/v{}", &new_version, )) ); log::info!( "{}\n\n {}\n", colors::gray("Blog post:"), colors::bold(get_minor_version_blog_post_url(&new_semver)) ); } pub fn upgrade_check_enabled() -> bool { matches!( env::var("DENO_NO_UPDATE_CHECK"), Err(env::VarError::NotPresent) ) } pub fn check_for_upgrades( http_client_provider: Arc<HttpClientProvider>, cache_file_path: PathBuf, ) { if !upgrade_check_enabled() { return; } let env = RealUpdateCheckerEnvironment::new(cache_file_path); let version_provider = RealVersionProvider::new( http_client_provider.clone(), UpgradeCheckKind::Execution, ); let update_checker = UpdateChecker::new(env, version_provider); if update_checker.should_check_for_new_version() { let env = update_checker.env.clone(); let version_provider = update_checker.version_provider.clone(); // do this asynchronously on a separate task spawn(async move { // Sleep for a small amount of time to not unnecessarily impact startup // time. tokio::time::sleep(UPGRADE_CHECK_FETCH_DELAY).await; fetch_and_store_latest_version(&env, &version_provider).await; // text is used by the test suite log::debug!("Finished upgrade checker.") }); } // Don't bother doing any more computation if we're not in TTY environment. let should_prompt = log::log_enabled!(log::Level::Info) && std::io::stderr().is_terminal(); if !should_prompt { return; } // Print a message if an update is available if let Some((release_channel, upgrade_version)) = update_checker.should_prompt() { match release_channel { ReleaseChannel::Stable => { log::info!( "{} {} → {} {}", colors::green("A new release of Deno is available:"), colors::cyan(version::DENO_VERSION_INFO.deno), colors::cyan(&upgrade_version), colors::italic_gray("Run `deno upgrade` to install it.") ); } ReleaseChannel::Canary => { log::info!( "{} {}", colors::green("A new canary release of Deno is available."), colors::italic_gray("Run `deno upgrade canary` to install it.") ); } ReleaseChannel::Rc => { log::info!( "{} {}", colors::green("A new release candidate of Deno is available."), colors::italic_gray("Run `deno upgrade rc` to install it.") ); } ReleaseChannel::Lts => { log::info!( "{} {} → {} {}", colors::green("A new LTS release of Deno is available:"), colors::cyan(version::DENO_VERSION_INFO.deno), colors::cyan(&upgrade_version), colors::italic_gray("Run `deno upgrade lts` to install it.") ); } ReleaseChannel::Alpha => { log::info!( "{} {} → {} {}", colors::green("A new alpha release of Deno is available:"), colors::cyan(version::DENO_VERSION_INFO.deno), colors::cyan(&upgrade_version), colors::italic_gray("Run `deno upgrade alpha` to install it.") ); } ReleaseChannel::Beta => { log::info!( "{} {} → {} {}", colors::green("A new beta release of Deno is available:"), colors::cyan(version::DENO_VERSION_INFO.deno), colors::cyan(&upgrade_version), colors::italic_gray("Run `deno upgrade beta` to install it.") ); } } update_checker.store_prompted(); } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct LspVersionUpgradeInfo { pub latest_version: String, pub is_canary: bool, } pub async fn check_for_upgrades_for_lsp( http_client_provider: Arc<HttpClientProvider>, ) -> Result<Option<LspVersionUpgradeInfo>, AnyError> { if !upgrade_check_enabled() { return Ok(None); } let version_provider = RealVersionProvider::new(http_client_provider, UpgradeCheckKind::Lsp); check_for_upgrades_for_lsp_with_provider(&version_provider).await } async fn check_for_upgrades_for_lsp_with_provider( version_provider: &impl VersionProvider, ) -> Result<Option<LspVersionUpgradeInfo>, AnyError> { let release_channel = version_provider.get_current_exe_release_channel(); let latest_version = version_provider.latest_version(release_channel).await?; let current_version = version_provider.current_version(); // Nothing to upgrade if current_version == latest_version.version_or_hash { return Ok(None); } match release_channel { ReleaseChannel::Stable | ReleaseChannel::Rc | ReleaseChannel::Lts | ReleaseChannel::Alpha | ReleaseChannel::Beta => { if let Ok(current) = Version::parse_standard(¤t_version) && let Ok(latest) = Version::parse_standard(&latest_version.version_or_hash) && current >= latest { return Ok(None); // nothing to upgrade } Ok(Some(LspVersionUpgradeInfo { latest_version: latest_version.version_or_hash, is_canary: false, })) } ReleaseChannel::Canary => Ok(Some(LspVersionUpgradeInfo { latest_version: latest_version.version_or_hash, is_canary: true, })), } } async fn fetch_and_store_latest_version< TEnvironment: UpdateCheckerEnvironment, TVersionProvider: VersionProvider, >( env: &TEnvironment, version_provider: &TVersionProvider, ) { let release_channel = version_provider.get_current_exe_release_channel(); let Ok(latest_version) = version_provider.latest_version(release_channel).await else { return; }; let version_file = CheckVersionFile { // put a date in the past here so that prompt can be shown on next run last_prompt: env .current_time() .sub(chrono::Duration::hours(UPGRADE_CHECK_INTERVAL + 1)), last_checked: env.current_time(), current_version: version_provider.current_version().to_string(), latest_version: latest_version.version_or_hash, current_release_channel: release_channel, }; env.write_check_file(&version_file.serialize()); } fn get_binary_cache_path( dl_dir: &Path, version: &str, release_channel: ReleaseChannel, ) -> PathBuf { let binary_path_suffix = match release_channel { ReleaseChannel::Canary => { format!("canary/{}/{}", version, *ARCHIVE_NAME) } _ => { format!("release/v{}/{}", version, *ARCHIVE_NAME) } }; dl_dir.join(binary_path_suffix) } fn prune_canary_cache< Sys: sys_traits::FsReadDir + sys_traits::FsRemoveDirAll, >( sys: &Sys, dl_dir: &Path, max_entries: usize, ) { let canary_dir = dl_dir.join("canary"); let Ok(entries) = sys.fs_read_dir(&canary_dir) else { return; }; let mut dirs: Vec<(PathBuf, std::time::SystemTime)> = entries .filter_map(|e| e.ok()) .filter(|e| e.file_type().map(|t| t.is_dir()).unwrap_or(false)) .filter_map(|e| { let modified = e.metadata().ok()?.modified().ok()?; Some((e.path().into_owned(), modified)) }) .collect(); if dirs.len() <= max_entries { return; } // Sort by modification time, most recent first dirs.sort_by_key(|b| std::cmp::Reverse(b.1)); for (path, _) in dirs.into_iter().skip(max_entries) { let _ = sys.fs_remove_dir_all(&path); } } fn try_read_cached_binary( sys: &impl sys_traits::FsRead, cache_path: &Path, ) -> Option<Vec<u8>> { match sys.fs_read(cache_path) { Ok(data) => Some(data.into_owned()), Err(_) => None, } } fn store_cached_binary( sys: &impl deno_path_util::fs::AtomicWriteFileSys, cache_path: &Path, data: &[u8], ) { if let Err(err) = deno_path_util::fs::atomic_write_file(sys, cache_path, data, 0o644) { log::debug!("Failed to cache binary: {}", err); } } /// Get the artifact name for the current platform. /// CI artifacts are named like `{profile}-{os}-{arch}-deno`. fn get_pr_artifact_name() -> Result<String, AnyError> { let target = env!("TARGET"); let (os, arch) = if target.contains("linux") && target.contains("x86_64") { ("linux", "x86_64") } else if target.contains("linux") && target.contains("aarch64") { ("linux", "aarch64") } else if target.contains("apple") && target.contains("x86_64") { ("macos", "x86_64") } else if target.contains("apple") && target.contains("aarch64") { ("macos", "aarch64") } else if target.contains("windows") && target.contains("x86_64") { ("windows", "x86_64") } else if target.contains("windows") && target.contains("aarch64") { ("windows", "aarch64") } else { bail!("Unsupported platform for PR builds: {}", target) }; // Prefer release builds, fall back to debug Ok(format!("release-{os}-{arch}-deno")) } fn get_pr_debug_artifact_name() -> Result<String, AnyError> { let release_name = get_pr_artifact_name()?; Ok(release_name.replacen("release-", "debug-", 1)) } fn upgrade_from_pr( pr_number: u64, upgrade_flags: &UpgradeFlags, ) -> Result<(), AnyError> { // Check that `gh` CLI is available let gh_version = Command::new("gh").arg("--version").output(); if gh_version.is_err() { bail!( "The `gh` CLI is required for installing from a PR.\n\ Install it from https://cli.github.com/ and run `gh auth login`." ); } log::info!("{}", colors::gray(format!("Looking up PR #{pr_number}..."))); // Verify the PR exists and get its title/state/branch let pr_info = Command::new("gh") .args([ "pr", "view", &pr_number.to_string(), "--repo", "denoland/deno", "--json", "title,state,headRefName,headRefOid", "-q", r#"[.title, .state, .headRefName, .headRefOid] | @tsv"#, ]) .output() .context("failed to run `gh pr view`")?; if !pr_info.status.success() { let stderr = String::from_utf8_lossy(&pr_info.stderr); bail!("Failed to find PR #{pr_number}: {stderr}"); } let pr_info_str = String::from_utf8_lossy(&pr_info.stdout); let pr_fields: Vec<&str> = pr_info_str.trim().splitn(4, '\t').collect(); let pr_title = pr_fields.first().unwrap_or(&"unknown"); let pr_state = pr_fields.get(1).unwrap_or(&"unknown"); let pr_branch = pr_fields.get(2).unwrap_or(&""); let pr_head_sha = pr_fields.get(3).unwrap_or(&""); log::info!( "PR #{}: {} ({})", pr_number, colors::bold(pr_title), pr_state ); let artifact_name = get_pr_artifact_name()?; let debug_artifact_name = get_pr_debug_artifact_name()?; // Find CI runs for this PR by branch name log::info!("{}", colors::gray("Finding CI artifacts...")); let mut all_run_ids = Vec::new(); if !pr_branch.is_empty() { // Filter by headSha to ensure we only get runs for the PR's current commit let jq_filter = if pr_head_sha.is_empty() { ".[].databaseId".to_string() } else { format!( r#"[.[] | select(.headSha == "{}")] | .[].databaseId"#, pr_head_sha ) }; let branch_runs = Command::new("gh") .args([ "run", "list", "--repo", "denoland/deno", "--branch", pr_branch, "--workflow", "ci", "--limit", "5", "--json", "databaseId,headSha", "-q", &jq_filter, ]) .output() .context("failed to query CI runs by branch")?; if branch_runs.status.success() { let ids = String::from_utf8_lossy(&branch_runs.stdout); for id in ids.trim().lines() { if !id.is_empty() { all_run_ids.push(id.to_string()); } } } } if all_run_ids.is_empty() { bail!( "No CI runs found for PR #{pr_number}. \ The PR may not have been pushed yet, CI hasn't started, \ or CI hasn't run on the latest commit yet." ); } // Try each run to find one with our artifact let temp_dir = tempfile::TempDir::new().context("failed to create temporary directory")?; let download_dir = temp_dir.path(); let mut downloaded = false; for run_id in &all_run_ids { // Try release build first, then debug for name in [&artifact_name, &debug_artifact_name] { log::info!( "{}", colors::gray(format!("Trying run {run_id}, artifact \"{name}\"...")) ); let dl_result = Command::new("gh") .args([ "run", "download", run_id, "--repo", "denoland/deno", "--name", name, "--dir", &download_dir.to_string_lossy(), ]) .output() .context("failed to run `gh run download`")?; if dl_result.status.success() { log::info!( "Downloaded artifact \"{}\" from run {}", colors::green(name), run_id ); downloaded = true; break; } } if downloaded { break; } } if !downloaded { bail!( "Could not find a \"{}\" artifact for PR #{pr_number}.\n\ Available artifacts may have expired or CI may not have completed.\n\ Only release builds on linux-x86_64 and debug builds are typically available for PRs.", artifact_name ); } // Find the downloaded binary let exe_name = if cfg!(windows) { "deno.exe" } else { "deno" }; let new_exe_path = download_dir.join(exe_name); if !new_exe_path.exists() { bail!( "Downloaded artifact does not contain '{}'. Contents: {:?}", exe_name, fs::read_dir(download_dir)? .filter_map(|e| e.ok()) .map(|e| e.file_name().to_string_lossy().to_string()) .collect::<Vec<_>>() ); } // Set executable permissions #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; fs::set_permissions(&new_exe_path, std::fs::Permissions::from_mode(0o755))?; } // Verify the binary works check_exe(&new_exe_path)?; if upgrade_flags.dry_run { log::info!("Upgraded successfully (dry run)"); drop(temp_dir); return Ok(()); } let current_exe_path = std::env::current_exe() .context("failed to get the path of the current executable")?; let output_exe_path = if let Some(output) = &upgrade_flags.output { Cow::Owned(PathBuf::from(output)) } else { Cow::Borrowed(¤t_exe_path) }; #[cfg(windows)] kill_running_deno_lsp_processes(); let output_result = if *output_exe_path == current_exe_path { replace_exe(&new_exe_path, &output_exe_path) } else { fs::rename(&new_exe_path, &*output_exe_path) .or_else(|_| fs::copy(&new_exe_path, &*output_exe_path).map(|_| ())) }; check_windows_access_denied_error(output_result, &output_exe_path)?; log::info!( "\nUpgraded successfully from PR #{} {}\n", colors::green(&pr_number.to_string()), colors::gray(&format!("({})", pr_title)) ); drop(temp_dir); Ok(()) } fn upgrade_from_branch( branch: &str, upgrade_flags: &UpgradeFlags, ) -> Result<(), AnyError> { let gh_version = Command::new("gh").arg("--version").output(); if gh_version.is_err() { bail!( "The `gh` CLI is required for installing from a branch.\n\ Install it from https://cli.github.com/ and run `gh auth login`." ); } log::info!( "{}", colors::gray(format!("Finding CI artifacts for branch '{branch}'...")) ); let artifact_name = get_pr_artifact_name()?; let debug_artifact_name = get_pr_debug_artifact_name()?; let runs_output = Command::new("gh") .args([ "run", "list", "--repo", "denoland/deno", "--branch", branch, "--workflow", "ci", "--limit", "5", "--json", "databaseId", "-q", ".[].databaseId", ]) .output() .context("failed to query CI runs")?; if !runs_output.status.success() { let stderr = String::from_utf8_lossy(&runs_output.stderr); bail!("Failed to find CI runs for branch '{branch}': {stderr}"); } let run_ids: Vec<String> = String::from_utf8_lossy(&runs_output.stdout) .trim() .lines() .filter(|l| !l.is_empty()) .map(|s| s.to_string()) .collect(); if run_ids.is_empty() { bail!( "No CI runs found for branch '{branch}'. \ CI may not have run yet." ); } let temp_dir = tempfile::TempDir::new().context("failed to create temporary directory")?; let download_dir = temp_dir.path(); let mut downloaded = false; for run_id in &run_ids { for name in [&artifact_name, &debug_artifact_name] { log::info!( "{}", colors::gray(format!("Trying run {run_id}, artifact \"{name}\"...")) ); let dl_result = Command::new("gh") .args([ "run", "download", run_id, "--repo", "denoland/deno", "--name", name, "--dir", &download_dir.to_string_lossy(), ]) .output() .context("failed to run `gh run download`")?; if dl_result.status.success() { log::info!( "Downloaded artifact \"{}\" from run {}", colors::green(name), run_id ); downloaded = true; break; } } if downloaded { break; } } if !downloaded { bail!( "Could not find a \"{artifact_name}\" artifact for branch '{branch}'.\n\ Artifacts may have expired or CI may not have completed." ); } let exe_name = if cfg!(windows) { "deno.exe" } else { "deno" }; let new_exe_path = download_dir.join(exe_name); if !new_exe_path.exists() { bail!("Downloaded artifact does not contain '{exe_name}'."); } #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; fs::set_permissions(&new_exe_path, std::fs::Permissions::from_mode(0o755))?; } check_exe(&new_exe_path)?; if upgrade_flags.dry_run { log::info!("Upgraded successfully (dry run)"); drop(temp_dir); return Ok(()); } let current_exe_path = std::env::current_exe() .context("failed to get the path of the current executable")?; let output_exe_path = if let Some(output) = &upgrade_flags.output { Cow::Owned(PathBuf::from(output)) } else { Cow::Borrowed(¤t_exe_path) }; #[cfg(windows)] kill_running_deno_lsp_processes(); let output_result = if *output_exe_path == current_exe_path { replace_exe(&new_exe_path, &output_exe_path) } else { fs::rename(&new_exe_path, &*output_exe_path) .or_else(|_| fs::copy(&new_exe_path, &*output_exe_path).map(|_| ())) }; check_windows_access_denied_error(output_result, &output_exe_path)?; log::info!( "\nUpgraded successfully from branch '{}'\n", colors::green(branch), ); drop(temp_dir); Ok(()) } pub async fn upgrade( flags: Arc<Flags>, upgrade_flags: UpgradeFlags, ) -> Result<(), AnyError> { if let Some(pr_number) = upgrade_flags.pr { return upgrade_from_pr(pr_number, &upgrade_flags); } if let Some(ref branch) = upgrade_flags.branch { return upgrade_from_branch(branch, &upgrade_flags); } let factory = CliFactory::from_flags(flags); let cli_options = factory.cli_options()?; let http_client_provider = factory.http_client_provider(); let client = http_client_provider.get_or_create()?; let current_exe_path = std::env::current_exe() .context("failed to get the path of the current executable")?; let full_path_output_flag = upgrade_flags .output .as_ref() .map(|output| cli_options.initial_cwd().join(output)); let output_exe_path = full_path_output_flag.as_ref().unwrap_or(¤t_exe_path); let permissions = set_exe_permissions(¤t_exe_path, output_exe_path)?; let force_selection_of_new_version = upgrade_flags.force || full_path_output_flag.is_some(); let requested_version = RequestedVersion::from_upgrade_flags(upgrade_flags.clone())?; log::info!("Current Deno version: v{}", version::DENO_VERSION_INFO.deno); let maybe_selected_version_to_upgrade = match &requested_version { RequestedVersion::Latest(channel) => { find_latest_version_to_upgrade( http_client_provider.clone(), *channel, force_selection_of_new_version, ) .await? } RequestedVersion::SpecificVersion(channel, version) => { select_specific_version_for_upgrade( *channel, version.clone(), force_selection_of_new_version, )? } }; let Some(selected_version_to_upgrade) = maybe_selected_version_to_upgrade else { return Ok(()); }; let banner_handle = spawn_banner_task( &selected_version_to_upgrade.version_or_hash, selected_version_to_upgrade.release_channel, http_client_provider.get_or_create()?, ); let dl_dir = factory.deno_dir()?.dl_folder_path(); let cache_path = get_binary_cache_path( &dl_dir, &selected_version_to_upgrade.version_or_hash, requested_version.release_channel(), ); let sys = &sys_traits::impls::RealSys; // Try delta upgrade for stable releases when not disabled let maybe_delta_binary = if !upgrade_flags.no_delta && requested_version.release_channel() == ReleaseChannel::Stable && try_read_cached_binary(sys, &cache_path).is_none() { try_delta_upgrade( &client, ¤t_exe_path, version::DENO_VERSION_INFO.deno, &selected_version_to_upgrade.version_or_hash, ) .await } else { None }; let temp_dir = tempfile::TempDir::new().context("failed to create temporary directory")?; let new_exe_path = if let Some(delta_binary) = maybe_delta_binary { // Delta upgrade succeeded -- write the patched binary directly log::info!( "{}", colors::gray(format!( "Deno is upgrading to version {}", &selected_version_to_upgrade.version_or_hash )) ); let exe_name = if cfg!(windows) { "deno.exe" } else { "deno" }; let new_exe_path = temp_dir.path().join(exe_name); std::fs::write(&new_exe_path, &delta_binary).with_context(|| { format!( "failed to write patched binary to '{}'", new_exe_path.display() ) })?; new_exe_path } else { // Full download path (existing behavior) let archive_data = if let Some(data) = try_read_cached_binary(sys, &cache_path) { log::info!( "{}", colors::gray(format!( "Using cached binary from {}", cache_path.display() )) ); data } else { let download_url = get_download_url( &selected_version_to_upgrade.version_or_hash, requested_version.release_channel(), )?; log::info!("{}", colors::gray(format!("Downloading {}", &download_url))); let Some(data) = download_package(&client, download_url).await? else { log::error!("Download could not be found, aborting"); if requested_version.release_channel() == ReleaseChannel::Canary { log::error!("Note: canary releases are only kept for 30 days."); } deno_runtime::exit(1) }; store_cached_binary(sys, &cache_path, &data); data }; // verify checksum if provided if let Some(expected_checksum) = &upgrade_flags.checksum { verify_checksum(&archive_data, expected_checksum)?; } log::info!( "{}", colors::gray(format!( "Deno is upgrading to version {}", &selected_version_to_upgrade.version_or_hash )) ); archive::unpack_into_dir(archive::UnpackArgs { exe_name: if cfg!(windows) { "deno.exe" } else { "deno" }, archive_name: &ARCHIVE_NAME, archive_data: &archive_data, dest_path: temp_dir.path(), }) .context("failed to extract archive")? }; fs::set_permissions(&new_exe_path, permissions).with_context(|| { format!("failed to set permissions on '{}'", new_exe_path.display()) })?; check_exe(&new_exe_path)?; if upgrade_flags.dry_run { fs::remove_file(&new_exe_path).with_context(|| { format!("failed to remove '{}'", new_exe_path.display()) })?; log::info!("Upgraded successfully (dry run)"); if requested_version.release_channel() == ReleaseChannel::Stable { print_release_notes( version::DENO_VERSION_INFO.deno, &selected_version_to_upgrade.version_or_hash, &client, ) .await; } drop(temp_dir); return Ok(()); } let output_exe_path = full_path_output_flag.as_ref().unwrap_or(¤t_exe_path); #[cfg(windows)] kill_running_deno_lsp_processes(); let output_result = if *output_exe_path == current_exe_path { replace_exe(&new_exe_path, output_exe_path) } else { fs::rename(&new_exe_path, output_exe_path) .or_else(|_| fs::copy(&new_exe_path, output_exe_path).map(|_| ())) }; check_windows_access_denied_error(output_result, output_exe_path)?; log::info!( "\nUpgraded successfully to Deno {} {}\n", colors::green(selected_version_to_upgrade.display()), colors::gray(&format!( "({})", selected_version_to_upgrade.release_channel.name() )) ); if requested_version.release_channel() == ReleaseChannel::Stable { print_release_notes( version::DENO_VERSION_INFO.deno, &selected_version_to_upgrade.version_or_hash, &client, ) .await; } if let Ok(Some(text)) = banner_handle.await { log::info!("\n{}\n", text); } drop(temp_dir); // delete the temp dir // Prune old canary cache entries to avoid unbounded growth if requested_version.release_channel() == ReleaseChannel::Canary { prune_canary_cache(sys, &dl_dir, 10); } Ok(()) } #[derive(Debug, PartialEq)] enum RequestedVersion { Latest(ReleaseChannel), SpecificVersion(ReleaseChannel, String), } impl RequestedVersion { fn from_upgrade_flags(upgrade_flags: UpgradeFlags) -> Result<Self, AnyError> { let is_canary = upgrade_flags.canary; let re_hash = lazy_regex::regex!("^[0-9a-f]{40}$"); let channel = if is_canary { ReleaseChannel::Canary } else if upgrade_flags.release_candidate { ReleaseChannel::Rc } else { ReleaseChannel::Stable }; let mut maybe_passed_version = upgrade_flags.version.clone(); // TODO(bartlomieju): prefer flags first? This whole logic could be cleaned up... if let Some(val) = &upgrade_flags.version_or_hash_or_channel { if let Ok(channel) = ReleaseChannel::deserialize(&val.to_lowercase()) { // TODO(bartlomieju): print error if any other flags passed? return Ok(Self::Latest(channel)); } else if re_hash.is_match(val) { return Ok(Self::SpecificVersion( ReleaseChannel::Canary, val.to_string(), )); } else { maybe_passed_version = Some(val.to_string()); } } let Some(passed_version) = maybe_passed_version else { return Ok(Self::Latest(channel)); }; let passed_version = passed_version .strip_prefix('v') .unwrap_or(&passed_version) .to_string(); let (channel, passed_version) = if is_canary { if !re_hash.is_match(&passed_version) { bail!( "Invalid commit hash passed ({})\n\nPass a semver, or a full 40 character git commit hash, or a release channel name.\n\nUsage:\n{}", colors::gray(passed_version), UPGRADE_USAGE ); } (ReleaseChannel::Canary, passed_version) } else { let Ok(semver) = Version::parse_standard(&passed_version) else { bail!( "Invalid version passed ({})\n\nPass a semver, or a full 40 character git commit hash, or a release channel name.\n\nUsage:\n{}", colors::gray(passed_version), UPGRADE_USAGE ); }; if semver.pre.contains(&SmallStackString::from_static("alpha")) { (ReleaseChannel::Alpha, passed_version) } else if semver.pre.contains(&SmallStackString::from_static("beta")) { (ReleaseChannel::Beta, passed_version) } else if semver.pre.contains(&SmallStackString::from_static("rc")) { (ReleaseChannel::Rc, passed_version) } else { (ReleaseChannel::Stable, passed_version) } }; Ok(RequestedVersion::SpecificVersion(channel, passed_version)) } /// Channels that use Git hashes as versions are considered canary. pub fn release_channel(&self) -> ReleaseChannel { match self { Self::Latest(channel) => *channel, Self::SpecificVersion(channel, _) => *channel, } } } fn select_specific_version_for_upgrade( release_channel: ReleaseChannel, version: String, force: bool, ) -> Result<Option<AvailableVersion>, AnyError> { let current_is_passed = match release_channel { ReleaseChannel::Stable | ReleaseChannel::Rc | ReleaseChannel::Lts | ReleaseChannel::Alpha | ReleaseChannel::Beta => { version::DENO_VERSION_INFO.release_channel == release_channel && version::DENO_VERSION_INFO.deno == version } ReleaseChannel::Canary => version::DENO_VERSION_INFO.git_hash == version, }; if !force && current_is_passed { log::info!( "Version {} is already installed", version::DENO_VERSION_INFO.deno ); return Ok(None); } Ok(Some(AvailableVersion { version_or_hash: version, release_channel, })) } async fn find_latest_version_to_upgrade( http_client_provider: Arc<HttpClientProvider>, release_channel: ReleaseChannel, force: bool, ) -> Result<Option<AvailableVersion>, AnyError> { log::info!( "{}", colors::gray(&format!("Looking up {} version", release_channel.name())) ); let client = http_client_provider.get_or_create()?; let latest_version_found = match fetch_latest_version( &client, release_channel, UpgradeCheckKind::Execution, ) .await { Ok(v) => v, Err(err) => { if err.to_string().contains("Not found") { bail!( "No {} release available at the moment.", release_channel.name() ); } else { return Err(err); } } }; let current_version = match release_channel { ReleaseChannel::Canary => version::DENO_VERSION_INFO.git_hash, ReleaseChannel::Stable | ReleaseChannel::Lts | ReleaseChannel::Rc | ReleaseChannel::Alpha | ReleaseChannel::Beta => version::DENO_VERSION_INFO.deno, }; let should_upgrade = force || current_version != latest_version_found.version_or_hash || version::DENO_VERSION_INFO.release_channel != release_channel; log::info!(""); if should_upgrade { log::info!( "Found latest {} version {}", latest_version_found.release_channel.name(), color_print::cformat!("<g>{}</>", latest_version_found.display()) ); } else { log::info!( "Local deno version {} is the most recent release", color_print::cformat!("<g>{}</>", current_version) ); } log::info!(""); Ok(should_upgrade.then_some(latest_version_found)) } #[derive(Debug, Clone, PartialEq)] struct AvailableVersion { version_or_hash: String, release_channel: ReleaseChannel, } impl AvailableVersion { /// Format display version, appending `v` before version number /// for non-canary releases. fn display(&self) -> Cow<'_, str> { match self.release_channel { ReleaseChannel::Canary => Cow::Borrowed(&self.version_or_hash), _ => Cow::Owned(format!("v{}", self.version_or_hash)), } } } async fn fetch_latest_version( client: &HttpClient, release_channel: ReleaseChannel, check_kind: UpgradeCheckKind, ) -> Result<AvailableVersion, AnyError> { let url = get_latest_version_url(release_channel, env!("TARGET"), check_kind); let text = client.download_text(url.parse()?).await?; let version = normalize_version_from_server(release_channel, &text)?; Ok(version) } fn normalize_version_from_server( release_channel: ReleaseChannel, text: &str, ) -> Result<AvailableVersion, AnyError> { let text = text.trim(); match release_channel { ReleaseChannel::Stable | ReleaseChannel::Rc | ReleaseChannel::Lts | ReleaseChannel::Alpha | ReleaseChannel::Beta => { let v = text.trim_start_matches('v').to_string(); Ok(AvailableVersion { version_or_hash: v.to_string(), release_channel, }) } ReleaseChannel::Canary => Ok(AvailableVersion { version_or_hash: text.to_string(), release_channel, }), } } fn get_latest_version_url( release_channel: ReleaseChannel, target_tuple: &str, check_kind: UpgradeCheckKind, ) -> String { let file_name = match release_channel { ReleaseChannel::Stable => Cow::Borrowed("release-latest.txt"), ReleaseChannel::Canary => { Cow::Owned(format!("canary-{target_tuple}-latest.txt")) } ReleaseChannel::Rc => Cow::Borrowed("release-rc-latest.txt"), ReleaseChannel::Lts => Cow::Borrowed("release-lts-latest.txt"), ReleaseChannel::Alpha => Cow::Borrowed("release-alpha-latest.txt"), ReleaseChannel::Beta => Cow::Borrowed("release-beta-latest.txt"), }; let query_param = match check_kind { UpgradeCheckKind::Execution => "", UpgradeCheckKind::Lsp => "?lsp", }; format!("{}/{}{}", base_upgrade_url(), file_name, query_param) } fn base_upgrade_url() -> Cow<'static, str> { // this is used by the test suite if let Ok(url) = env::var("DENO_DONT_USE_INTERNAL_BASE_UPGRADE_URL") { Cow::Owned(url) } else { Cow::Borrowed("https://dl.deno.land") } } fn get_download_url( version: &str, release_channel: ReleaseChannel, ) -> Result<Url, AnyError> { let download_url = match release_channel { ReleaseChannel::Stable | ReleaseChannel::Alpha | ReleaseChannel::Beta => { let release_url = if std::env::var_os("DENO_TESTING_UPGRADE").is_some() { "http://localhost:4545/deno-upgrade" } else { RELEASE_URL }; format!("{}/download/v{}/{}", release_url, version, *ARCHIVE_NAME) } ReleaseChannel::Rc => { format!("{}/v{}/{}", DL_RELEASE_URL, version, *ARCHIVE_NAME) } ReleaseChannel::Canary => { format!("{}/{}/{}", CANARY_URL, version, *ARCHIVE_NAME) } ReleaseChannel::Lts => { format!("{}/v{}/{}", DL_RELEASE_URL, version, *ARCHIVE_NAME) } }; Url::parse(&download_url).with_context(|| { format!( "Failed to parse URL to download new release: {}", download_url ) }) } /// Build the chain of consecutive versions from current to target. /// Each release only has a delta from its immediate predecessor, so /// multi-step upgrades (e.g. 2.7.10 -> 2.7.12) require chaining: /// download 2.7.11's delta from 2.7.10, apply, /// download 2.7.12's delta from 2.7.11, apply. /// /// Supports: /// - Same-minor patch upgrades (e.g. 2.7.10 -> 2.7.12) /// - Cross-minor upgrades (e.g. 2.7.13 -> 2.8.0 -> 2.8.1): the first /// step crosses the minor boundary, remaining steps are same-minor. /// /// Total chain length is capped at 3 steps. Returns empty if the /// versions are not eligible for delta upgrade. fn build_delta_chain( current_version: &str, target_version: &str, ) -> Vec<(String, String)> { let Ok(current) = Version::parse_standard(current_version) else { return vec![]; }; let Ok(target) = Version::parse_standard(target_version) else { return vec![]; }; if !current.pre.is_empty() || !target.pre.is_empty() || current.major != target.major { return vec![]; } let mut chain = Vec::new(); if current.minor == target.minor { // Same minor: chain through consecutive patches let diff = target.patch.saturating_sub(current.patch); if diff == 0 || diff > 3 { return vec![]; } for i in 0..diff { chain.push(( format!("{}.{}.{}", current.major, current.minor, current.patch + i), format!( "{}.{}.{}", current.major, current.minor, current.patch + i + 1 ), )); } } else if target.minor == current.minor + 1 { // Cross-minor: first step jumps to target_minor.0, then chain patches let minor_zero = format!("{}.{}.0", target.major, target.minor); chain.push((current_version.to_string(), minor_zero)); for i in 0..target.patch { chain.push(( format!("{}.{}.{}", target.major, target.minor, i), format!("{}.{}.{}", target.major, target.minor, i + 1), )); } if chain.len() > 3 { return vec![]; } } else { // Too many minor versions apart return vec![]; } chain } /// Construct the delta patch download URL for a stable release. fn get_delta_download_url( current_version: &str, target_version: &str, ) -> Result<Url, AnyError> { let release_url = if std::env::var_os("DENO_TESTING_UPGRADE").is_some() { "http://localhost:4545/deno-upgrade" } else { RELEASE_URL }; let url = format!( "{}/download/v{}/{}.from-{}.bsdiff", release_url, target_version, *DELTA_TARGET_NAME, current_version ); Url::parse(&url) .with_context(|| format!("Failed to parse delta download URL: {}", url)) } /// Construct the URL for the delta patch's SHA-256 checksum file. fn get_delta_checksum_url( current_version: &str, target_version: &str, ) -> Result<Url, AnyError> { let release_url = if std::env::var_os("DENO_TESTING_UPGRADE").is_some() { "http://localhost:4545/deno-upgrade" } else { RELEASE_URL }; let url = format!( "{}/download/v{}/{}.from-{}.bsdiff.sha256sum", release_url, target_version, *DELTA_TARGET_NAME, current_version ); Url::parse(&url) .with_context(|| format!("Failed to parse delta checksum URL: {}", url)) } /// Construct the URL for the uncompressed binary's SHA-256 checksum file. fn get_binary_checksum_url(target_version: &str) -> Result<Url, AnyError> { let release_url = if std::env::var_os("DENO_TESTING_UPGRADE").is_some() { "http://localhost:4545/deno-upgrade" } else { RELEASE_URL }; let url = format!( "{}/download/v{}/{}.sha256sum", release_url, target_version, *DELTA_TARGET_NAME ); Url::parse(&url) .with_context(|| format!("Failed to parse binary checksum URL: {}", url)) } /// Fetch a SHA-256 hash from a .sha256sum file hosted as a release asset. /// The file format is expected to be: `<hex_hash> <filename>\n` async fn fetch_sha256_from_url( client: &HttpClient, url: Url, ) -> Option<String> { let text = client.download_text(url).await.ok()?; // sha256sum files are formatted as "<hash> <filename>" or just "<hash>" let hash = text.split_whitespace().next()?; if hash.len() == 64 && hash.chars().all(|c| c.is_ascii_hexdigit()) { Some(hash.to_lowercase()) } else { None } } /// Compute the SHA-256 hash of the given data, returning it as a lowercase /// hex string. fn compute_sha256(data: &[u8]) -> String { let digest = sha2::Sha256::digest(data); faster_hex::hex_string(&digest) } /// Attempt a delta upgrade by chaining consecutive bsdiff patches. /// For a single-step upgrade (e.g. 2.7.11 -> 2.7.12), downloads and /// applies one patch. For multi-step (e.g. 2.7.10 -> 2.7.12), chains /// patches: downloads the 2.7.10->2.7.11 patch from the 2.7.11 release, /// applies it, then downloads the 2.7.11->2.7.12 patch from the 2.7.12 /// release, applies that. Returns `Some(final_binary)` on success, or /// `None` on any failure (triggering full download fallback). async fn try_delta_upgrade( client: &HttpClient, current_exe: &Path, current_version: &str, target_version: &str, ) -> Option<Vec<u8>> { let chain = build_delta_chain(current_version, target_version); if chain.is_empty() { log::debug!( "No delta chain from {} to {}, skipping delta upgrade", current_version, target_version ); return None; } let steps = chain.len(); log::info!( "{}", colors::gray(format!( "Attempting delta upgrade ({} step{})", steps, if steps > 1 { "s" } else { "" } )) ); // Read the current binary let mut binary = match std::fs::read(current_exe) { Ok(data) => data, Err(e) => { log::debug!("Failed to read current binary: {}", e); return None; } }; // Verify the current binary's integrity let current_checksum_url = get_binary_checksum_url(current_version).ok()?; if let Some(expected_hash) = fetch_sha256_from_url(client, current_checksum_url).await { let actual_hash = compute_sha256(&binary); if actual_hash != expected_hash { log::debug!("Current binary checksum mismatch, skipping delta"); return None; } } else { log::debug!( "Could not fetch checksum for current binary v{}, skipping delta", current_version, ); return None; } // Apply each step in the chain for (i, (src, dst)) in chain.iter().enumerate() { if steps > 1 { log::info!( "{}", colors::gray(format!("Step {}/{}: {} -> {}", i + 1, steps, src, dst)) ); } // Download the delta patch (from the DST version's release) let delta_url = get_delta_download_url(src, dst).ok()?; log::info!( "{}", colors::gray(format!("Downloading delta patch {}", &delta_url)) ); let delta_data = match download_package(client, delta_url).await { Ok(Some(data)) => data, Ok(None) => { log::debug!("Delta patch not found for {} -> {}", src, dst); return None; } Err(e) => { log::debug!("Failed to download delta patch: {}", e); return None; } }; // Verify the delta patch's own checksum let delta_checksum_url = get_delta_checksum_url(src, dst).ok()?; if let Some(expected_delta_hash) = fetch_sha256_from_url(client, delta_checksum_url).await { let actual_delta_hash = compute_sha256(&delta_data); if actual_delta_hash != expected_delta_hash { log::warn!("Delta patch checksum mismatch for {} -> {}", src, dst); return None; } } else { log::debug!( "Could not fetch delta patch checksum for {} -> {}", src, dst ); return None; } // Apply the bsdiff patch binary = match apply_bsdiff_patch(&binary, &delta_data) { Ok(data) => data, Err(e) => { log::warn!("Failed to apply delta patch {} -> {}: {}", src, dst, e); return None; } }; // Verify the intermediate/final binary let checksum_url = get_binary_checksum_url(dst).ok()?; if let Some(expected_hash) = fetch_sha256_from_url(client, checksum_url).await { let actual_hash = compute_sha256(&binary); if actual_hash != expected_hash { log::warn!( "Patched binary checksum mismatch after step {} -> {}", src, dst ); return None; } } else { log::debug!( "Could not fetch binary checksum for v{}, skipping delta", dst ); return None; } } log::info!("{}", colors::gray("Delta upgrade checksum verified")); log::info!( "{}", colors::gray(format!( "Delta upgrade applied successfully ({:.1} MB binary)", binary.len() as f64 / 1_000_000.0 )) ); Some(binary) } /// zstd magic number — published patches generated by `tools/bsdiff_helper` /// are zstd-wrapped. Detected here so we can fall through to legacy raw /// bsdiff for patches uploaded before that change (e.g. v2.8.0's /// `from-2.7.14.bsdiff`). const ZSTD_MAGIC: [u8; 4] = [0x28, 0xB5, 0x2F, 0xFD]; /// Apply a bsdiff patch to produce a new binary. /// /// Accepts both zstd-wrapped patches (current format) and raw bsdiff patches /// (legacy format, pre-zstd-wrapping). fn apply_bsdiff_patch( old: &[u8], patch_data: &[u8], ) -> Result<Vec<u8>, AnyError> { let decoded; let raw_patch = if patch_data.starts_with(&ZSTD_MAGIC) { // Stream the decompression so the output buffer grows incrementally // instead of pre-allocating MAX_PATCH_SIZE up front (which OOMs on // low-memory devices). `.take(MAX_PATCH_SIZE)` keeps the anti-DoS cap. use std::io::Read; let decoder = zstd::stream::Decoder::new(patch_data) .context("failed to zstd-decompress delta patch")?; let mut buf = Vec::new(); decoder .take(MAX_PATCH_SIZE as u64) .read_to_end(&mut buf) .context("failed to zstd-decompress delta patch")?; decoded = buf; decoded.as_slice() } else { patch_data }; let mut cursor = std::io::Cursor::new(raw_patch); let mut new = Vec::new(); bsdiff::patch(old, &mut cursor, &mut new) .context("bspatch failed to apply delta patch")?; Ok(new) } /// Upper bound on the decompressed bsdiff patch size. Generous (2 GiB) since /// the raw bsdiff stream can exceed the new binary's size; we only need to /// reject pathological/malicious inputs, not enforce a tight budget. const MAX_PATCH_SIZE: usize = 2 * 1024 * 1024 * 1024; fn spawn_banner_task( version: &str, release_channel: ReleaseChannel, client: HttpClient, ) -> deno_core::unsync::JoinHandle<Option<String>> { let banner_url = get_banner_url(version, release_channel); deno_core::unsync::spawn(async move { let banner_url = banner_url?; tokio::select! { result = client.download_text(banner_url) => { result.ok() } _ = tokio::time::sleep(Duration::from_secs(5)) => { None } } }) } fn get_banner_url( version: &str, release_channel: ReleaseChannel, ) -> Option<Url> { let download_url = match release_channel { ReleaseChannel::Stable => { format!("{}/v{}/banner.txt", DL_RELEASE_URL, version) } ReleaseChannel::Rc | ReleaseChannel::Lts | ReleaseChannel::Canary | ReleaseChannel::Alpha | ReleaseChannel::Beta => { return None; } }; Url::parse(&download_url).ok() } async fn download_package( client: &HttpClient, download_url: Url, ) -> Result<Option<Vec<u8>>, AnyError> { let progress_bar = ProgressBar::new(ProgressBarStyle::DownloadBars); // provide an empty string here in order to prefer the downloading // text above which will stay alive after the progress bars are complete let progress = progress_bar.update(""); let response = client .download_with_progress_and_retries(download_url.clone(), &Default::default(), &progress) .await .with_context(|| format!("Failed downloading {download_url}. The version you requested may not have been built for the current architecture."))?; Ok(response.into_maybe_bytes()?) } fn verify_checksum( data: &[u8], expected_checksum: &str, ) -> Result<(), AnyError> { let computed = sha2::Sha256::digest(data); let computed_hex = faster_hex::hex_string(&computed); let expected_checksum = expected_checksum.trim().to_lowercase(); if computed_hex != expected_checksum { bail!( "Checksum verification failed.\n Actual: {}\n Expected: {}", expected_checksum, computed_hex ); } log::info!("{}", colors::gray("Checksum verified")); Ok(()) } fn replace_exe(from: &Path, to: &Path) -> Result<(), std::io::Error> { if cfg!(windows) { // On windows you cannot replace the currently running executable. // so first we rename it to deno.old.exe fs::rename(to, to.with_extension("old.exe"))?; } else { fs::remove_file(to)?; } // Windows cannot rename files across device boundaries, so if rename fails, // we try again with copy. fs::rename(from, to).or_else(|_| fs::copy(from, to).map(|_| ()))?; Ok(()) } fn check_windows_access_denied_error( output_result: Result<(), std::io::Error>, output_exe_path: &Path, ) -> Result<(), AnyError> { let Err(err) = output_result else { return Ok(()); }; if !cfg!(windows) { return Err(err).with_context(|| { format!( "failed to replace the executable at '{}'", output_exe_path.display() ) }); } const WIN_ERROR_ACCESS_DENIED: i32 = 5; if err.raw_os_error() != Some(WIN_ERROR_ACCESS_DENIED) { return Err(err.into()); }; Err(err).with_context(|| { format!( concat!( "Could not replace the deno executable. This may be because an ", "existing deno process is running. Please ensure there are no ", "running deno processes (ex. Stop-Process -Name deno ; deno {}), ", "close any editors before upgrading, and ensure you have ", "sufficient permission to '{}'." ), // skip the first argument, which is the executable path std::env::args().skip(1).collect::<Vec<_>>().join(" "), output_exe_path.display(), ) }) } #[cfg(windows)] fn kill_running_deno_lsp_processes() { // limit this to `deno lsp` invocations to avoid killing important programs someone might be running let is_debug = log::log_enabled!(log::Level::Debug); let get_pipe = || { if is_debug { std::process::Stdio::inherit() } else { std::process::Stdio::null() } }; let _ = Command::new("powershell.exe") .args([ "-Command", r#"Get-WmiObject Win32_Process | Where-Object { $_.Name -eq 'deno.exe' -and $_.CommandLine -match '^(?:\"[^\"]+\"|\S+)\s+lsp\b' } | ForEach-Object { if ($_.Terminate()) { Write-Host 'Terminated:' $_.ProcessId } }"#, ]) .stdout(get_pipe()) .stderr(get_pipe()) .output(); } fn set_exe_permissions( current_exe_path: &Path, output_exe_path: &Path, ) -> Result<std::fs::Permissions, AnyError> { let Ok(metadata) = fs::metadata(output_exe_path) else { let metadata = fs::metadata(current_exe_path).with_context(|| { format!( "failed to get metadata of the current executable at '{}'", current_exe_path.display() ) })?; return Ok(metadata.permissions()); }; let permissions = metadata.permissions(); if permissions.readonly() { bail!( "You do not have write permission to {}", output_exe_path.display() ); } #[cfg(unix)] if std::os::unix::fs::MetadataExt::uid(&metadata) == 0 && !nix::unistd::Uid::effective().is_root() { bail!( concat!( "You don't have write permission to {} because it's owned by root.\n", "Consider updating deno through your package manager if its installed from it.\n", "Otherwise run `deno upgrade` as root.", ), output_exe_path.display() ); } Ok(permissions) } fn check_exe(exe_path: &Path) -> Result<(), AnyError> { let output = Command::new(exe_path) .arg("-V") .stderr(std::process::Stdio::inherit()) .output() .with_context(|| format!("failed to run '{}'", exe_path.display()))?; if !output.status.success() { bail!( "Failed to validate Deno executable. This may be because your OS is unsupported or the executable is corrupted" ) } else { Ok(()) } } #[derive(Debug)] struct CheckVersionFile { pub last_prompt: chrono::DateTime<chrono::Utc>, pub last_checked: chrono::DateTime<chrono::Utc>, pub current_version: String, pub latest_version: String, pub current_release_channel: ReleaseChannel, } impl CheckVersionFile { pub fn parse(content: String) -> Option<Self> { let split_content = content.split('!').collect::<Vec<_>>(); if split_content.len() != 5 { return None; } let latest_version = split_content[2].trim().to_owned(); if latest_version.is_empty() { return None; } let current_version = split_content[3].trim().to_owned(); if current_version.is_empty() { return None; } let current_release_channel = split_content[4].trim().to_owned(); if current_release_channel.is_empty() { return None; } let Ok(current_release_channel) = ReleaseChannel::deserialize(¤t_release_channel) else { return None; }; let last_prompt = chrono::DateTime::parse_from_rfc3339(split_content[0]) .map(|dt| dt.with_timezone(&chrono::Utc)) .ok()?; let last_checked = chrono::DateTime::parse_from_rfc3339(split_content[1]) .map(|dt| dt.with_timezone(&chrono::Utc)) .ok()?; Some(CheckVersionFile { last_prompt, last_checked, current_version, latest_version, current_release_channel, }) } fn serialize(&self) -> String { format!( "{}!{}!{}!{}!{}", self.last_prompt.to_rfc3339(), self.last_checked.to_rfc3339(), self.latest_version, self.current_version, self.current_release_channel.serialize() ) } fn with_last_prompt(self, dt: chrono::DateTime<chrono::Utc>) -> Self { Self { last_prompt: dt, ..self } } } #[cfg(test)] mod test { use std::cell::RefCell; use std::rc::Rc; use test_util::assert_contains; use super::*; #[test] fn test_get_pr_artifact_name() { let name = get_pr_artifact_name().unwrap(); // Should match the pattern "release-{os}-{arch}-deno" assert!( name.starts_with("release-"), "artifact name should start with 'release-': {name}" ); assert!( name.ends_with("-deno"), "artifact name should end with '-deno': {name}" ); // Should contain a valid os assert!( name.contains("linux") || name.contains("macos") || name.contains("windows"), "artifact name should contain os: {name}" ); // Should contain a valid arch assert!( name.contains("x86_64") || name.contains("aarch64"), "artifact name should contain arch: {name}" ); } #[test] fn test_get_pr_debug_artifact_name() { let release_name = get_pr_artifact_name().unwrap(); let debug_name = get_pr_debug_artifact_name().unwrap(); assert!( debug_name.starts_with("debug-"), "debug artifact name should start with 'debug-': {debug_name}" ); // The rest should match assert_eq!( release_name.strip_prefix("release-"), debug_name.strip_prefix("debug-"), ); } #[test] fn test_requested_version() { let mut upgrade_flags = UpgradeFlags { dry_run: false, force: false, release_candidate: false, canary: false, no_delta: false, version: None, output: None, version_or_hash_or_channel: None, checksum: None, pr: None, branch: None, }; let req_ver = RequestedVersion::from_upgrade_flags(upgrade_flags.clone()).unwrap(); assert_eq!(req_ver, RequestedVersion::Latest(ReleaseChannel::Stable)); upgrade_flags.version = Some("1.46.0".to_string()); let req_ver = RequestedVersion::from_upgrade_flags(upgrade_flags.clone()).unwrap(); assert_eq!( req_ver, RequestedVersion::SpecificVersion( ReleaseChannel::Stable, "1.46.0".to_string() ) ); upgrade_flags.version = None; upgrade_flags.canary = true; let req_ver = RequestedVersion::from_upgrade_flags(upgrade_flags.clone()).unwrap(); assert_eq!(req_ver, RequestedVersion::Latest(ReleaseChannel::Canary)); upgrade_flags.version = Some("5c69b4861b52ab406e73b9cd85c254f0505cb20f".to_string()); let req_ver = RequestedVersion::from_upgrade_flags(upgrade_flags.clone()).unwrap(); assert_eq!( req_ver, RequestedVersion::SpecificVersion( ReleaseChannel::Canary, "5c69b4861b52ab406e73b9cd85c254f0505cb20f".to_string() ) ); upgrade_flags.version = None; upgrade_flags.canary = false; upgrade_flags.release_candidate = true; let req_ver = RequestedVersion::from_upgrade_flags(upgrade_flags.clone()).unwrap(); assert_eq!(req_ver, RequestedVersion::Latest(ReleaseChannel::Rc)); upgrade_flags.release_candidate = false; upgrade_flags.version_or_hash_or_channel = Some("v1.46.5".to_string()); let req_ver = RequestedVersion::from_upgrade_flags(upgrade_flags.clone()).unwrap(); assert_eq!( req_ver, RequestedVersion::SpecificVersion( ReleaseChannel::Stable, "1.46.5".to_string() ) ); upgrade_flags.version_or_hash_or_channel = Some("2.0.0-rc.0".to_string()); let req_ver = RequestedVersion::from_upgrade_flags(upgrade_flags.clone()).unwrap(); assert_eq!( req_ver, RequestedVersion::SpecificVersion( ReleaseChannel::Rc, "2.0.0-rc.0".to_string() ) ); upgrade_flags.version_or_hash_or_channel = Some("canary".to_string()); let req_ver = RequestedVersion::from_upgrade_flags(upgrade_flags.clone()).unwrap(); assert_eq!(req_ver, RequestedVersion::Latest(ReleaseChannel::Canary,)); upgrade_flags.version_or_hash_or_channel = Some("rc".to_string()); let req_ver = RequestedVersion::from_upgrade_flags(upgrade_flags.clone()).unwrap(); assert_eq!(req_ver, RequestedVersion::Latest(ReleaseChannel::Rc,)); upgrade_flags.version_or_hash_or_channel = Some("alpha".to_string()); let req_ver = RequestedVersion::from_upgrade_flags(upgrade_flags.clone()).unwrap(); assert_eq!(req_ver, RequestedVersion::Latest(ReleaseChannel::Alpha)); upgrade_flags.version_or_hash_or_channel = Some("beta".to_string()); let req_ver = RequestedVersion::from_upgrade_flags(upgrade_flags.clone()).unwrap(); assert_eq!(req_ver, RequestedVersion::Latest(ReleaseChannel::Beta)); upgrade_flags.version_or_hash_or_channel = Some("2.8.0-alpha.0".to_string()); let req_ver = RequestedVersion::from_upgrade_flags(upgrade_flags.clone()).unwrap(); assert_eq!( req_ver, RequestedVersion::SpecificVersion( ReleaseChannel::Alpha, "2.8.0-alpha.0".to_string() ) ); upgrade_flags.version_or_hash_or_channel = Some("2.8.0-beta.1".to_string()); let req_ver = RequestedVersion::from_upgrade_flags(upgrade_flags.clone()).unwrap(); assert_eq!( req_ver, RequestedVersion::SpecificVersion( ReleaseChannel::Beta, "2.8.0-beta.1".to_string() ) ); upgrade_flags.version_or_hash_or_channel = Some("5c69b4861b52ab406e73b9cd85c254f0505cb20f".to_string()); let req_ver = RequestedVersion::from_upgrade_flags(upgrade_flags.clone()).unwrap(); assert_eq!( req_ver, RequestedVersion::SpecificVersion( ReleaseChannel::Canary, "5c69b4861b52ab406e73b9cd85c254f0505cb20f".to_string() ) ); upgrade_flags.version_or_hash_or_channel = Some("5c69b4861b52a".to_string()); let err = RequestedVersion::from_upgrade_flags(upgrade_flags.clone()) .unwrap_err() .to_string(); assert_contains!(err, "Invalid version passed"); assert_contains!( err, "Pass a semver, or a full 40 character git commit hash, or a release channel name." ); upgrade_flags.version_or_hash_or_channel = Some("11.asd.1324".to_string()); let err = RequestedVersion::from_upgrade_flags(upgrade_flags.clone()) .unwrap_err() .to_string(); assert_contains!(err, "Invalid version passed"); assert_contains!( err, "Pass a semver, or a full 40 character git commit hash, or a release channel name." ); } #[test] fn test_parse_upgrade_check_file() { // NOTE(bartlomieju): pre-1.46 format let maybe_file = CheckVersionFile::parse( "2020-01-01T00:00:00+00:00!2020-01-01T00:00:00+00:00!1.2.3!1.2.2" .to_string(), ); assert!(maybe_file.is_none()); // NOTE(bartlomieju): post-1.46 format let file = CheckVersionFile::parse( "2020-01-01T00:00:00+00:00!2020-01-01T00:00:00+00:00!1.2.3!1.2.2!stable" .to_string(), ) .unwrap(); assert_eq!( file.last_prompt.to_rfc3339(), "2020-01-01T00:00:00+00:00".to_string() ); assert_eq!( file.last_checked.to_rfc3339(), "2020-01-01T00:00:00+00:00".to_string() ); assert_eq!(file.latest_version, "1.2.3".to_string()); assert_eq!(file.current_version, "1.2.2".to_string()); assert_eq!(file.current_release_channel, ReleaseChannel::Stable); let result = CheckVersionFile::parse("2020-01-01T00:00:00+00:00!".to_string()); assert!(result.is_none()); let result = CheckVersionFile::parse("garbage!test".to_string()); assert!(result.is_none()); let result = CheckVersionFile::parse("test".to_string()); assert!(result.is_none()); } #[test] fn test_serialize_upgrade_check_file() { let mut file = CheckVersionFile { last_prompt: chrono::DateTime::parse_from_rfc3339("2020-01-01T00:00:00Z") .unwrap() .with_timezone(&chrono::Utc), last_checked: chrono::DateTime::parse_from_rfc3339( "2020-01-01T00:00:00Z", ) .unwrap() .with_timezone(&chrono::Utc), latest_version: "1.2.3".to_string(), current_version: "1.2.2".to_string(), current_release_channel: ReleaseChannel::Stable, }; assert_eq!( file.serialize(), "2020-01-01T00:00:00+00:00!2020-01-01T00:00:00+00:00!1.2.3!1.2.2!stable" ); file.current_release_channel = ReleaseChannel::Canary; assert_eq!( file.serialize(), "2020-01-01T00:00:00+00:00!2020-01-01T00:00:00+00:00!1.2.3!1.2.2!canary" ); file.current_release_channel = ReleaseChannel::Rc; assert_eq!( file.serialize(), "2020-01-01T00:00:00+00:00!2020-01-01T00:00:00+00:00!1.2.3!1.2.2!rc" ); file.current_release_channel = ReleaseChannel::Lts; assert_eq!( file.serialize(), "2020-01-01T00:00:00+00:00!2020-01-01T00:00:00+00:00!1.2.3!1.2.2!lts" ); file.current_release_channel = ReleaseChannel::Alpha; assert_eq!( file.serialize(), "2020-01-01T00:00:00+00:00!2020-01-01T00:00:00+00:00!1.2.3!1.2.2!alpha" ); file.current_release_channel = ReleaseChannel::Beta; assert_eq!( file.serialize(), "2020-01-01T00:00:00+00:00!2020-01-01T00:00:00+00:00!1.2.3!1.2.2!beta" ); } #[derive(Clone)] struct TestUpdateCheckerEnvironment { file_text: Rc<RefCell<String>>, release_channel: Rc<RefCell<ReleaseChannel>>, current_version: Rc<RefCell<String>>, latest_version: Rc<RefCell<Result<AvailableVersion, String>>>, time: Rc<RefCell<chrono::DateTime<chrono::Utc>>>, } impl TestUpdateCheckerEnvironment { pub fn new() -> Self { Self { file_text: Default::default(), current_version: Default::default(), release_channel: Rc::new(RefCell::new(ReleaseChannel::Stable)), latest_version: Rc::new(RefCell::new(Ok(AvailableVersion { version_or_hash: "".to_string(), release_channel: ReleaseChannel::Stable, }))), time: Rc::new(RefCell::new(chrono::Utc::now())), } } pub fn add_hours(&self, hours: i64) { let mut time = self.time.borrow_mut(); *time = time .checked_add_signed(chrono::Duration::hours(hours)) .unwrap(); } pub fn set_file_text(&self, text: &str) { *self.file_text.borrow_mut() = text.to_string(); } pub fn set_current_version(&self, version: &str) { *self.current_version.borrow_mut() = version.to_string(); } pub fn set_latest_version( &self, version: &str, release_channel: ReleaseChannel, ) { *self.latest_version.borrow_mut() = Ok(AvailableVersion { version_or_hash: version.to_string(), release_channel, }); } pub fn set_latest_version_err(&self, err: &str) { *self.latest_version.borrow_mut() = Err(err.to_string()); } pub fn set_release_channel(&self, channel: ReleaseChannel) { *self.release_channel.borrow_mut() = channel; } } #[async_trait(?Send)] impl VersionProvider for TestUpdateCheckerEnvironment { // TODO(bartlomieju): update to handle `Lts` and `Rc` channels async fn latest_version( &self, _release_channel: ReleaseChannel, ) -> Result<AvailableVersion, AnyError> { match self.latest_version.borrow().clone() { Ok(result) => Ok(result), Err(err) => bail!("{}", err), } } fn current_version(&self) -> Cow<'_, str> { Cow::Owned(self.current_version.borrow().clone()) } fn get_current_exe_release_channel(&self) -> ReleaseChannel { *self.release_channel.borrow() } } impl UpdateCheckerEnvironment for TestUpdateCheckerEnvironment { fn read_check_file(&self) -> String { self.file_text.borrow().clone() } fn write_check_file(&self, text: &str) { self.set_file_text(text); } fn current_time(&self) -> chrono::DateTime<chrono::Utc> { *self.time.borrow() } } #[tokio::test] async fn test_update_checker() { let env = TestUpdateCheckerEnvironment::new(); env.set_current_version("1.0.0"); env.set_latest_version("1.1.0", ReleaseChannel::Stable); let checker = UpdateChecker::new(env.clone(), env.clone()); // no version, so we should check, but not prompt assert!(checker.should_check_for_new_version()); assert_eq!(checker.should_prompt(), None); // store the latest version fetch_and_store_latest_version(&env, &env).await; // reload let checker = UpdateChecker::new(env.clone(), env.clone()); // should not check for latest version because we just did assert!(!checker.should_check_for_new_version()); // but should prompt assert_eq!( checker.should_prompt(), Some((ReleaseChannel::Stable, "1.1.0".to_string())) ); // fast forward an hour and bump the latest version env.add_hours(1); env.set_latest_version("1.2.0", ReleaseChannel::Stable); assert!(!checker.should_check_for_new_version()); assert_eq!( checker.should_prompt(), Some((ReleaseChannel::Stable, "1.1.0".to_string())) ); // fast forward again and it should check for a newer version env.add_hours(UPGRADE_CHECK_INTERVAL); assert!(checker.should_check_for_new_version()); assert_eq!( checker.should_prompt(), Some((ReleaseChannel::Stable, "1.1.0".to_string())) ); fetch_and_store_latest_version(&env, &env).await; // reload and store that we prompted let checker = UpdateChecker::new(env.clone(), env.clone()); assert!(!checker.should_check_for_new_version()); assert_eq!( checker.should_prompt(), Some((ReleaseChannel::Stable, "1.2.0".to_string())) ); checker.store_prompted(); // reload and it should now say not to prompt let checker = UpdateChecker::new(env.clone(), env.clone()); assert!(!checker.should_check_for_new_version()); assert_eq!(checker.should_prompt(), None); // but if we fast forward past the upgrade interval it should prompt again env.add_hours(UPGRADE_CHECK_INTERVAL + 1); assert!(checker.should_check_for_new_version()); assert_eq!( checker.should_prompt(), Some((ReleaseChannel::Stable, "1.2.0".to_string())) ); // upgrade the version and it should stop prompting env.set_current_version("1.2.0"); assert!(checker.should_check_for_new_version()); assert_eq!(checker.should_prompt(), None); // now try failing when fetching the latest version env.add_hours(UPGRADE_CHECK_INTERVAL + 1); env.set_latest_version_err("Failed"); env.set_latest_version("1.3.0", ReleaseChannel::Stable); // this will silently fail fetch_and_store_latest_version(&env, &env).await; assert!(checker.should_check_for_new_version()); assert_eq!(checker.should_prompt(), None); // now switch to RC release env.set_release_channel(ReleaseChannel::Rc); env.set_current_version("1.46.0-rc.0"); env.set_latest_version("1.46.0-rc.1", ReleaseChannel::Rc); fetch_and_store_latest_version(&env, &env).await; env.add_hours(UPGRADE_CHECK_INTERVAL + 1); // We should check for new version and prompt let checker = UpdateChecker::new(env.clone(), env.clone()); assert!(checker.should_check_for_new_version()); assert_eq!( checker.should_prompt(), Some((ReleaseChannel::Rc, "1.46.0-rc.1".to_string())) ); // now switch to Alpha release env.set_release_channel(ReleaseChannel::Alpha); env.set_current_version("2.8.0-alpha.0"); env.set_latest_version("2.8.0-alpha.1", ReleaseChannel::Alpha); fetch_and_store_latest_version(&env, &env).await; env.add_hours(UPGRADE_CHECK_INTERVAL + 1); let checker = UpdateChecker::new(env.clone(), env.clone()); assert!(checker.should_check_for_new_version()); assert_eq!( checker.should_prompt(), Some((ReleaseChannel::Alpha, "2.8.0-alpha.1".to_string())) ); // now switch to Beta release env.set_release_channel(ReleaseChannel::Beta); env.set_current_version("2.8.0-beta.0"); env.set_latest_version("2.8.0-beta.1", ReleaseChannel::Beta); fetch_and_store_latest_version(&env, &env).await; env.add_hours(UPGRADE_CHECK_INTERVAL + 1); let checker = UpdateChecker::new(env.clone(), env.clone()); assert!(checker.should_check_for_new_version()); assert_eq!( checker.should_prompt(), Some((ReleaseChannel::Beta, "2.8.0-beta.1".to_string())) ); } #[tokio::test] async fn test_update_checker_current_newer_than_latest() { let env = TestUpdateCheckerEnvironment::new(); let file_content = CheckVersionFile { last_prompt: env .current_time() .sub(chrono::Duration::hours(UPGRADE_CHECK_INTERVAL + 1)), last_checked: env.current_time(), latest_version: "1.26.2".to_string(), current_version: "1.27.0".to_string(), current_release_channel: ReleaseChannel::Stable, } .serialize(); env.write_check_file(&file_content); env.set_current_version("1.27.0"); env.set_latest_version("1.26.2", ReleaseChannel::Stable); let checker = UpdateChecker::new(env.clone(), env); // since currently running version is newer than latest available (eg. CDN // propagation might be delated) we should not prompt assert_eq!(checker.should_prompt(), None); } #[tokio::test] async fn test_should_not_prompt_if_current_cli_version_has_changed() { let env = TestUpdateCheckerEnvironment::new(); let file_content = CheckVersionFile { last_prompt: env .current_time() .sub(chrono::Duration::hours(UPGRADE_CHECK_INTERVAL + 1)), last_checked: env.current_time(), latest_version: "1.26.2".to_string(), current_version: "1.25.0".to_string(), current_release_channel: ReleaseChannel::Stable, } .serialize(); env.write_check_file(&file_content); // simulate an upgrade done to a canary version env.set_current_version("61fbfabe440f1cfffa7b8d17426ffdece4d430d0"); let checker = UpdateChecker::new(env.clone(), env); assert_eq!(checker.should_prompt(), None); } #[test] fn test_get_latest_version_url() { assert_eq!( get_latest_version_url( ReleaseChannel::Canary, "aarch64-apple-darwin", UpgradeCheckKind::Execution ), "https://dl.deno.land/canary-aarch64-apple-darwin-latest.txt" ); assert_eq!( get_latest_version_url( ReleaseChannel::Canary, "aarch64-apple-darwin", UpgradeCheckKind::Lsp ), "https://dl.deno.land/canary-aarch64-apple-darwin-latest.txt?lsp" ); assert_eq!( get_latest_version_url( ReleaseChannel::Canary, "x86_64-pc-windows-msvc", UpgradeCheckKind::Execution ), "https://dl.deno.land/canary-x86_64-pc-windows-msvc-latest.txt" ); assert_eq!( get_latest_version_url( ReleaseChannel::Canary, "x86_64-pc-windows-msvc", UpgradeCheckKind::Lsp ), "https://dl.deno.land/canary-x86_64-pc-windows-msvc-latest.txt?lsp" ); assert_eq!( get_latest_version_url( ReleaseChannel::Stable, "aarch64-apple-darwin", UpgradeCheckKind::Execution ), "https://dl.deno.land/release-latest.txt" ); assert_eq!( get_latest_version_url( ReleaseChannel::Stable, "aarch64-apple-darwin", UpgradeCheckKind::Lsp ), "https://dl.deno.land/release-latest.txt?lsp" ); assert_eq!( get_latest_version_url( ReleaseChannel::Stable, "x86_64-pc-windows-msvc", UpgradeCheckKind::Execution ), "https://dl.deno.land/release-latest.txt" ); assert_eq!( get_latest_version_url( ReleaseChannel::Rc, "x86_64-pc-windows-msvc", UpgradeCheckKind::Lsp ), "https://dl.deno.land/release-rc-latest.txt?lsp" ); assert_eq!( get_latest_version_url( ReleaseChannel::Rc, "aarch64-apple-darwin", UpgradeCheckKind::Execution ), "https://dl.deno.land/release-rc-latest.txt" ); assert_eq!( get_latest_version_url( ReleaseChannel::Rc, "aarch64-apple-darwin", UpgradeCheckKind::Lsp ), "https://dl.deno.land/release-rc-latest.txt?lsp" ); assert_eq!( get_latest_version_url( ReleaseChannel::Rc, "x86_64-pc-windows-msvc", UpgradeCheckKind::Execution ), "https://dl.deno.land/release-rc-latest.txt" ); assert_eq!( get_latest_version_url( ReleaseChannel::Rc, "x86_64-pc-windows-msvc", UpgradeCheckKind::Lsp ), "https://dl.deno.land/release-rc-latest.txt?lsp" ); assert_eq!( get_latest_version_url( ReleaseChannel::Lts, "x86_64-pc-windows-msvc", UpgradeCheckKind::Lsp ), "https://dl.deno.land/release-lts-latest.txt?lsp" ); assert_eq!( get_latest_version_url( ReleaseChannel::Lts, "aarch64-apple-darwin", UpgradeCheckKind::Execution ), "https://dl.deno.land/release-lts-latest.txt" ); assert_eq!( get_latest_version_url( ReleaseChannel::Lts, "aarch64-apple-darwin", UpgradeCheckKind::Lsp ), "https://dl.deno.land/release-lts-latest.txt?lsp" ); assert_eq!( get_latest_version_url( ReleaseChannel::Lts, "x86_64-pc-windows-msvc", UpgradeCheckKind::Execution ), "https://dl.deno.land/release-lts-latest.txt" ); assert_eq!( get_latest_version_url( ReleaseChannel::Lts, "x86_64-pc-windows-msvc", UpgradeCheckKind::Lsp ), "https://dl.deno.land/release-lts-latest.txt?lsp" ); assert_eq!( get_latest_version_url( ReleaseChannel::Alpha, "aarch64-apple-darwin", UpgradeCheckKind::Execution ), "https://dl.deno.land/release-alpha-latest.txt" ); assert_eq!( get_latest_version_url( ReleaseChannel::Alpha, "x86_64-pc-windows-msvc", UpgradeCheckKind::Lsp ), "https://dl.deno.land/release-alpha-latest.txt?lsp" ); assert_eq!( get_latest_version_url( ReleaseChannel::Beta, "aarch64-apple-darwin", UpgradeCheckKind::Execution ), "https://dl.deno.land/release-beta-latest.txt" ); assert_eq!( get_latest_version_url( ReleaseChannel::Beta, "x86_64-pc-windows-msvc", UpgradeCheckKind::Lsp ), "https://dl.deno.land/release-beta-latest.txt?lsp" ); } #[test] fn test_normalize_version_server() { // should strip v for stable assert_eq!( normalize_version_from_server(ReleaseChannel::Stable, "v1.0.0").unwrap(), AvailableVersion { version_or_hash: "1.0.0".to_string(), release_channel: ReleaseChannel::Stable, }, ); // should not replace v after start assert_eq!( normalize_version_from_server( ReleaseChannel::Stable, " v1.0.0-test-v\n\n " ) .unwrap(), AvailableVersion { version_or_hash: "1.0.0-test-v".to_string(), release_channel: ReleaseChannel::Stable, } ); // should not strip v for canary assert_eq!( normalize_version_from_server( ReleaseChannel::Canary, " v1452345asdf \n\n " ) .unwrap(), AvailableVersion { version_or_hash: "v1452345asdf".to_string(), release_channel: ReleaseChannel::Canary, } ); assert_eq!( normalize_version_from_server(ReleaseChannel::Rc, "v1.46.0-rc.0\n\n") .unwrap(), AvailableVersion { version_or_hash: "1.46.0-rc.0".to_string(), release_channel: ReleaseChannel::Rc, }, ); assert_eq!( normalize_version_from_server( ReleaseChannel::Alpha, "v2.8.0-alpha.0\n\n" ) .unwrap(), AvailableVersion { version_or_hash: "2.8.0-alpha.0".to_string(), release_channel: ReleaseChannel::Alpha, }, ); assert_eq!( normalize_version_from_server(ReleaseChannel::Beta, "v2.8.0-beta.1\n\n") .unwrap(), AvailableVersion { version_or_hash: "2.8.0-beta.1".to_string(), release_channel: ReleaseChannel::Beta, }, ); } #[tokio::test] async fn test_upgrades_lsp() { let env = TestUpdateCheckerEnvironment::new(); env.set_current_version("1.0.0"); env.set_latest_version("2.0.0", ReleaseChannel::Stable); // greater { let maybe_info = check_for_upgrades_for_lsp_with_provider(&env) .await .unwrap(); assert_eq!( maybe_info, Some(LspVersionUpgradeInfo { latest_version: "2.0.0".to_string(), is_canary: false, }) ); } // equal { env.set_latest_version("1.0.0", ReleaseChannel::Stable); let maybe_info = check_for_upgrades_for_lsp_with_provider(&env) .await .unwrap(); assert_eq!(maybe_info, None); } // less { env.set_latest_version("0.9.0", ReleaseChannel::Stable); let maybe_info = check_for_upgrades_for_lsp_with_provider(&env) .await .unwrap(); assert_eq!(maybe_info, None); } // canary equal { env.set_current_version("123"); env.set_latest_version("123", ReleaseChannel::Stable); env.set_release_channel(ReleaseChannel::Canary); let maybe_info = check_for_upgrades_for_lsp_with_provider(&env) .await .unwrap(); assert_eq!(maybe_info, None); } // canary different { env.set_latest_version("1234", ReleaseChannel::Stable); let maybe_info = check_for_upgrades_for_lsp_with_provider(&env) .await .unwrap(); assert_eq!( maybe_info, Some(LspVersionUpgradeInfo { latest_version: "1234".to_string(), is_canary: true, }) ); } // rc equal { env.set_release_channel(ReleaseChannel::Rc); env.set_current_version("1.2.3-rc.0"); env.set_latest_version("1.2.3-rc.0", ReleaseChannel::Rc); let maybe_info = check_for_upgrades_for_lsp_with_provider(&env) .await .unwrap(); assert_eq!(maybe_info, None); } // rc different { env.set_latest_version("1.2.3-rc.0", ReleaseChannel::Rc); env.set_latest_version("1.2.3-rc.1", ReleaseChannel::Rc); let maybe_info = check_for_upgrades_for_lsp_with_provider(&env) .await .unwrap(); assert_eq!( maybe_info, Some(LspVersionUpgradeInfo { latest_version: "1.2.3-rc.1".to_string(), is_canary: false, }) ); } // alpha equal { env.set_release_channel(ReleaseChannel::Alpha); env.set_current_version("2.8.0-alpha.0"); env.set_latest_version("2.8.0-alpha.0", ReleaseChannel::Alpha); let maybe_info = check_for_upgrades_for_lsp_with_provider(&env) .await .unwrap(); assert_eq!(maybe_info, None); } // alpha newer available { env.set_latest_version("2.8.0-alpha.1", ReleaseChannel::Alpha); let maybe_info = check_for_upgrades_for_lsp_with_provider(&env) .await .unwrap(); assert_eq!( maybe_info, Some(LspVersionUpgradeInfo { latest_version: "2.8.0-alpha.1".to_string(), is_canary: false, }) ); } // beta equal { env.set_release_channel(ReleaseChannel::Beta); env.set_current_version("2.8.0-beta.0"); env.set_latest_version("2.8.0-beta.0", ReleaseChannel::Beta); let maybe_info = check_for_upgrades_for_lsp_with_provider(&env) .await .unwrap(); assert_eq!(maybe_info, None); } // beta newer available { env.set_latest_version("2.8.0-beta.1", ReleaseChannel::Beta); let maybe_info = check_for_upgrades_for_lsp_with_provider(&env) .await .unwrap(); assert_eq!( maybe_info, Some(LspVersionUpgradeInfo { latest_version: "2.8.0-beta.1".to_string(), is_canary: false, }) ); } } #[test] fn blog_post_links() { let version = Version::parse_standard("1.46.0").unwrap(); assert_eq!( get_minor_version_blog_post_url(&version), "https://deno.com/blog/v1.46" ); let version = Version::parse_standard("2.1.1").unwrap(); assert_eq!( get_minor_version_blog_post_url(&version), "https://deno.com/blog/v2.1" ); let version = Version::parse_standard("2.0.0-rc.0").unwrap(); assert_eq!( get_rc_version_blog_post_url(&version), "https://deno.com/blog/v2.0-rc-0" ); let version = Version::parse_standard("2.0.0-rc.2").unwrap(); assert_eq!( get_rc_version_blog_post_url(&version), "https://deno.com/blog/v2.0-rc-2" ); } #[test] fn test_get_binary_cache_path() { let dl_dir = Path::new("/dl"); let path = get_binary_cache_path(dl_dir, "1.46.0", ReleaseChannel::Stable); assert_eq!( path, dl_dir.join(format!("release/v1.46.0/{}", *ARCHIVE_NAME)) ); let path = get_binary_cache_path(dl_dir, "1.46.0-rc.0", ReleaseChannel::Rc); assert_eq!( path, dl_dir.join(format!("release/v1.46.0-rc.0/{}", *ARCHIVE_NAME)) ); let path = get_binary_cache_path(dl_dir, "1.0.0", ReleaseChannel::Lts); assert_eq!( path, dl_dir.join(format!("release/v1.0.0/{}", *ARCHIVE_NAME)) ); let path = get_binary_cache_path(dl_dir, "abc123def456", ReleaseChannel::Canary); assert_eq!( path, dl_dir.join(format!("canary/abc123def456/{}", *ARCHIVE_NAME)) ); let path = get_binary_cache_path(dl_dir, "2.8.0-alpha.0", ReleaseChannel::Alpha); assert_eq!( path, dl_dir.join(format!("release/v2.8.0-alpha.0/{}", *ARCHIVE_NAME)) ); let path = get_binary_cache_path(dl_dir, "2.8.0-beta.1", ReleaseChannel::Beta); assert_eq!( path, dl_dir.join(format!("release/v2.8.0-beta.1/{}", *ARCHIVE_NAME)) ); } #[test] fn test_try_read_cached_binary() { use sys_traits::FsCreateDirAll; use sys_traits::FsWrite; use sys_traits::impls::InMemorySys; let sys = InMemorySys::default(); // Returns None when file doesn't exist let result = try_read_cached_binary(&sys, Path::new("/dl/release/v1.0.0/deno.zip")); assert!(result.is_none()); // Returns data when file exists let cache_path = Path::new("/dl/release/v1.0.0/deno.zip"); sys.fs_create_dir_all(cache_path.parent().unwrap()).unwrap(); sys.fs_write(cache_path, b"archive-data").unwrap(); let result = try_read_cached_binary(&sys, cache_path); assert_eq!(result.unwrap(), b"archive-data"); } #[test] fn test_store_cached_binary() { use sys_traits::FsRead; use sys_traits::impls::InMemorySys; let sys = InMemorySys::default(); sys.set_seed(Some(42)); let cache_path = Path::new("/dl/release/v1.0.0/deno.zip"); // Should create parent dirs and write the file store_cached_binary(&sys, cache_path, b"archive-data"); let data = sys.fs_read(cache_path).unwrap(); assert_eq!(data.as_ref(), b"archive-data"); } #[test] fn test_store_and_read_cached_binary_roundtrip() { use sys_traits::impls::InMemorySys; let sys = InMemorySys::default(); sys.set_seed(Some(42)); let cache_path = Path::new("/dl/canary/abc123/deno.zip"); // Initially no cache assert!(try_read_cached_binary(&sys, cache_path).is_none()); // Store it let original_data = b"test-archive-contents"; store_cached_binary(&sys, cache_path, original_data); // Read it back let cached = try_read_cached_binary(&sys, cache_path).unwrap(); assert_eq!(cached, original_data); } #[test] fn test_prune_canary_cache_no_dir() { use sys_traits::impls::InMemorySys; let sys = InMemorySys::default(); // Should not panic when directory doesn't exist prune_canary_cache(&sys, Path::new("/dl"), 10); } #[test] fn test_prune_canary_cache_under_limit() { use sys_traits::FsCreateDirAll; use sys_traits::FsMetadata; use sys_traits::impls::InMemorySys; let sys = InMemorySys::default(); let dl_dir = Path::new("/dl"); sys.fs_create_dir_all(dl_dir.join("canary/hash1")).unwrap(); sys.fs_create_dir_all(dl_dir.join("canary/hash2")).unwrap(); prune_canary_cache(&sys, dl_dir, 5); // Both should still exist assert!(sys.fs_exists(dl_dir.join("canary/hash1")).unwrap()); assert!(sys.fs_exists(dl_dir.join("canary/hash2")).unwrap()); } #[test] fn test_prune_canary_cache_over_limit() { use std::time::Duration; use std::time::SystemTime; use sys_traits::FsCreateDirAll; use sys_traits::FsMetadata; use sys_traits::FsSetFileTimes; use sys_traits::impls::InMemorySys; let sys = InMemorySys::default(); let dl_dir = Path::new("/dl"); let base_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1_000_000); // Create 4 canary dirs with different modification times for (i, name) in ["oldest", "old", "new", "newest"].iter().enumerate() { let dir = dl_dir.join(format!("canary/{}", name)); sys.fs_create_dir_all(&dir).unwrap(); let time = base_time + Duration::from_secs(i as u64 * 100); sys.fs_set_file_times(&dir, time, time).unwrap(); } // Prune to keep only 2 prune_canary_cache(&sys, dl_dir, 2); // Newest 2 should remain assert!(sys.fs_exists(dl_dir.join("canary/newest")).unwrap()); assert!(sys.fs_exists(dl_dir.join("canary/new")).unwrap()); // Oldest 2 should be pruned assert!(!sys.fs_exists(dl_dir.join("canary/oldest")).unwrap()); assert!(!sys.fs_exists(dl_dir.join("canary/old")).unwrap()); } #[test] fn test_build_delta_chain() { // Single step assert_eq!( build_delta_chain("2.7.11", "2.7.12"), vec![("2.7.11".to_string(), "2.7.12".to_string())] ); // Multi-step (2 steps) assert_eq!( build_delta_chain("2.7.10", "2.7.12"), vec![ ("2.7.10".to_string(), "2.7.11".to_string()), ("2.7.11".to_string(), "2.7.12".to_string()), ] ); // Multi-step (3 steps, max) assert_eq!( build_delta_chain("2.7.9", "2.7.12"), vec![ ("2.7.9".to_string(), "2.7.10".to_string()), ("2.7.10".to_string(), "2.7.11".to_string()), ("2.7.11".to_string(), "2.7.12".to_string()), ] ); // Too far apart (> 3 steps, same minor) assert!(build_delta_chain("2.7.0", "2.7.5").is_empty()); assert!(build_delta_chain("2.7.8", "2.7.12").is_empty()); // Cross-minor: direct jump to .0 assert_eq!( build_delta_chain("2.7.13", "2.8.0"), vec![("2.7.13".to_string(), "2.8.0".to_string())] ); // Cross-minor: jump to .0 then one patch assert_eq!( build_delta_chain("2.7.13", "2.8.1"), vec![ ("2.7.13".to_string(), "2.8.0".to_string()), ("2.8.0".to_string(), "2.8.1".to_string()), ] ); // Cross-minor: jump to .0 then two patches (3 steps total, max) assert_eq!( build_delta_chain("2.7.13", "2.8.2"), vec![ ("2.7.13".to_string(), "2.8.0".to_string()), ("2.8.0".to_string(), "2.8.1".to_string()), ("2.8.1".to_string(), "2.8.2".to_string()), ] ); // Cross-minor: too many steps (> 3) assert!(build_delta_chain("2.7.13", "2.8.3").is_empty()); // Cross-minor: too many minors apart assert!(build_delta_chain("2.6.10", "2.8.0").is_empty()); // Different major assert!(build_delta_chain("1.46.3", "2.0.0").is_empty()); // Downgrade assert!(build_delta_chain("2.7.12", "2.7.11").is_empty()); // Same version assert!(build_delta_chain("2.7.12", "2.7.12").is_empty()); // Pre-release assert!(build_delta_chain("2.7.11-rc.0", "2.7.12").is_empty()); // Invalid assert!(build_delta_chain("invalid", "2.7.12").is_empty()); } #[test] fn test_get_delta_download_url() { let url = get_delta_download_url("2.7.11", "2.7.12").unwrap(); let url_str = url.to_string(); assert!( url_str.contains("/download/v2.7.12/"), "URL should contain target version: {url_str}" ); assert!( url_str.contains(".from-2.7.11.bsdiff"), "URL should contain source version and bsdiff extension: {url_str}" ); assert!( url_str.contains("deno-"), "URL should contain target name: {url_str}" ); } #[test] fn test_apply_bsdiff_patch_raw() { // Legacy format: bsdiff output written straight to disk. Must keep // working so clients on the current release can still apply the // uncompressed patches already published (e.g. v2.8.0's // `from-2.7.14.bsdiff`). let old_data = b"Hello, World!"; let new_data = b"Hello, Deno!"; let mut patch = Vec::new(); bsdiff::diff(old_data, new_data, &mut patch).unwrap(); assert!( !patch.starts_with(&ZSTD_MAGIC), "raw bsdiff output should not look like a zstd stream" ); let result = apply_bsdiff_patch(old_data, &patch).unwrap(); assert_eq!(result, new_data); } #[test] fn test_apply_bsdiff_patch_zstd_wrapped() { // Current format produced by `tools/bsdiff_helper`: bsdiff output wrapped // in zstd. Must round-trip identically to the raw form. let old_data = b"Hello, World!"; let new_data = b"Hello, Deno!"; let mut raw_patch = Vec::new(); bsdiff::diff(old_data, new_data, &mut raw_patch).unwrap(); let wrapped = zstd::bulk::compress(&raw_patch, 19).unwrap(); assert!( wrapped.starts_with(&ZSTD_MAGIC), "zstd-compressed patch should start with the zstd magic number" ); let result = apply_bsdiff_patch(old_data, &wrapped).unwrap(); assert_eq!(result, new_data); } #[test] fn test_apply_bsdiff_patch_zstd_streamed() { // Regression for the OOM in #34809: a zstd frame written by a streaming // encoder carries no content-size header, so the decoder cannot know the // output size ahead of time. The fix decompresses incrementally instead of // pre-allocating MAX_PATCH_SIZE, so this must still round-trip. use std::io::Write; let old_data = b"Hello, World!"; let new_data = b"Hello, Deno!"; let mut raw_patch = Vec::new(); bsdiff::diff(old_data, new_data, &mut raw_patch).unwrap(); let mut encoder = zstd::stream::Encoder::new(Vec::new(), 19).unwrap(); encoder.write_all(&raw_patch).unwrap(); let wrapped = encoder.finish().unwrap(); assert!(wrapped.starts_with(&ZSTD_MAGIC)); let result = apply_bsdiff_patch(old_data, &wrapped).unwrap(); assert_eq!(result, new_data); } #[test] fn test_bsdiff_patch_is_compressed() { // Regression test for #34312: the raw `bsdiff` crate emits uncompressed // control/diff/extra streams that are routinely larger than the new // binary itself. The release helper (`tools/bsdiff_helper`) wraps the // output in zstd. This test asserts that wrapping is effective — the // compressed patch between two near-identical "binaries" must be much // smaller than either side. // // We use a 256 KiB pseudo-binary that differs by ~64 bytes between // versions. The raw bsdiff output for this is dominated by the // unchanged "extra" stream and balloons close to the binary size; // zstd squashes that runs-of-zeros payload to a few hundred bytes. let mut binary_v1 = Vec::with_capacity(256 * 1024); for i in 0..(256 * 1024) { binary_v1.push((i % 251) as u8); } let mut binary_v2 = binary_v1.clone(); for byte in binary_v2.iter_mut().skip(100_000).take(64) { *byte = byte.wrapping_add(1); } let mut raw_patch = Vec::new(); bsdiff::diff(&binary_v1, &binary_v2, &mut raw_patch).unwrap(); let wrapped = zstd::bulk::compress(&raw_patch, 19).unwrap(); // The patch is what users actually download; it must be substantially // smaller than the binary, otherwise delta upgrade is a pessimization. assert!( wrapped.len() < binary_v2.len() / 4, "compressed patch ({} bytes) should be far smaller than the new \ binary ({} bytes); raw bsdiff was {} bytes", wrapped.len(), binary_v2.len(), raw_patch.len(), ); // And it must still apply. let result = apply_bsdiff_patch(&binary_v1, &wrapped).unwrap(); assert_eq!(result, binary_v2); } #[test] fn test_compute_sha256() { let data = b"test data"; let hash = compute_sha256(data); assert_eq!(hash.len(), 64); assert!(hash.chars().all(|c| c.is_ascii_hexdigit())); // Known SHA-256 of "test data" assert_eq!( hash, "916f0027a575074ce72a331777c3478d6513f786a591bd892da1a577bf2335f9" ); } #[test] fn test_chained_bsdiff_patches() { // Simulate a 3-step upgrade: 2.7.10 -> 2.7.11 -> 2.7.12 -> 2.7.13 // Use realistic-ish binary content that differs slightly per version let base = b"DENO_BINARY_v2.7.10_padding_to_simulate_real_binary_size_"; let mut binary_v10 = base.to_vec(); binary_v10.extend_from_slice(&[0u8; 1024]); let mut binary_v11 = binary_v10.clone(); // Simulate code changes: modify some bytes binary_v11[20..26].copy_from_slice(b"2.7.11"); binary_v11[500..510].copy_from_slice(b"new_code_1"); let mut binary_v12 = binary_v11.clone(); binary_v12[20..26].copy_from_slice(b"2.7.12"); binary_v12[700..710].copy_from_slice(b"new_code_2"); let mut binary_v13 = binary_v12.clone(); binary_v13[20..26].copy_from_slice(b"2.7.13"); binary_v13[900..910].copy_from_slice(b"new_code_3"); // Generate consecutive delta patches let mut patch_10_11 = Vec::new(); bsdiff::diff(&binary_v10, &binary_v11, &mut patch_10_11).unwrap(); let mut patch_11_12 = Vec::new(); bsdiff::diff(&binary_v11, &binary_v12, &mut patch_11_12).unwrap(); let mut patch_12_13 = Vec::new(); bsdiff::diff(&binary_v12, &binary_v13, &mut patch_12_13).unwrap(); // Chain: apply patches sequentially starting from v10 let result_v11 = apply_bsdiff_patch(&binary_v10, &patch_10_11).unwrap(); assert_eq!(result_v11, binary_v11, "v10 -> v11 patch failed"); let result_v12 = apply_bsdiff_patch(&result_v11, &patch_11_12).unwrap(); assert_eq!(result_v12, binary_v12, "v11 -> v12 patch failed"); let result_v13 = apply_bsdiff_patch(&result_v12, &patch_12_13).unwrap(); assert_eq!(result_v13, binary_v13, "v12 -> v13 patch failed"); // Verify patches are reasonable size (bsdiff has overhead for small // inputs, so just check they're not wildly larger) assert!( patch_10_11.len() < binary_v10.len() * 2, "patch should not be much larger than binary: {} vs {}", patch_10_11.len(), binary_v10.len() ); } #[test] fn test_chained_patches_checksum_verification() { // Verify that applying a wrong patch produces a different checksum, // which would be caught by the per-step verification in // try_delta_upgrade let binary_a = b"binary_version_a_content_here_padding"; let binary_b = b"binary_version_b_content_here_padding"; let binary_c = b"binary_version_c_content_here_padding"; let mut patch_ab = Vec::new(); bsdiff::diff(binary_a, binary_b, &mut patch_ab).unwrap(); let mut patch_bc = Vec::new(); bsdiff::diff(binary_b, binary_c, &mut patch_bc).unwrap(); // Correct chain: A -> B -> C let result_b = apply_bsdiff_patch(binary_a, &patch_ab).unwrap(); let hash_b = compute_sha256(&result_b); let expected_hash_b = compute_sha256(binary_b); assert_eq!(hash_b, expected_hash_b, "intermediate hash should match"); let result_c = apply_bsdiff_patch(&result_b, &patch_bc).unwrap(); let hash_c = compute_sha256(&result_c); let expected_hash_c = compute_sha256(binary_c); assert_eq!(hash_c, expected_hash_c, "final hash should match"); // Wrong order: applying B->C patch to A should produce wrong result // (bsdiff will produce SOMETHING but it won't match) let wrong_result = apply_bsdiff_patch(binary_a, &patch_bc).unwrap(); let wrong_hash = compute_sha256(&wrong_result); assert_ne!( wrong_hash, expected_hash_c, "wrong patch order should produce different hash" ); } #[test] fn test_delta_url_uses_target_release() { // Verify that the delta URL downloads from the TARGET version's // release (not the source), since patches live on the new release let url = get_delta_download_url("2.7.10", "2.7.11").unwrap(); assert!( url.to_string().contains("/download/v2.7.11/"), "delta should be fetched from target release: {url}" ); assert!( url.to_string().contains(".from-2.7.10.bsdiff"), "delta filename should reference source version: {url}" ); // For a chain 2.7.10 -> 2.7.12, the intermediate step should // fetch from v2.7.11's release let chain = build_delta_chain("2.7.10", "2.7.12"); assert_eq!(chain.len(), 2); let url_step1 = get_delta_download_url(&chain[0].0, &chain[0].1).unwrap(); assert!( url_step1.to_string().contains("/download/v2.7.11/"), "step 1 should fetch from v2.7.11 release" ); let url_step2 = get_delta_download_url(&chain[1].0, &chain[1].1).unwrap(); assert!( url_step2.to_string().contains("/download/v2.7.12/"), "step 2 should fetch from v2.7.12 release" ); } }