diff --git a/Cargo.lock b/Cargo.lock index 5d14bb1..c810cd4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1669,6 +1669,7 @@ dependencies = [ "cortex-protocol", "cortex-tui-capture", "cortex-tui-components", + "cortex-update", "crossterm", "dirs 6.0.0", "futures", @@ -1828,6 +1829,7 @@ dependencies = [ "dirs 6.0.0", "flate2", "futures", + "getrandom 0.2.17", "hex", "libc", "reqwest 0.13.1", diff --git a/src/cortex-tui/Cargo.toml b/src/cortex-tui/Cargo.toml index 6ad2322..c7dee93 100644 --- a/src/cortex-tui/Cargo.toml +++ b/src/cortex-tui/Cargo.toml @@ -16,6 +16,9 @@ audio = ["dep:rodio"] # Cortex TUI core (the TUI engine) cortex-core = { path = "../cortex-core" } +# Auto-update system +cortex-update = { path = "../cortex-update" } + # Centralized TUI components cortex-tui-components = { path = "../cortex-tui-components" } diff --git a/src/cortex-tui/src/app/mod.rs b/src/cortex-tui/src/app/mod.rs index 7b9c9fe..852cd40 100644 --- a/src/cortex-tui/src/app/mod.rs +++ b/src/cortex-tui/src/app/mod.rs @@ -15,7 +15,7 @@ mod types; pub use approval::{ApprovalState, PendingToolResult}; pub use autocomplete::{AutocompleteItem, AutocompleteState}; pub use session::{ActiveModal, SessionSummary}; -pub use state::AppState; +pub use state::{AppState, UpdateStatus}; pub use streaming::StreamingState; pub use subagent::{ SubagentDisplayStatus, SubagentTaskDisplay, SubagentTodoItem, SubagentTodoStatus, diff --git a/src/cortex-tui/src/app/state.rs b/src/cortex-tui/src/app/state.rs index d79a5f9..9177822 100644 --- a/src/cortex-tui/src/app/state.rs +++ b/src/cortex-tui/src/app/state.rs @@ -7,6 +7,9 @@ use cortex_core::{ style::ThemeColors, widgets::{CortexInput, Message}, }; +// DownloadProgress and UpdateInfo are used in future download tracking feature +#[allow(unused_imports)] +use cortex_update::{DownloadProgress, UpdateInfo}; use uuid::Uuid; use crate::permissions::PermissionMode; @@ -22,6 +25,59 @@ use super::streaming::StreamingState; use super::subagent::SubagentTaskDisplay; use super::types::{AppView, FocusTarget, OperationMode}; +/// Status of the auto-update system +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub enum UpdateStatus { + /// No update check performed yet + #[default] + NotChecked, + /// An update is available + Available { + /// The new version available + version: String, + }, + /// Currently downloading the update + Downloading { + /// The version being downloaded + version: String, + /// Download progress percentage (0-100) + progress: u8, + }, + /// Download complete, restart required + ReadyToRestart { + /// The version that was downloaded + version: String, + }, +} + +impl UpdateStatus { + /// Returns true if an update notification should be shown + pub fn should_show_banner(&self) -> bool { + matches!( + self, + UpdateStatus::Available { .. } + | UpdateStatus::Downloading { .. } + | UpdateStatus::ReadyToRestart { .. } + ) + } + + /// Get the banner text for the current status + pub fn banner_text(&self) -> Option { + match self { + UpdateStatus::Available { version } => { + Some(format!("A new version ({}) is available", version)) + } + UpdateStatus::Downloading { progress, .. } => { + Some(format!("Downloading update... {}%", progress)) + } + UpdateStatus::ReadyToRestart { .. } => { + Some("You must restart to run the latest version".to_string()) + } + _ => None, + } + } +} + /// Main application state pub struct AppState { pub view: AppView, @@ -172,6 +228,10 @@ pub struct AppState { pub user_email: Option, /// Organization name for welcome screen pub org_name: Option, + /// Current update status for the banner notification + pub update_status: UpdateStatus, + /// Cached update info when an update is available + pub update_info: Option, } impl AppState { @@ -272,6 +332,8 @@ impl AppState { user_name: None, user_email: None, org_name: None, + update_status: UpdateStatus::default(), + update_info: None, } } @@ -679,3 +741,39 @@ impl AppState { self.diff_scroll = (self.diff_scroll + delta).max(0); } } + +// ============================================================================ +// APPSTATE METHODS - Update Status +// ============================================================================ + +impl AppState { + /// Set the update status + pub fn set_update_status(&mut self, status: UpdateStatus) { + self.update_status = status; + } + + /// Set update info when an update is available + pub fn set_update_info(&mut self, info: Option) { + self.update_info = info; + } + + /// Check if an update banner should be shown + pub fn should_show_update_banner(&self) -> bool { + self.update_status.should_show_banner() + } + + /// Get the update banner text if one should be shown + pub fn get_update_banner_text(&self) -> Option { + self.update_status.banner_text() + } + + /// Update download progress + pub fn update_download_progress(&mut self, progress: u8) { + if let UpdateStatus::Downloading { version, .. } = &self.update_status { + self.update_status = UpdateStatus::Downloading { + version: version.clone(), + progress, + }; + } + } +} diff --git a/src/cortex-tui/src/runner/app_runner/runner.rs b/src/cortex-tui/src/runner/app_runner/runner.rs index 4ae39b2..e79ac37 100644 --- a/src/cortex-tui/src/runner/app_runner/runner.rs +++ b/src/cortex-tui/src/runner/app_runner/runner.rs @@ -4,7 +4,7 @@ use super::auth_status::AuthStatus; use super::exit_info::{AppExitInfo, ExitReason}; use super::trusted_workspaces::{is_workspace_trusted, mark_workspace_trusted}; -use crate::app::AppState; +use crate::app::{AppState, UpdateStatus}; use crate::bridge::SessionBridge; use crate::providers::ProviderManager; use crate::runner::event_loop::EventLoop; @@ -15,7 +15,9 @@ use anyhow::Result; use cortex_engine::Config; use cortex_login::{CredentialsStoreMode, load_auth, logout_with_fallback}; use cortex_protocol::ConversationId; +use cortex_update::UpdateManager; use std::path::PathBuf; +use std::time::Duration; use tracing; // ============================================================================ @@ -552,6 +554,23 @@ impl AppRunner { let session_history_task = tokio::task::spawn_blocking(|| CortexSession::list_recent(50).ok()); + // 2. Background update check task - check for new versions without blocking startup + let update_check_task = tokio::spawn(async move { + match UpdateManager::new() { + Ok(manager) => match manager.check_update().await { + Ok(info) => info, + Err(e) => { + tracing::debug!("Update check failed: {}", e); + None + } + }, + Err(e) => { + tracing::debug!("Failed to create update manager: {}", e); + None + } + } + }); + // 3. Models prefetch and session validation - spawn in background // We use a channel to receive results and update provider_manager later let models_and_validation_task = { @@ -640,6 +659,23 @@ impl AppRunner { ); } + // Collect update check result (with short timeout to not block startup) + if let Ok(Ok(Some(info))) = + tokio::time::timeout(Duration::from_secs(3), update_check_task).await + { + tracing::info!( + "Update available: {} -> {}", + info.current_version, + info.latest_version + ); + app_state.set_update_status(UpdateStatus::Available { + version: info.latest_version.clone(), + }); + app_state.set_update_info(Some(info)); + } else { + tracing::debug!("Update check did not complete in time or no update available"); + } + // Check validation result (with short timeout - don't block TUI) // We'll handle models update after event loop is created let validation_result = tokio::time::timeout( diff --git a/src/cortex-tui/src/views/minimal_session/rendering.rs b/src/cortex-tui/src/views/minimal_session/rendering.rs index 7381603..892c90c 100644 --- a/src/cortex-tui/src/views/minimal_session/rendering.rs +++ b/src/cortex-tui/src/views/minimal_session/rendering.rs @@ -960,3 +960,65 @@ pub fn render_motd_compact( Paragraph::new(lines).render(text_area, buf); } + +/// Renders an update notification banner above the input box. +/// Shows different states: Available, Downloading (with progress), ReadyToRestart +pub fn render_update_banner( + area: Rect, + buf: &mut Buffer, + colors: &AdaptiveColors, + update_status: &crate::app::UpdateStatus, +) { + use crate::app::UpdateStatus; + + if area.is_empty() || area.height < 1 { + return; + } + + let (icon, text, style) = match update_status { + UpdateStatus::Available { version } => { + let icon = "↑"; + let text = format!(" A new version ({}) is available ", version); + let style = Style::default() + .fg(colors.accent) + .add_modifier(Modifier::BOLD); + (icon, text, style) + } + UpdateStatus::Downloading { + version: _, + progress, + } => { + let icon = "⟳"; + let text = format!(" Downloading update... {}% ", progress); + let style = Style::default().fg(colors.warning); + (icon, text, style) + } + UpdateStatus::ReadyToRestart { version: _ } => { + let icon = "✓"; + let text = " You must restart to run the latest version ".to_string(); + let style = Style::default() + .fg(colors.success) + .add_modifier(Modifier::BOLD); + (icon, text, style) + } + _ => return, // Don't render for other states + }; + + // Calculate banner width + let banner_width = (icon.len() + text.len() + 2) as u16; // +2 for spacing + + // Position at left side of the area with some padding + let x = area.x + 2; + let y = area.y; + + // Ensure we don't overflow + if x + banner_width > area.right() { + return; + } + + // Render icon + buf.set_string(x, y, icon, style); + + // Render text + buf.set_string(x + icon.len() as u16 + 1, y, &text, style); +} diff --git a/src/cortex-tui/src/views/minimal_session/view.rs b/src/cortex-tui/src/views/minimal_session/view.rs index 725dd0e..a03f51f 100644 --- a/src/cortex-tui/src/views/minimal_session/view.rs +++ b/src/cortex-tui/src/views/minimal_session/view.rs @@ -569,6 +569,8 @@ impl<'a> Widget for MinimalSessionView<'a> { self.app_state.autocomplete.visible && self.app_state.autocomplete.has_items(); let autocomplete_height: u16 = if autocomplete_visible { 10 } else { 0 }; let status_height: u16 = if is_task_running { 1 } else { 0 }; + let show_update_banner = self.app_state.should_show_update_banner(); + let update_banner_height: u16 = if show_update_banner { 1 } else { 0 }; let input_height: u16 = 3; let hints_height: u16 = 1; @@ -584,7 +586,12 @@ impl<'a> Widget for MinimalSessionView<'a> { layout.gap(1); // Calculate available height for scrollable content (before input/hints) - let bottom_reserved = status_height + input_height + autocomplete_height + hints_height + 2; // +2 for gaps + let bottom_reserved = status_height + + update_banner_height + + input_height + + autocomplete_height + + hints_height + + 2; // +2 for gaps let available_height = area.height.saturating_sub(1 + bottom_reserved); // 1 for top margin // Render scrollable content area (welcome cards + messages together) @@ -608,7 +615,19 @@ impl<'a> Widget for MinimalSessionView<'a> { next_y += status_height; } - // 6. Input area - follows status (or content if no status) + // 5.5 Update banner (if applicable) - above input + if show_update_banner { + let banner_area = Rect::new(area.x, next_y, area.width, update_banner_height); + super::rendering::render_update_banner( + banner_area, + buf, + &self.colors, + &self.app_state.update_status, + ); + next_y += update_banner_height; + } + + // 6. Input area - follows update banner (or status if no banner) let input_y = next_y; let input_area = Rect::new(area.x, input_y, area.width, input_height); diff --git a/src/cortex-update/Cargo.toml b/src/cortex-update/Cargo.toml index 49d33f4..2395f46 100644 --- a/src/cortex-update/Cargo.toml +++ b/src/cortex-update/Cargo.toml @@ -39,6 +39,9 @@ flate2 = "1.0" zip = "2.2" tar = "0.4" +# Secure random +getrandom = "0.2" + # Self-replacement self-replace = "1.5" diff --git a/src/cortex-update/src/api.rs b/src/cortex-update/src/api.rs index 84eb456..6424f96 100644 --- a/src/cortex-update/src/api.rs +++ b/src/cortex-update/src/api.rs @@ -90,16 +90,31 @@ impl CortexSoftwareClient { impl CortexSoftwareClient { /// Create a new client with the default URL. pub fn new() -> Self { - Self::with_url(SOFTWARE_URL.to_string()) + // Default URL is known to be HTTPS, so unwrap is safe + Self::with_url(SOFTWARE_URL.to_string()).expect("Default SOFTWARE_URL must be HTTPS") } /// Create a new client with a custom URL. - pub fn with_url(base_url: String) -> Self { + /// + /// # Errors + /// + /// Returns `UpdateError::InsecureUrl` if the URL does not use HTTPS + /// (except for localhost/127.0.0.1 which are allowed for development). + pub fn with_url(base_url: String) -> UpdateResult { + // Validate URL uses HTTPS for security (allow http for localhost development only) + let url_lower = base_url.to_lowercase(); + if !url_lower.starts_with("https://") + && !url_lower.starts_with("http://localhost") + && !url_lower.starts_with("http://127.0.0.1") + { + return Err(UpdateError::InsecureUrl { url: base_url }); + } + let client = create_client_builder() .build() .unwrap_or_else(|_| Client::new()); - Self { client, base_url } + Ok(Self { client, base_url }) } /// Get the latest release for a channel. @@ -204,6 +219,17 @@ impl CortexSoftwareClient { where F: FnMut(u64, u64), // (downloaded, total) { + // Validate asset URL uses HTTPS (allow localhost for development) + let url_lower = asset.url.to_lowercase(); + if !url_lower.starts_with("https://") + && !url_lower.starts_with("http://localhost") + && !url_lower.starts_with("http://127.0.0.1") + { + return Err(UpdateError::InsecureUrl { + url: asset.url.clone(), + }); + } + let response = self.client .get(&asset.url) diff --git a/src/cortex-update/src/config.rs b/src/cortex-update/src/config.rs index b70b43a..5cee4d3 100644 --- a/src/cortex-update/src/config.rs +++ b/src/cortex-update/src/config.rs @@ -106,10 +106,17 @@ impl UpdateConfig { .map(|h| h.join(".cortex").join("update.json")) .filter(|p| p.exists()); - if let Some(path) = config_path { - if let Ok(content) = std::fs::read_to_string(&path) { - if let Ok(config) = serde_json::from_str(&content) { - return config; + if let Some(path) = config_path + && let Ok(content) = std::fs::read_to_string(&path) + { + match serde_json::from_str(&content) { + Ok(config) => return config, + Err(e) => { + tracing::warn!( + "Failed to parse update config at {}: {}. Using defaults.", + path.display(), + e + ); } } } diff --git a/src/cortex-update/src/download.rs b/src/cortex-update/src/download.rs index 1ec37e1..5d66de7 100644 --- a/src/cortex-update/src/download.rs +++ b/src/cortex-update/src/download.rs @@ -63,16 +63,13 @@ impl Downloader { /// Uses a randomly-named subdirectory to prevent symlink attacks and /// predictable file name exploits. pub fn new(client: CortexSoftwareClient) -> UpdateResult { - // Use a random suffix to prevent predictable temp directory names - // This mitigates symlink attacks where an attacker pre-creates - // files with expected names - let random_suffix: u64 = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_nanos() as u64) - .unwrap_or(0) - ^ std::process::id() as u64; + // Use cryptographically secure random bytes to prevent predictable temp directory names + // This mitigates symlink attacks where an attacker pre-creates files with expected names + let mut random_bytes = [0u8; 8]; + getrandom::getrandom(&mut random_bytes).map_err(|_| UpdateError::TempDirFailed)?; + let random_suffix = u64::from_ne_bytes(random_bytes); - let temp_dir = std::env::temp_dir().join(format!("cortex-update-{:x}", random_suffix)); + let temp_dir = std::env::temp_dir().join(format!("cortex-update-{:016x}", random_suffix)); // Create with restrictive permissions on Unix #[cfg(unix)] @@ -123,6 +120,7 @@ impl Downloader { } /// Download a signature file. + #[allow(dead_code)] pub async fn download_signature(&self, url: &str, version: &str) -> UpdateResult { let filename = format!("{}.sig", version); let dest = self.temp_dir.join(filename); @@ -134,11 +132,13 @@ impl Downloader { } /// Get the temp directory path. + #[allow(dead_code)] pub fn temp_dir(&self) -> &Path { &self.temp_dir } /// Clean up temp files. + #[allow(dead_code)] pub fn cleanup(&self) -> UpdateResult<()> { if self.temp_dir.exists() { std::fs::remove_dir_all(&self.temp_dir)?; diff --git a/src/cortex-update/src/error.rs b/src/cortex-update/src/error.rs index 1e16d9c..c4d157f 100644 --- a/src/cortex-update/src/error.rs +++ b/src/cortex-update/src/error.rs @@ -90,6 +90,10 @@ pub enum UpdateError { // Cancelled #[error("Update cancelled by user")] Cancelled, + + // Security errors + #[error("Insecure URL rejected (HTTPS required): {url}")] + InsecureUrl { url: String }, } impl UpdateError { diff --git a/src/cortex-update/src/install.rs b/src/cortex-update/src/install.rs index a4772fc..d757da9 100644 --- a/src/cortex-update/src/install.rs +++ b/src/cortex-update/src/install.rs @@ -120,37 +120,114 @@ impl Installer { } /// Extract a tar.gz archive. - async fn extract_tar_gz(&self, archive: &Path, dest: &Path) -> UpdateResult { + /// + /// Uses single-pass extraction with inline validation to prevent TOCTOU race conditions. + async fn extract_tar_gz(&self, archive_path: &Path, dest: &Path) -> UpdateResult { use flate2::read::GzDecoder; use std::fs::File; use tar::Archive; - let file = File::open(archive)?; + let file = File::open(archive_path)?; let gz = GzDecoder::new(file); let mut archive = Archive::new(gz); - archive - .unpack(dest) + // Single-pass: validate and extract each entry to prevent TOCTOU race conditions + for entry_result in archive + .entries() .map_err(|e| UpdateError::ExtractionFailed { message: e.to_string(), + })? + { + let mut entry = entry_result.map_err(|e| UpdateError::ExtractionFailed { + message: e.to_string(), + })?; + + let path = entry.path().map_err(|e| UpdateError::ExtractionFailed { + message: e.to_string(), })?; + // Check for path traversal + if path + .components() + .any(|c| matches!(c, std::path::Component::ParentDir)) + { + return Err(UpdateError::ExtractionFailed { + message: format!("Path traversal detected in archive: {}", path.display()), + }); + } + + // Extract the entry immediately after validation + entry + .unpack_in(dest) + .map_err(|e| UpdateError::ExtractionFailed { + message: e.to_string(), + })?; + } + self.find_binary(dest).await } /// Extract a zip archive. - async fn extract_zip(&self, archive: &Path, dest: &Path) -> UpdateResult { - let file = std::fs::File::open(archive)?; + /// + /// Uses single-pass extraction with inline validation to prevent TOCTOU race conditions. + async fn extract_zip(&self, archive_path: &Path, dest: &Path) -> UpdateResult { + let file = std::fs::File::open(archive_path)?; let mut archive = zip::ZipArchive::new(file).map_err(|e| UpdateError::ExtractionFailed { message: e.to_string(), })?; - archive - .extract(dest) - .map_err(|e| UpdateError::ExtractionFailed { - message: e.to_string(), - })?; + // Single-pass: validate and extract each entry to prevent TOCTOU race conditions + for i in 0..archive.len() { + let mut zip_file = archive + .by_index(i) + .map_err(|e| UpdateError::ExtractionFailed { + message: e.to_string(), + })?; + + let path = std::path::Path::new(zip_file.name()); + + // Check for path traversal + if path + .components() + .any(|c| matches!(c, std::path::Component::ParentDir)) + { + return Err(UpdateError::ExtractionFailed { + message: format!("Path traversal detected in archive: {}", zip_file.name()), + }); + } + + // Determine the output path + let outpath = dest.join(zip_file.name()); + + // Extract the entry immediately after validation + if zip_file.is_dir() { + std::fs::create_dir_all(&outpath)?; + } else { + // Ensure parent directory exists + if let Some(parent) = outpath.parent() { + std::fs::create_dir_all(parent)?; + } + let mut outfile = + std::fs::File::create(&outpath).map_err(|e| UpdateError::ExtractionFailed { + message: format!("Failed to create file {}: {}", outpath.display(), e), + })?; + std::io::copy(&mut zip_file, &mut outfile).map_err(|e| { + UpdateError::ExtractionFailed { + message: format!("Failed to extract {}: {}", outpath.display(), e), + } + })?; + + // Set file permissions on Unix + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + if let Some(mode) = zip_file.unix_mode() { + std::fs::set_permissions(&outpath, std::fs::Permissions::from_mode(mode))?; + } + } + } + } self.find_binary(dest).await } @@ -244,6 +321,11 @@ impl Installer { const MOVEFILE_REPLACE_EXISTING: u32 = 0x1; const MOVEFILE_DELAY_UNTIL_REBOOT: u32 = 0x4; + // SAFETY: MoveFileExW is a Windows API function that schedules a file move/rename + // operation to occur at the next system restart. We pass valid null-terminated + // wide strings for source and destination paths. The MOVEFILE_DELAY_UNTIL_REBOOT + // flag ensures this is a deferred operation. The return value of 0 indicates failure, + // in which case we retrieve the error via last_os_error(). let result = unsafe { windows_sys::Win32::Storage::FileSystem::MoveFileExW( old_path.as_ptr(), @@ -269,6 +351,7 @@ impl Installer { } /// Check if we have permission to write to the installation directory. +#[allow(dead_code)] pub fn check_write_permission() -> UpdateResult<()> { let exe_path = std::env::current_exe()?; let parent = exe_path diff --git a/src/cortex-update/src/lib.rs b/src/cortex-update/src/lib.rs index f3cdc1b..ab99a06 100644 --- a/src/cortex-update/src/lib.rs +++ b/src/cortex-update/src/lib.rs @@ -1,4 +1,3 @@ -#![allow(warnings, clippy::all)] //! Cortex Update - Auto-update system for Cortex CLI //! //! Provides automatic update checking and installation via: @@ -36,6 +35,7 @@ mod version; pub use api::{CortexSoftwareClient, ReleaseAsset, ReleaseInfo}; pub use config::{ReleaseChannel, UpdateConfig, UpdateMode}; +pub use download::DownloadProgress; pub use error::{UpdateError, UpdateResult}; pub use install::DownloadedUpdate; pub use manager::{UpdateInfo, UpdateManager, UpdateOutcome}; diff --git a/src/cortex-update/src/manager.rs b/src/cortex-update/src/manager.rs index 16b1525..9b7f2a0 100644 --- a/src/cortex-update/src/manager.rs +++ b/src/cortex-update/src/manager.rs @@ -1,7 +1,5 @@ //! Update manager - main API for update operations. -use std::path::PathBuf; - use crate::CURRENT_VERSION; use crate::api::{CortexSoftwareClient, ReleaseAsset, ReleaseInfo}; use crate::config::{ReleaseChannel, UpdateConfig}; @@ -61,7 +59,7 @@ impl UpdateManager { /// Create with a specific config. pub fn with_config(config: UpdateConfig) -> UpdateResult { let client = if let Some(url) = &config.custom_url { - CortexSoftwareClient::with_url(url.clone()) + CortexSoftwareClient::with_url(url.clone())? } else { CortexSoftwareClient::new() }; @@ -89,10 +87,12 @@ impl UpdateManager { pub async fn check_update(&self) -> UpdateResult> { // Try to use cache first if let Some(cache) = VersionCache::load() { - if cache.is_valid(&self.config) { - if cache.has_update() && !self.config.is_version_skipped(&cache.latest.version) { - return Ok(Some(self.build_update_info(&cache.latest)?)); - } + if cache.is_valid(&self.config) + && cache.has_update() + && !self.config.is_version_skipped(&cache.latest.version) + { + return Ok(Some(self.build_update_info(&cache.latest)?)); + } else if cache.is_valid(&self.config) { return Ok(None); } } diff --git a/src/cortex-update/src/method.rs b/src/cortex-update/src/method.rs index 6a81c2b..2bdae59 100644 --- a/src/cortex-update/src/method.rs +++ b/src/cortex-update/src/method.rs @@ -1,6 +1,8 @@ //! Installation method detection. use serde::{Deserialize, Serialize}; +use std::path::Path; +#[cfg(test)] use std::path::PathBuf; /// Installation method for Cortex CLI. @@ -27,10 +29,10 @@ impl InstallMethod { /// Detect the installation method based on environment and paths. pub fn detect() -> Self { // 1. Check executable path first - if let Ok(exe_path) = std::env::current_exe() { - if let Some(method) = Self::detect_from_path(&exe_path) { - return method; - } + if let Ok(exe_path) = std::env::current_exe() + && let Some(method) = Self::detect_from_path(&exe_path) + { + return method; } // 2. Platform-specific defaults @@ -40,7 +42,7 @@ impl InstallMethod { if which_exists("winget") { return Self::WinGet; } - return Self::PowerShellScript; + Self::PowerShellScript } #[cfg(not(windows))] @@ -49,12 +51,12 @@ impl InstallMethod { if which_exists("brew") { return Self::Homebrew; } - return Self::CurlScript; + Self::CurlScript } } /// Detect from executable path. - fn detect_from_path(path: &PathBuf) -> Option { + fn detect_from_path(path: &Path) -> Option { let path_str = path.to_string_lossy().to_lowercase(); // Homebrew paths (macOS and Linux) diff --git a/src/cortex-update/src/verify.rs b/src/cortex-update/src/verify.rs index b338c82..3fbac30 100644 --- a/src/cortex-update/src/verify.rs +++ b/src/cortex-update/src/verify.rs @@ -34,6 +34,7 @@ pub async fn verify_sha256(path: &Path, expected: &str) -> UpdateResult<()> { } /// Verify SHA256 checksum synchronously (for smaller files). +#[allow(dead_code)] pub fn verify_sha256_sync(path: &Path, expected: &str) -> UpdateResult<()> { let content = std::fs::read(path)?; let result = Sha256::digest(&content); @@ -49,6 +50,7 @@ pub fn verify_sha256_sync(path: &Path, expected: &str) -> UpdateResult<()> { } /// Calculate SHA256 hash of a file. +#[allow(dead_code)] pub async fn calculate_sha256(path: &Path) -> UpdateResult { let mut file = tokio::fs::File::open(path).await?; let mut hasher = Sha256::new(); diff --git a/src/cortex-update/src/version.rs b/src/cortex-update/src/version.rs index c8f81d8..d174593 100644 --- a/src/cortex-update/src/version.rs +++ b/src/cortex-update/src/version.rs @@ -125,6 +125,7 @@ fn parse_version(version: &str) -> (u32, u32, u32, String) { } /// Check if a version meets the minimum requirement. +#[allow(dead_code)] pub fn meets_minimum(current: &str, minimum: &str) -> bool { compare_versions(current, minimum) != VersionComparison::Older }