diff --git a/Cargo.lock b/Cargo.lock index 8ac2200..a0602d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -295,7 +295,7 @@ dependencies = [ [[package]] name = "taskmgr-rs" -version = "0.2.5" +version = "0.2.6" dependencies = [ "crc32fast", "crossbeam-channel", diff --git a/Cargo.toml b/Cargo.toml index 832b7d6..476994a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ # 这里定义包名、版本、发布构建策略,以及 Win32 API 绑定所需的功能开关。 [package] name = "taskmgr-rs" -version = "0.2.5" +version = "0.2.6" authors = ["taskmgr-rs contributors"] description = "A classic Windows Task Manager built in Rust with native Win32 APIs." license = "MIT" diff --git a/README.md b/README.md index aba2498..4738347 100644 --- a/README.md +++ b/README.md @@ -79,9 +79,10 @@ Pull requests and pushes to `main` run formatting, all-target checks, strict Cli x86_64 release build on GitHub's Windows Server 2025 / Visual Studio 2026 runner. Separate jobs compile i686 and ARM64; i686 tests also run under WOW64. -To publish a version, first update `Cargo.toml` and `Cargo.lock`, merge the change, then push the -matching tag such as `v0.2.5`. The Release workflow rejects a tag that does not exactly match the -Cargo package version. It builds all three architectures in parallel, verifies the PE machine and +To publish a version, update `Cargo.toml` and `Cargo.lock` together and merge the change to `main`. +The Release workflow reads the package version and automatically creates the matching tag (for +example `v0.2.6`) when it does not exist. It then builds all three architectures in parallel, +verifies the PE machine and Windows version resources, writes `SHA256SUMS.txt`, creates GitHub build-provenance attestations, and publishes only after the uploaded sizes and digests match. A publishing failure leaves only a draft; a build failure creates no release, so neither path exposes a partial release. diff --git a/README.zh-CN.md b/README.zh-CN.md index 79b3a55..c1cd482 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -79,8 +79,8 @@ Pull Request 和推送到 `main` 的提交会在 GitHub 的 Windows Server 2025 runner 上执行格式检查、全目标 check、严格 Clippy、完整测试和 x86_64 release 构建。独立任务 同时验证 i686 与 ARM64,i686 测试会在 WOW64 下实际执行。 -发布版本时,先同步修改 `Cargo.toml` 与 `Cargo.lock` 并合入主线,再推送与版本完全一致的 tag, -例如 `v0.2.5`。Release 工作流会拒绝与 Cargo 包版本不一致的 tag,并行构建三种架构,核对 PE +发布版本时,只需同步修改 `Cargo.toml` 与 `Cargo.lock` 并合入 `main`。Release 工作流会读取包 +版本;如果对应 tag(例如 `v0.2.6`)尚不存在,就自动创建 tag,并行构建三种架构,核对 PE 机器类型和 Windows 版本资源,生成 `SHA256SUMS.txt` 与 GitHub 构建来源证明。只有 GitHub 服务器上的资产大小和摘要都与本地一致时才公开 Release。发布阶段失败时只保留草稿,构建 阶段失败时不会创建 Release,因此都不会暴露不完整发布。 diff --git a/src/config/options.rs b/src/config/options.rs index a0a9f05..871d1af 100644 --- a/src/config/options.rs +++ b/src/config/options.rs @@ -15,7 +15,9 @@ use std::mem::{size_of, zeroed}; // 数据合法性校验以及注册表的读写边界。 use std::ptr::null_mut; -use windows_sys::Win32::Foundation::{ERROR_REVISION_MISMATCH, ERROR_SUCCESS, RECT}; +use windows_sys::Win32::Foundation::{ + ERROR_FILE_NOT_FOUND, ERROR_INVALID_DATA, ERROR_REVISION_MISMATCH, ERROR_SUCCESS, RECT, +}; use windows_sys::Win32::Graphics::Gdi::{MONITOR_DEFAULTTONULL, MonitorFromRect}; use windows_sys::Win32::System::Registry::{ HKEY, HKEY_CURRENT_USER, KEY_READ, KEY_WRITE, REG_BINARY, REG_OPTION_NON_VOLATILE, RegCloseKey, @@ -31,8 +33,12 @@ use crate::app::page_registry::PageId; use crate::infrastructure::native::{record_win32_error, to_wide_null}; use crate::ui::resource_ids::NUM_COLUMN; -const TASKMAN_KEY: &str = "Software\\Microsoft\\Windows NT\\CurrentVersion\\TaskManager"; -const OPTIONS_KEY: &str = "Preferences"; +const OPTIONS_KEY: &str = "Software\\taskmgr-rs\\TaskManager"; +const OPTIONS_VALUE: &str = "OptionsV1"; +const LEGACY_TASKMAN_KEY: &str = "Software\\Microsoft\\Windows NT\\CurrentVersion\\TaskManager"; +const LEGACY_OPTIONS_VALUE: &str = "Preferences"; +const OPTIONS_STORAGE_MAGIC: [u8; 8] = *b"TMGRRS01"; +const OPTIONS_STORAGE_VERSION: u32 = 1; const OPTIONS_SCHEMA_VERSION: i32 = 2; const SCHEMA_0_NETWORK_PAGE: i32 = 3; const SCHEMA_0_USERS_PAGE: i32 = 4; @@ -144,6 +150,48 @@ pub struct Options { pub unused2: i32, } +#[repr(C)] +#[derive(Clone, Copy)] +struct StoredOptions { + magic: [u8; 8], + storage_version: u32, + payload_size: u32, + options: Options, +} + +impl StoredOptions { + fn new(options: Options) -> Self { + Self { + magic: OPTIONS_STORAGE_MAGIC, + storage_version: OPTIONS_STORAGE_VERSION, + payload_size: size_of::() as u32, + options, + } + } + + fn into_options(self) -> Result { + if self.magic != OPTIONS_STORAGE_MAGIC || self.payload_size != size_of::() as u32 { + return Err(ERROR_INVALID_DATA); + } + if self.storage_version != OPTIONS_STORAGE_VERSION { + return Err(ERROR_REVISION_MISMATCH); + } + Ok(self.options) + } +} + +/// Marker for fixed-layout registry values whose every bit pattern is a valid Rust value. +/// +/// # Safety +/// +/// Implementors must be `Copy`, have a stable layout, contain no references, and permit every +/// possible byte pattern. This lets the registry reader initialize the value as zeroed storage +/// before Windows fills every byte. +unsafe trait RegistryPod: Copy {} + +unsafe impl RegistryPod for Options {} +unsafe impl RegistryPod for StoredOptions {} + impl Default for Options { fn default() -> Self { // 默认值尽量贴近经典任务管理器的首次启动体验。 @@ -200,109 +248,81 @@ impl Options { } pub fn load(&mut self, min_width: i32, min_height: i32) -> bool { - // 读取失败或数据不合法时,统一回退到默认配置,避免坏配置把程序带崩。 + // taskmgr-rs owns a versioned application-specific value. The Microsoft Task Manager key + // is read only for a one-time migration of values that carry taskmgr-rs' exact schema + // marker; it is never written or deleted. if modifiers_force_defaults() { self.set_default_values(min_width, min_height); return false; } - // 安全性: registry buffers point to live local variables for the duration of each call; - // loaded binary data is size/type checked before being used. - unsafe { - let key_name = to_wide_null(TASKMAN_KEY); - let value_name = to_wide_null(OPTIONS_KEY); - let mut key: HKEY = null_mut(); - if RegOpenKeyExW(HKEY_CURRENT_USER, key_name.as_ptr(), 0, KEY_READ, &mut key) - != ERROR_SUCCESS - { - self.set_default_values(min_width, min_height); - return false; + match read_registry_binary::(OPTIONS_KEY, OPTIONS_VALUE) { + Ok(Some(stored)) => { + let loaded = match stored.into_options() { + Ok(loaded) => loaded, + Err(error) => { + record_win32_error("taskmgr-rs options envelope", error); + self.set_default_values(min_width, min_height); + return false; + } + }; + return self.apply_loaded_options(loaded, min_width, min_height, false); } - - let mut loaded = zeroed::(); - let mut value_type = 0u32; - let mut value_size = size_of::() as u32; - let status = RegQueryValueExW( - key, - value_name.as_ptr(), - null_mut(), - &mut value_type, - &mut loaded as *mut Options as *mut u8, - &mut value_size, - ); - RegCloseKey(key); - - if status != ERROR_SUCCESS - || value_type != REG_BINARY - || value_size != size_of::() as u32 - { + Ok(None) => {} + Err(error) => { + record_win32_error("reading taskmgr-rs options", error); self.set_default_values(min_width, min_height); return false; } + } - let migrated = match loaded.migrate_schema() { - Ok(migrated) => migrated, - Err(()) => { - record_win32_error("unsupported options schema", ERROR_REVISION_MISMATCH); - self.set_default_values(min_width, min_height); - return false; - } - }; - let loaded_was_valid = loaded.is_valid(min_width, min_height); - if !loaded_was_valid { - loaded.normalize(min_width, min_height); + match read_registry_binary::(LEGACY_TASKMAN_KEY, LEGACY_OPTIONS_VALUE) { + Ok(Some(legacy)) if legacy_options_is_taskmgr_rs(&legacy) => { + self.apply_loaded_options(legacy, min_width, min_height, true) } - *self = loaded; - if (migrated || !loaded_was_valid) - && let Err(error) = self.save() - { - record_win32_error("normalized options persistence", error); + Ok(Some(_)) | Ok(None) => { + self.set_default_values(min_width, min_height); + false + } + Err(error) => { + record_win32_error("reading legacy taskmgr-rs options", error); + self.set_default_values(min_width, min_height); + false } - loaded_was_valid } } - pub fn save(&self) -> Result<(), u32> { - // 整个结构体按历史格式整体写入注册表,保持与原版偏好布局兼容。 - // 安全性: registry handles are opened and closed in this block; the value buffer points - // to `self` and is written as the historical binary Options format. - unsafe { - let key_name = to_wide_null(TASKMAN_KEY); - let value_name = to_wide_null(OPTIONS_KEY); - let mut key: HKEY = null_mut(); - let mut disposition = 0u32; - - let create_status = RegCreateKeyExW( - HKEY_CURRENT_USER, - key_name.as_ptr(), - 0, - null_mut(), - REG_OPTION_NON_VOLATILE, - KEY_WRITE, - null_mut(), - &mut key, - &mut disposition, - ); - if create_status != ERROR_SUCCESS { - return Err(create_status); - } - - let set_status = RegSetValueExW( - key, - value_name.as_ptr(), - 0, - REG_BINARY, - self as *const Options as *const u8, - size_of::() as u32, - ); - RegCloseKey(key); - - if set_status == ERROR_SUCCESS { - Ok(()) - } else { - Err(set_status) + fn apply_loaded_options( + &mut self, + mut loaded: Options, + min_width: i32, + min_height: i32, + persist_migration: bool, + ) -> bool { + let migrated = match loaded.migrate_schema() { + Ok(migrated) => migrated, + Err(()) => { + record_win32_error("unsupported options schema", ERROR_REVISION_MISMATCH); + self.set_default_values(min_width, min_height); + return false; } + }; + let loaded_was_valid = loaded.is_valid(min_width, min_height); + if !loaded_was_valid { + loaded.normalize(min_width, min_height); } + *self = loaded; + if (persist_migration || migrated || !loaded_was_valid) + && let Err(error) = self.save() + { + record_win32_error("normalized options persistence", error); + } + loaded_was_valid + } + + pub fn save(&self) -> Result<(), u32> { + let stored = StoredOptions::new(*self); + write_registry_binary(OPTIONS_KEY, OPTIONS_VALUE, &stored) } pub fn minimize_on_use(&self) -> bool { @@ -467,6 +487,135 @@ impl Options { } } +fn legacy_options_is_taskmgr_rs(options: &Options) -> bool { + options.cb_size == size_of::() as u32 + && options.unused == OPTIONS_SCHEMA_VERSION + && options.unused2 == 0 + && options.current_page >= -1 + && options.current_page < PageId::COUNT as i32 + && is_valid_view_mode(options.view_mode) + && is_valid_cpu_history_mode(options.cpu_history_mode) + && is_valid_update_speed(options.update_speed) + && options.flags & !ALL_VALID_FLAGS == 0 + && process_columns_are_valid(&options.active_process_columns, &options.column_widths) +} + +fn read_registry_binary( + key_path: &str, + value_name: &str, +) -> Result, u32> { + unsafe { + let key_path = to_wide_null(key_path); + let value_name = to_wide_null(value_name); + let mut key: HKEY = null_mut(); + let open_status = + RegOpenKeyExW(HKEY_CURRENT_USER, key_path.as_ptr(), 0, KEY_READ, &mut key); + if open_status == ERROR_FILE_NOT_FOUND { + return Ok(None); + } + if open_status != ERROR_SUCCESS { + return Err(open_status); + } + + let mut value_type = 0u32; + let mut value_size = 0u32; + let size_status = RegQueryValueExW( + key, + value_name.as_ptr(), + null_mut(), + &mut value_type, + null_mut(), + &mut value_size, + ); + if size_status == ERROR_FILE_NOT_FOUND { + let close_status = RegCloseKey(key); + return if close_status == ERROR_SUCCESS { + Ok(None) + } else { + Err(close_status) + }; + } + if size_status != ERROR_SUCCESS { + RegCloseKey(key); + return Err(size_status); + } + if value_type != REG_BINARY || value_size != size_of::() as u32 { + RegCloseKey(key); + return Err(ERROR_INVALID_DATA); + } + + // SAFETY: RegistryPod requires every bit pattern to be valid. The exact-size check above + // and the second query's unchanged byte count guarantee that Windows initializes all bytes. + let mut value = zeroed::(); + let mut actual_size = value_size; + let read_status = RegQueryValueExW( + key, + value_name.as_ptr(), + null_mut(), + &mut value_type, + (&mut value as *mut T).cast::(), + &mut actual_size, + ); + let close_status = RegCloseKey(key); + if read_status != ERROR_SUCCESS { + return Err(read_status); + } + if close_status != ERROR_SUCCESS { + return Err(close_status); + } + if value_type != REG_BINARY || actual_size != size_of::() as u32 { + return Err(ERROR_INVALID_DATA); + } + Ok(Some(value)) + } +} + +fn write_registry_binary( + key_path: &str, + value_name: &str, + value: &T, +) -> Result<(), u32> { + unsafe { + let key_path = to_wide_null(key_path); + let value_name = to_wide_null(value_name); + let mut key: HKEY = null_mut(); + let mut disposition = 0u32; + let create_status = RegCreateKeyExW( + HKEY_CURRENT_USER, + key_path.as_ptr(), + 0, + null_mut(), + REG_OPTION_NON_VOLATILE, + KEY_WRITE, + null_mut(), + &mut key, + &mut disposition, + ); + if create_status != ERROR_SUCCESS { + return Err(create_status); + } + + // SAFETY: RegistryPod has a fixed initialized representation and contains no references. + let bytes = std::slice::from_raw_parts((value as *const T).cast::(), size_of::()); + let set_status = RegSetValueExW( + key, + value_name.as_ptr(), + 0, + REG_BINARY, + bytes.as_ptr(), + bytes.len() as u32, + ); + let close_status = RegCloseKey(key); + if set_status != ERROR_SUCCESS { + Err(set_status) + } else if close_status != ERROR_SUCCESS { + Err(close_status) + } else { + Ok(()) + } + } +} + fn modifiers_force_defaults() -> bool { // 安全性: `GetKeyState` only reads current keyboard state for virtual-key codes. unsafe { @@ -628,13 +777,64 @@ fn screen_reader_enabled() -> bool { #[cfg(test)] mod tests { use super::{ - ColumnId, NUM_COLUMN, OPTIONS_SCHEMA_VERSION, Options, SCHEMA_0_NETWORK_PAGE, + ColumnId, ERROR_INVALID_DATA, ERROR_REVISION_MISMATCH, LEGACY_OPTIONS_VALUE, + LEGACY_TASKMAN_KEY, NUM_COLUMN, OPTIONS_KEY, OPTIONS_SCHEMA_VERSION, OPTIONS_STORAGE_MAGIC, + OPTIONS_STORAGE_VERSION, OPTIONS_VALUE, Options, SCHEMA_0_NETWORK_PAGE, SCHEMA_0_USERS_PAGE, SCHEMA_1_GPU_PAGE, SCHEMA_1_NETWORK_PAGE, SCHEMA_1_USERS_PAGE, - UpdateSpeed, normalize_process_columns, process_columns_are_valid, - update_speed_timer_interval, window_rect_dimensions_are_valid, window_rect_is_valid, + StoredOptions, UpdateSpeed, legacy_options_is_taskmgr_rs, normalize_process_columns, + process_columns_are_valid, update_speed_timer_interval, window_rect_dimensions_are_valid, + window_rect_is_valid, }; use windows_sys::Win32::Foundation::RECT; + #[test] + fn stored_options_envelope_round_trips_and_rejects_foreign_data() { + let options = Options::default(); + assert_eq!( + StoredOptions::new(options).into_options().unwrap().unused, + options.unused + ); + + let mut foreign = StoredOptions::new(options); + foreign.magic = *b"NATIVE00"; + assert_eq!(foreign.into_options().err(), Some(ERROR_INVALID_DATA)); + + let mut future = StoredOptions::new(options); + future.storage_version = OPTIONS_STORAGE_VERSION + 1; + assert_eq!(future.into_options().err(), Some(ERROR_REVISION_MISMATCH)); + + let mut wrong_size = StoredOptions::new(options); + wrong_size.payload_size = 0; + assert_eq!(wrong_size.into_options().err(), Some(ERROR_INVALID_DATA)); + assert_eq!(OPTIONS_STORAGE_MAGIC, *b"TMGRRS01"); + } + + #[test] + fn legacy_migration_requires_the_exact_taskmgr_rs_schema_marker() { + let current = Options::default(); + assert!(legacy_options_is_taskmgr_rs(¤t)); + + for schema in [0, 1, OPTIONS_SCHEMA_VERSION + 1] { + let candidate = Options { + unused: schema, + ..current + }; + assert!(!legacy_options_is_taskmgr_rs(&candidate)); + } + + let wrong_size = Options { + cb_size: 0, + ..current + }; + assert!(!legacy_options_is_taskmgr_rs(&wrong_size)); + } + + #[test] + fn application_options_namespace_is_distinct_from_windows_task_manager() { + assert_ne!(OPTIONS_KEY, LEGACY_TASKMAN_KEY); + assert_ne!(OPTIONS_VALUE, LEGACY_OPTIONS_VALUE); + } + #[test] fn process_columns_reject_missing_primary_and_duplicates() { let mut columns = [-1; NUM_COLUMN + 1]; diff --git a/src/pages/network.rs b/src/pages/network.rs index 1491d74..8642580 100644 --- a/src/pages/network.rs +++ b/src/pages/network.rs @@ -79,7 +79,8 @@ struct RawAdapterEntry { key: AdapterIdentity, name: String, state: String, - link_speed_bps: u64, + transmit_link_speed_bps: u64, + receive_link_speed_bps: u64, bytes_sent: u64, bytes_received: u64, } @@ -859,22 +860,32 @@ impl NetworkPageState { .as_ref() .map(|state| (state.current_sent, state.current_received)), ); - // A zero curve point marks an unavailable interval after first sight or counter - // reset; the textual value remains "-" so it is not presented as measured idle. - let total_delta = counter_delta.map_or(0, |delta| delta.2); - let sent_util = counter_delta.map_or(0, |delta| { - utilization_percent_for_history(delta.0, raw_adapter.link_speed_bps, elapsed_secs) - }); - let received_util = counter_delta.map_or(0, |delta| { - utilization_percent_for_history(delta.1, raw_adapter.link_speed_bps, elapsed_secs) - }); - let total_util = counter_delta.map_or(0, |delta| { - utilization_percent_for_history(delta.2, raw_adapter.link_speed_bps, elapsed_secs) - }); + // Each direction owns an independent full-duplex capacity. "Total" is the + // busiest direction's percentage, which remains meaningful for asymmetric links and + // cannot fabricate a 200% value by summing independent capacities. + let (sent_ratio, received_ratio, total_ratio) = + counter_delta.map_or((None, None, None), |delta| { + directional_utilization_ratios( + delta.0, + delta.1, + raw_adapter.transmit_link_speed_bps, + raw_adapter.receive_link_speed_bps, + elapsed_secs, + ) + }); - push_history(&mut sent_history, sent_util); - push_history(&mut received_history, received_util); - push_history(&mut total_history, total_util); + push_history( + &mut sent_history, + utilization_percent_for_history(sent_ratio), + ); + push_history( + &mut received_history, + utilization_percent_for_history(received_ratio), + ); + push_history( + &mut total_history, + utilization_percent_for_history(total_ratio), + ); let bytes_total = raw_adapter .bytes_sent @@ -884,12 +895,11 @@ impl NetworkPageState { key: raw_adapter.key, name: raw_adapter.name, state: raw_adapter.state, - link_speed: format_link_speed(raw_adapter.link_speed_bps), - utilization: counter_delta - .map(|_| { - utilization_text(total_delta, raw_adapter.link_speed_bps, elapsed_secs) - }) - .unwrap_or_else(|| "-".to_string()), + link_speed: format_link_speeds( + raw_adapter.transmit_link_speed_bps, + raw_adapter.receive_link_speed_bps, + ), + utilization: utilization_text(total_ratio), bytes_sent: format_counter(raw_adapter.bytes_sent), bytes_received: format_counter(raw_adapter.bytes_received), bytes_total: bytes_total @@ -964,7 +974,8 @@ impl NetworkPageState { key, name, state: adapter_state_text(row.OperStatus), - link_speed_bps: row.ReceiveLinkSpeed.max(row.TransmitLinkSpeed), + transmit_link_speed_bps: row.TransmitLinkSpeed, + receive_link_speed_bps: row.ReceiveLinkSpeed, bytes_sent: row.OutOctets, bytes_received: row.InOctets, }); @@ -1378,7 +1389,7 @@ fn collapse_raw_adapters(adapters: Vec) -> Vec fn raw_adapter_rank(adapter: &RawAdapterEntry) -> (u8, u8, i64) { ( - u8::from(adapter.link_speed_bps != 0), + u8::from(adapter.transmit_link_speed_bps != 0 || adapter.receive_link_speed_bps != 0), u8::from(!adapter.state.eq_ignore_ascii_case("disconnected")), -(adapter.name.len() as i64), ) @@ -1423,22 +1434,38 @@ fn utilization_ratio_percent( link_speed_bps: u64, elapsed_secs: f64, ) -> Option { - if bytes_per_interval == 0 || link_speed_bps == 0 || elapsed_secs <= 0.0 { + if link_speed_bps == 0 || !elapsed_secs.is_finite() || elapsed_secs <= 0.0 { return None; } + if bytes_per_interval == 0 { + return Some(0.0); + } let bits_per_second = (bytes_per_interval as f64 * 8.0) / elapsed_secs; - Some(((bits_per_second * 100.0) / link_speed_bps as f64).clamp(0.0, 100.0)) + let percent = (bits_per_second * 100.0) / link_speed_bps as f64; + percent.is_finite().then(|| percent.clamp(0.0, 100.0)) } -fn utilization_percent_for_history( - bytes_per_interval: u64, - link_speed_bps: u64, +fn directional_utilization_ratios( + sent_bytes: u64, + received_bytes: u64, + transmit_link_speed_bps: u64, + receive_link_speed_bps: u64, elapsed_secs: f64, -) -> u8 { - let Some(ratio_percent) = - utilization_ratio_percent(bytes_per_interval, link_speed_bps, elapsed_secs) - else { +) -> (Option, Option, Option) { + let sent = utilization_ratio_percent(sent_bytes, transmit_link_speed_bps, elapsed_secs); + let received = utilization_ratio_percent(received_bytes, receive_link_speed_bps, elapsed_secs); + let total = match (sent, received) { + (Some(sent), Some(received)) => Some(sent.max(received)), + (Some(sent), None) => Some(sent), + (None, Some(received)) => Some(received), + (None, None) => None, + }; + (sent, received, total) +} + +fn utilization_percent_for_history(ratio_percent: Option) -> u8 { + let Some(ratio_percent) = ratio_percent else { return 0; }; @@ -1450,11 +1477,9 @@ fn utilization_percent_for_history( } } -fn utilization_text(bytes_per_interval: u64, link_speed_bps: u64, elapsed_secs: f64) -> String { - let Some(ratio_percent) = - utilization_ratio_percent(bytes_per_interval, link_speed_bps, elapsed_secs) - else { - return "0%".to_string(); +fn utilization_text(ratio_percent: Option) -> String { + let Some(ratio_percent) = ratio_percent else { + return "-".to_string(); }; if ratio_percent > 0.0 && ratio_percent < 1.0 { @@ -1464,6 +1489,18 @@ fn utilization_text(bytes_per_interval: u64, link_speed_bps: u64, elapsed_secs: } } +fn format_link_speeds(transmit_bits_per_second: u64, receive_bits_per_second: u64) -> String { + match (transmit_bits_per_second, receive_bits_per_second) { + (0, 0) => "-".to_string(), + (transmit, receive) if transmit == receive => format_link_speed(transmit), + (transmit, receive) => format!( + "Tx {} / Rx {}", + format_link_speed(transmit), + format_link_speed(receive) + ), + } +} + fn format_link_speed(bits_per_second: u64) -> String { // 链路速率采用十进制网络单位显示,更符合网卡/交换机常见标注方式。 if bits_per_second == 0 { @@ -1845,6 +1882,48 @@ mod tests { assert_eq!(adapter_row_texts(&adapter)[0], "Renamed Ethernet"); } + #[test] + fn asymmetric_links_use_each_direction_capacity() { + let (sent, received, total) = + directional_utilization_ratios(1_250_000, 0, 10_000_000, 100_000_000, 1.0); + assert_eq!(sent, Some(100.0)); + assert_eq!(received, Some(0.0)); + assert_eq!(total, Some(100.0)); + + let (sent, received, total) = + directional_utilization_ratios(0, 1_250_000, 100_000_000, 10_000_000, 1.0); + assert_eq!(sent, Some(0.0)); + assert_eq!(received, Some(100.0)); + assert_eq!(total, Some(100.0)); + } + + #[test] + fn full_duplex_total_is_the_busiest_direction_not_a_sum() { + let (_, _, total) = + directional_utilization_ratios(12_500_000, 12_500_000, 100_000_000, 100_000_000, 1.0); + assert_eq!(total, Some(100.0)); + } + + #[test] + fn unavailable_direction_does_not_poison_the_other_direction() { + let (sent, received, total) = + directional_utilization_ratios(0, 1_250_000, 0, 10_000_000, 1.0); + assert_eq!(sent, None); + assert_eq!(received, Some(100.0)); + assert_eq!(total, Some(100.0)); + assert_eq!(utilization_text(None), "-"); + assert_eq!(utilization_text(Some(0.0)), "0%"); + } + + #[test] + fn asymmetric_link_speed_text_preserves_both_capacities() { + assert_eq!( + format_link_speeds(10_000_000, 100_000_000), + "Tx 10.0 Mbps / Rx 100 Mbps" + ); + assert_eq!(format_link_speeds(1_000_000_000, 1_000_000_000), "1.0 Gbps"); + } + #[test] fn adapter_counter_delta_rejects_missing_regressed_and_overflowed_intervals() { assert_eq!(adapter_counter_delta(10, 20, None), None); diff --git a/src/pages/processes/actions.rs b/src/pages/processes/actions.rs index 58b859c..2fb48ec 100644 --- a/src/pages/processes/actions.rs +++ b/src/pages/processes/actions.rs @@ -12,16 +12,18 @@ //! Every target process is reopened through `ProcIdentity` immediately before use. use std::collections::{HashMap, HashSet}; +use std::ffi::c_void; use std::mem::{size_of, zeroed}; use std::path::Path; use std::ptr::{null, null_mut}; use windows_sys::Win32::Foundation::{ ERROR_BUSY, ERROR_FILE_NOT_FOUND, ERROR_GEN_FAILURE, ERROR_INSUFFICIENT_BUFFER, - ERROR_INVALID_DATA, ERROR_INVALID_HANDLE, ERROR_INVALID_PARAMETER, ERROR_NO_MORE_FILES, - ERROR_NOT_SUPPORTED, ERROR_PATH_NOT_FOUND, FILETIME, GetLastError, HANDLE, HWND, LPARAM, - WAIT_OBJECT_0, WPARAM, + ERROR_INVALID_DATA, ERROR_INVALID_HANDLE, ERROR_INVALID_PARAMETER, ERROR_MORE_DATA, + ERROR_NO_MORE_FILES, ERROR_NOT_SUPPORTED, ERROR_PATH_NOT_FOUND, FILETIME, GetLastError, HANDLE, + HWND, LPARAM, WAIT_OBJECT_0, WPARAM, }; +use windows_sys::Win32::Security::SECURITY_ATTRIBUTES; use windows_sys::Win32::Storage::FileSystem::SYNCHRONIZE; use windows_sys::Win32::System::Diagnostics::ToolHelp::{ CreateToolhelp32Snapshot, PROCESSENTRY32W, Process32FirstW, Process32NextW, TH32CS_SNAPPROCESS, @@ -29,22 +31,27 @@ use windows_sys::Win32::System::Diagnostics::ToolHelp::{ }; use windows_sys::Win32::System::Environment::ExpandEnvironmentStringsW; use windows_sys::Win32::System::Registry::{ - HKEY, HKEY_LOCAL_MACHINE, KEY_READ, REG_EXPAND_SZ, REG_SZ, RegCloseKey, RegOpenKeyExW, - RegQueryValueExW, + HKEY, HKEY_LOCAL_MACHINE, KEY_READ, KEY_WOW64_32KEY, KEY_WOW64_64KEY, REG_EXPAND_SZ, REG_SZ, + RegCloseKey, RegOpenKeyExW, RegQueryValueExW, }; use windows_sys::Win32::System::SystemInformation::{ - GROUP_AFFINITY, GetSystemTimeAsFileTime, GetWindowsDirectoryW, + GROUP_AFFINITY, GetSystemTimeAsFileTime, GetWindowsDirectoryW, IMAGE_FILE_MACHINE_AMD64, + IMAGE_FILE_MACHINE_ARM, IMAGE_FILE_MACHINE_ARM64, IMAGE_FILE_MACHINE_ARMNT, + IMAGE_FILE_MACHINE_I386, IMAGE_FILE_MACHINE_IA64, IMAGE_FILE_MACHINE_THUMB, + IMAGE_FILE_MACHINE_UNKNOWN, }; use windows_sys::Win32::System::Threading::{ - ABOVE_NORMAL_PRIORITY_CLASS, BELOW_NORMAL_PRIORITY_CLASS, CreateProcessW, + ABOVE_NORMAL_PRIORITY_CLASS, BELOW_NORMAL_PRIORITY_CLASS, CREATE_NEW_CONSOLE, CreateEventW, + CreateProcessW, DeleteProcThreadAttributeList, EXTENDED_STARTUPINFO_PRESENT, GetProcessAffinityMask, GetProcessGroupAffinity, GetProcessIdOfThread, GetThreadGroupAffinity, - HIGH_PRIORITY_CLASS, IDLE_PRIORITY_CLASS, NORMAL_PRIORITY_CLASS, OpenThread, - PROCESS_INFORMATION, PROCESS_QUERY_INFORMATION, PROCESS_QUERY_LIMITED_INFORMATION, - PROCESS_SET_INFORMATION, PROCESS_SET_LIMITED_INFORMATION, PROCESS_TERMINATE, - QueryFullProcessImageNameW, REALTIME_PRIORITY_CLASS, STARTUPINFOW, SetPriorityClass, - SetProcessAffinityMask, SetProcessDefaultCpuSets, SetThreadGroupAffinity, - THREAD_QUERY_LIMITED_INFORMATION, THREAD_SET_INFORMATION, TerminateProcess, - WaitForSingleObject, + HIGH_PRIORITY_CLASS, IDLE_PRIORITY_CLASS, InitializeProcThreadAttributeList, IsWow64Process2, + LPPROC_THREAD_ATTRIBUTE_LIST, NORMAL_PRIORITY_CLASS, OpenThread, + PROC_THREAD_ATTRIBUTE_HANDLE_LIST, PROCESS_INFORMATION, PROCESS_QUERY_INFORMATION, + PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_SET_INFORMATION, PROCESS_SET_LIMITED_INFORMATION, + PROCESS_TERMINATE, QueryFullProcessImageNameW, REALTIME_PRIORITY_CLASS, STARTUPINFOEXW, + STARTUPINFOW, SetPriorityClass, SetProcessAffinityMask, SetProcessDefaultCpuSets, + SetThreadGroupAffinity, THREAD_QUERY_LIMITED_INFORMATION, THREAD_SET_INFORMATION, + TerminateProcess, UpdateProcThreadAttribute, WaitForSingleObject, }; use windows_sys::Win32::UI::Controls::{ BST_CHECKED, BST_UNCHECKED, CheckDlgButton, IsDlgButtonChecked, @@ -277,17 +284,9 @@ impl ProcessPageState { outcome.completed_without_failure() } - // 以 AeDebug 注册表配置的调试器启动并附加到目标进程。命令行传 -p 。 + // 使用目标进程位数对应的 AeDebug 命令模板启动调试器。完整模板中的第一个 + // `%ld` 接收 PID,第二个接收唯一继承给调试器的 ready-event 句柄。 pub(super) fn attach_debugger(&mut self, identity: ProcIdentity) -> bool { - let Some(debugger_path) = self.debugger_path.as_ref() else { - let error = match self.debugger_error { - Some(error) => error, - None => ERROR_FILE_NOT_FOUND, - }; - self.show_failure_message(&self.strings.cant_debug, error); - return false; - }; - if !self.quick_confirm(&self.strings.warning, &self.strings.debug) { return false; } @@ -300,34 +299,88 @@ impl ProcessPageState { return false; } }; + let registry_view = match debugger_registry_view_for_process(target_handle.as_raw()) { + Ok(view) => view, + Err(error) => { + self.show_failure_message(&self.strings.cant_debug, error); + return false; + } + }; + let debugger = match load_debugger_command(registry_view) { + Ok(Some(debugger)) => debugger, + Ok(None) => { + self.show_failure_message(&self.strings.cant_debug, ERROR_FILE_NOT_FOUND); + return false; + } + Err(error) => { + self.show_failure_message(&self.strings.cant_debug, error); + return false; + } + }; + if !Path::new(&debugger.executable).is_file() { + self.show_failure_message(&self.strings.cant_debug, ERROR_FILE_NOT_FOUND); + return false; + } + + let security = SECURITY_ATTRIBUTES { + nLength: size_of::() as u32, + lpSecurityDescriptor: null_mut(), + bInheritHandle: 1, + }; + // SAFETY: `security` remains live for the synchronous call and requests one unnamed, + // nonsignaled event whose returned handle is adopted immediately. + let raw_event = unsafe { CreateEventW(&security, 0, 0, null()) }; + let Some(debugger_ready_event) = (unsafe { OwnedHandle::from_raw(raw_event) }) else { + self.show_failure_message(&self.strings.cant_debug, nonzero_last_error()); + return false; + }; + let command_line = match format_debugger_template( + &debugger.template, + identity.pid, + debugger_ready_event.as_raw() as usize, + ) { + Ok(command_line) => command_line, + Err(error) => { + self.show_failure_message(&self.strings.cant_debug, error); + return false; + } + }; + let attributes = match ProcThreadAttributeList::for_handle(debugger_ready_event.as_raw()) { + Ok(attributes) => attributes, + Err(error) => { + self.show_failure_message(&self.strings.cant_debug, error); + return false; + } + }; - let pid = identity.pid; - let command_line = format!("{} -p {pid}", quote_command_line_arg(debugger_path)); let mut command_line_wide = to_wide_null(&command_line); - let application_name = to_wide_null(debugger_path); - let startup_info = STARTUPINFOW { - cb: size_of::() as u32, - ..unsafe { zeroed() } + let application_name = to_wide_null(&debugger.executable); + let startup_info = STARTUPINFOEXW { + StartupInfo: STARTUPINFOW { + cb: size_of::() as u32, + ..unsafe { zeroed() } + }, + lpAttributeList: attributes.as_ptr(), }; let mut process_info = unsafe { zeroed::() }; - // SAFETY: the terminated application name, mutable command line, and initialized - // input/output structs all remain live for this synchronous call. + // SAFETY: the application/command buffers and extended startup information remain live + // for the call. bInheritHandles is required by PROC_THREAD_ATTRIBUTE_HANDLE_LIST, which + // restricts inheritance to the one event in `attributes`. let created = unsafe { CreateProcessW( application_name.as_ptr(), command_line_wide.as_mut_ptr(), null_mut(), null_mut(), - 0, - windows_sys::Win32::System::Threading::CREATE_NEW_CONSOLE, + 1, + CREATE_NEW_CONSOLE | EXTENDED_STARTUPINFO_PRESENT, null(), null(), - &startup_info, + &startup_info.StartupInfo, &mut process_info, ) }; - // Capture last-error before dropping `target_handle`, whose destructor may change it. let create_error = if created == 0 { unsafe { GetLastError() } } else { @@ -336,11 +389,18 @@ impl ProcessPageState { drop(target_handle); if created == 0 { - self.show_failure_message(&self.strings.cant_debug, create_error); + self.show_failure_message( + &self.strings.cant_debug, + if create_error == 0 { + ERROR_GEN_FAILURE + } else { + create_error + }, + ); false } else { - // SAFETY: this branch is reached only after CreateProcessW succeeded, which returned - // two fresh handles whose ownership is transferred here. + // SAFETY: successful CreateProcessW returned two fresh handles and this is their only + // ownership transfer. The child owns its inherited copy of the ready event. match unsafe { own_created_process_handles(process_info) } { Ok(_) => true, Err(error) => { @@ -1014,86 +1074,259 @@ pub(super) fn affinity_cpu_mask(cpu_index: i32) -> usize { .unwrap_or(0) } -pub(super) fn load_debugger_path() -> Result, u32> { - // 进程页的“调试”命令依赖 AeDebug 注册表配置。 - // 这里只提取真正的可执行文件路径,过滤掉旧式 drwtsn32 之类的无效值。 - let mut key: HKEY = null_mut(); - let key_name = to_wide_null("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AeDebug"); - let value_name = to_wide_null("Debugger"); - // SAFETY: both input strings are terminated and `key` is a valid output location. - let open_status = - unsafe { RegOpenKeyExW(HKEY_LOCAL_MACHINE, key_name.as_ptr(), 0, KEY_READ, &mut key) }; - if open_status != 0 { - return if open_status == ERROR_FILE_NOT_FOUND || open_status == ERROR_PATH_NOT_FOUND { - Ok(None) - } else { - Err(open_status) +const AEDEBUG_KEY: &str = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AeDebug"; +const AEDEBUG_VALUE: &str = "Debugger"; +const MAX_DEBUGGER_COMMAND_BYTES: u32 = 1024 * 1024; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum DebuggerRegistryView { + Native, + Registry32, + Registry64, +} + +impl DebuggerRegistryView { + const fn access_mask(self) -> u32 { + match self { + Self::Native => 0, + Self::Registry32 => KEY_WOW64_32KEY, + Self::Registry64 => KEY_WOW64_64KEY, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct DebuggerCommand { + template: String, + executable: String, +} + +struct ProcThreadAttributeList { + storage: Vec, + handles: Box<[HANDLE; 1]>, + initialized: bool, +} + +impl ProcThreadAttributeList { + fn for_handle(handle: HANDLE) -> Result { + if handle.is_null() { + return Err(ERROR_INVALID_HANDLE); + } + + let mut byte_count = 0usize; + unsafe { + InitializeProcThreadAttributeList(null_mut(), 1, 0, &mut byte_count); + } + if byte_count == 0 { + return Err(nonzero_last_error()); + } + + let word_count = byte_count.div_ceil(size_of::()); + let mut value = Self { + storage: vec![0usize; word_count], + handles: Box::new([handle]), + initialized: false, }; + if unsafe { InitializeProcThreadAttributeList(value.as_ptr(), 1, 0, &mut byte_count) } == 0 + { + return Err(nonzero_last_error()); + } + value.initialized = true; + if unsafe { + UpdateProcThreadAttribute( + value.as_ptr(), + 0, + PROC_THREAD_ATTRIBUTE_HANDLE_LIST as usize, + value.handles.as_mut_ptr().cast::(), + size_of::(), + null_mut(), + null_mut(), + ) + } == 0 + { + return Err(nonzero_last_error()); + } + Ok(value) + } + + fn as_ptr(&self) -> LPPROC_THREAD_ATTRIBUTE_LIST { + self.storage.as_ptr() as LPPROC_THREAD_ATTRIBUTE_LIST + } +} + +impl Drop for ProcThreadAttributeList { + fn drop(&mut self) { + if self.initialized { + unsafe { DeleteProcThreadAttributeList(self.as_ptr()) }; + } + } +} + +pub(super) fn load_debugger_path() -> Result, u32> { + // This is an availability probe for menu state. Launch-time selection is repeated against + // the selected process' machine type so the command can never use the wrong registry view. + let mut first_error = None; + for view in [ + DebuggerRegistryView::Native, + DebuggerRegistryView::Registry64, + DebuggerRegistryView::Registry32, + ] { + match load_debugger_command(view) { + Ok(Some(command)) if Path::new(&command.executable).is_file() => { + return Ok(Some(command.executable)); + } + Ok(_) => {} + Err(error) => { + first_error.get_or_insert(error); + } + } } + if let Some(error) = first_error { + Err(error) + } else { + Ok(None) + } +} - let mut value_size = 0u32; - // SAFETY: `key` was opened successfully; this size query uses no data buffer and writes only - // to `value_size`. - let size_status = unsafe { - RegQueryValueExW( +fn load_debugger_command(view: DebuggerRegistryView) -> Result, u32> { + let Some((raw_command, value_type)) = read_aedebug_string(view)? else { + return Ok(None); + }; + parse_debugger_command(&raw_command, value_type) +} + +fn read_aedebug_string(view: DebuggerRegistryView) -> Result, u32> { + unsafe { + let key_name = to_wide_null(AEDEBUG_KEY); + let value_name = to_wide_null(AEDEBUG_VALUE); + let mut key: HKEY = null_mut(); + let open_status = RegOpenKeyExW( + HKEY_LOCAL_MACHINE, + key_name.as_ptr(), + 0, + KEY_READ | view.access_mask(), + &mut key, + ); + if open_status == ERROR_FILE_NOT_FOUND || open_status == ERROR_PATH_NOT_FOUND { + return Ok(None); + } + if open_status != 0 { + return Err(open_status); + } + + let mut value_type = 0u32; + let mut value_size = 0u32; + let size_status = RegQueryValueExW( key, value_name.as_ptr(), null_mut(), - null_mut(), + &mut value_type, null_mut(), &mut value_size, - ) - }; - if size_status != 0 || value_size < 2 { - let close_status = unsafe { RegCloseKey(key) }; - if close_status != 0 { - return Err(close_status); + ); + if size_status == ERROR_FILE_NOT_FOUND { + let close_status = RegCloseKey(key); + return if close_status == 0 { + Ok(None) + } else { + Err(close_status) + }; + } + if size_status != 0 { + RegCloseKey(key); + return Err(size_status); + } + if !matches!(value_type, REG_SZ | REG_EXPAND_SZ) + || value_size < size_of::() as u32 + || !value_size.is_multiple_of(size_of::() as u32) + || value_size > MAX_DEBUGGER_COMMAND_BYTES + { + RegCloseKey(key); + return Err(ERROR_INVALID_DATA); } - return if size_status == ERROR_FILE_NOT_FOUND { - Ok(None) - } else if size_status != 0 { - Err(size_status) - } else { - Err(ERROR_INVALID_DATA) - }; - } - let mut buffer = vec![0u16; (value_size as usize).div_ceil(size_of::()).max(2)]; - let mut value_type = 0u32; - // SAFETY: `buffer` is writable for the byte count returned by the size query and all output - // pointers reference live local variables. - let status = unsafe { - RegQueryValueExW( + let mut buffer = vec![0u16; value_size as usize / size_of::()]; + let mut actual_size = value_size; + let read_status = RegQueryValueExW( key, value_name.as_ptr(), null_mut(), &mut value_type, - buffer.as_mut_ptr() as *mut u8, - &mut value_size, - ) - }; - let close_status = unsafe { RegCloseKey(key) }; - if close_status != 0 { - return Err(close_status); + buffer.as_mut_ptr().cast::(), + &mut actual_size, + ); + let close_status = RegCloseKey(key); + if read_status == ERROR_MORE_DATA { + return Err(ERROR_MORE_DATA); + } + if read_status != 0 { + return Err(read_status); + } + if close_status != 0 { + return Err(close_status); + } + if !matches!(value_type, REG_SZ | REG_EXPAND_SZ) + || actual_size < size_of::() as u32 + || !actual_size.is_multiple_of(size_of::() as u32) + || actual_size > value_size + { + return Err(ERROR_INVALID_DATA); + } + let units = actual_size as usize / size_of::(); + let Some(length) = buffer[..units].iter().position(|value| *value == 0) else { + return Err(ERROR_INVALID_DATA); + }; + Ok(Some(( + String::from_utf16(&buffer[..length]).map_err(|_| ERROR_INVALID_DATA)?, + value_type, + ))) } +} - if status != 0 || value_size < 2 || !(value_type == REG_SZ || value_type == REG_EXPAND_SZ) { - return Err(if status != 0 { - status - } else { - ERROR_INVALID_DATA - }); +fn debugger_registry_view_for_process(process: HANDLE) -> Result { + if process.is_null() { + return Err(ERROR_INVALID_HANDLE); + } + let mut process_machine = IMAGE_FILE_MACHINE_UNKNOWN; + let mut native_machine = IMAGE_FILE_MACHINE_UNKNOWN; + if unsafe { IsWow64Process2(process, &mut process_machine, &mut native_machine) } == 0 { + return Err(nonzero_last_error()); } + debugger_registry_view_for_machines(process_machine, native_machine) +} - let length = buffer - .iter() - .position(|value| *value == 0) - .unwrap_or(buffer.len()); - let raw_command = String::from_utf16_lossy(&buffer[..length]); - let Some(executable) = normalize_debugger_command(&raw_command, value_type)? else { - return Ok(None); +fn debugger_registry_view_for_machines( + process_machine: u16, + native_machine: u16, +) -> Result { + let effective_machine = if process_machine == IMAGE_FILE_MACHINE_UNKNOWN { + native_machine + } else { + process_machine }; - Ok(Path::new(&executable).is_file().then_some(executable)) + let target_is_32_bit = match effective_machine { + IMAGE_FILE_MACHINE_I386 + | IMAGE_FILE_MACHINE_ARM + | IMAGE_FILE_MACHINE_ARMNT + | IMAGE_FILE_MACHINE_THUMB => true, + IMAGE_FILE_MACHINE_AMD64 | IMAGE_FILE_MACHINE_ARM64 | IMAGE_FILE_MACHINE_IA64 => false, + _ => return Err(ERROR_NOT_SUPPORTED), + }; + let native_is_64_bit = match native_machine { + IMAGE_FILE_MACHINE_AMD64 | IMAGE_FILE_MACHINE_ARM64 | IMAGE_FILE_MACHINE_IA64 => true, + IMAGE_FILE_MACHINE_I386 + | IMAGE_FILE_MACHINE_ARM + | IMAGE_FILE_MACHINE_ARMNT + | IMAGE_FILE_MACHINE_THUMB => false, + _ => return Err(ERROR_NOT_SUPPORTED), + }; + Ok(if !native_is_64_bit { + DebuggerRegistryView::Native + } else if target_is_32_bit { + DebuggerRegistryView::Registry32 + } else { + DebuggerRegistryView::Registry64 + }) } // 引用命令行参数。只在包含空格、制表符或引号时加引号,并正确处理反斜杠转义。 @@ -1147,20 +1380,108 @@ pub(super) fn extract_first_command_token(command_line: &str) -> String { } } -// 将 AeDebug 注册表值规范化:展开环境变量后提取可执行文件路径,过滤无效调试器。 -fn normalize_debugger_command(command_line: &str, value_type: u32) -> Result, u32> { +fn parse_debugger_command( + command_line: &str, + value_type: u32, +) -> Result, u32> { let expanded = if value_type == REG_EXPAND_SZ { expand_environment_variables(command_line)? - } else { + } else if value_type == REG_SZ { command_line.to_string() + } else { + return Err(ERROR_INVALID_DATA); }; - Ok(normalize_debugger_command_with( - &expanded, - REG_SZ, - str::to_string, - )) + parse_expanded_debugger_command(&expanded) +} + +fn parse_expanded_debugger_command(command_line: &str) -> Result, u32> { + let template = command_line.trim().to_string(); + let executable = extract_first_command_token(&template); + let file_name = Path::new(&executable) + .file_name() + .and_then(|value| value.to_str()) + .unwrap_or_default(); + if executable.is_empty() + || !Path::new(&executable).is_absolute() + || file_name.eq_ignore_ascii_case("drwtsn32") + || file_name.eq_ignore_ascii_case("drwtsn32.exe") + { + return Ok(None); + } + validate_debugger_template(&template)?; + Ok(Some(DebuggerCommand { + template, + executable, + })) +} + +fn validate_debugger_template(template: &str) -> Result<(), u32> { + let mut placeholders = 0usize; + let mut chars = template.chars(); + while let Some(ch) = chars.next() { + if ch != '%' { + continue; + } + match chars.next() { + Some('%') => {} + Some('l' | 'L') => { + if !matches!(chars.next(), Some('d' | 'D')) { + return Err(ERROR_INVALID_DATA); + } + placeholders += 1; + } + Some('p' | 'P') => return Err(ERROR_NOT_SUPPORTED), + Some(_) | None => return Err(ERROR_INVALID_DATA), + } + } + if placeholders == 2 { + Ok(()) + } else { + Err(ERROR_INVALID_DATA) + } } +fn format_debugger_template( + template: &str, + process_id: u32, + event_handle: usize, +) -> Result { + validate_debugger_template(template)?; + let replacements = [process_id.to_string(), event_handle.to_string()]; + let mut replacement_index = 0usize; + let mut output = String::with_capacity(template.len() + 32); + let mut chars = template.chars(); + while let Some(ch) = chars.next() { + if ch != '%' { + output.push(ch); + continue; + } + match chars.next() { + Some('%') => output.push('%'), + Some('l' | 'L') => { + if !matches!(chars.next(), Some('d' | 'D')) { + return Err(ERROR_INVALID_DATA); + } + output.push_str( + replacements + .get(replacement_index) + .ok_or(ERROR_INVALID_DATA)?, + ); + replacement_index += 1; + } + Some('p' | 'P') => return Err(ERROR_NOT_SUPPORTED), + Some(_) | None => return Err(ERROR_INVALID_DATA), + } + } + if replacement_index == replacements.len() { + Ok(output) + } else { + Err(ERROR_INVALID_DATA) + } +} + +// Compatibility helper used by the existing pure parsing tests. +#[cfg(test)] pub(super) fn normalize_debugger_command_with( command_line: &str, value_type: u32, @@ -1171,14 +1492,19 @@ where { let expanded = if value_type == REG_EXPAND_SZ { expand_environment_variables(command_line) - } else { + } else if value_type == REG_SZ { command_line.to_string() + } else { + return None; }; let executable = extract_first_command_token(&expanded); - + let file_name = Path::new(&executable) + .file_name() + .and_then(|value| value.to_str()) + .unwrap_or_default(); if executable.is_empty() - || executable.eq_ignore_ascii_case("drwtsn32") - || executable.eq_ignore_ascii_case("drwtsn32.exe") + || file_name.eq_ignore_ascii_case("drwtsn32") + || file_name.eq_ignore_ascii_case("drwtsn32.exe") { None } else { @@ -1189,21 +1515,16 @@ where // 展开字符串中的环境变量(如 %SystemRoot%)。 // 使用 Win32 ExpandEnvironmentStringsW API,正确处理 WOW64 重定向和 %% 转义。 fn expand_environment_variables(command_line: &str) -> Result { - // 安全性: the Win32 ExpandEnvironmentStringsW API reads the process environment block - // maintained by the kernel, which handles system-variable edge cases (WOW64 redirections, - // %% escaping, variable-length limits) correctly. let wide_input = to_wide_null(command_line); let required = unsafe { ExpandEnvironmentStringsW(wide_input.as_ptr(), null_mut(), 0) }; if required == 0 { - let error = unsafe { GetLastError() }; - return Err(if error == 0 { ERROR_GEN_FAILURE } else { error }); + return Err(nonzero_last_error()); } let mut buffer = vec![0u16; required as usize]; let written = unsafe { ExpandEnvironmentStringsW(wide_input.as_ptr(), buffer.as_mut_ptr(), required) }; if written == 0 || written > required { - let error = unsafe { GetLastError() }; - return Err(if error == 0 { ERROR_GEN_FAILURE } else { error }); + return Err(nonzero_last_error()); } let len = buffer.iter().position(|&c| c == 0).unwrap_or(buffer.len()); Ok(String::from_utf16_lossy(&buffer[..len])) @@ -1465,3 +1786,81 @@ fn query_windows_directory() -> Result { buffer.resize(length.saturating_add(1), 0); } } + +#[cfg(test)] +mod debugger_tests { + use super::*; + + #[test] + fn full_aedebug_template_preserves_debugger_specific_arguments() { + let template = r#""C:\Debuggers\windbg.exe" -p %ld -e %ld -g"#; + let command = parse_expanded_debugger_command(template).unwrap().unwrap(); + assert_eq!(command.executable, r"C:\Debuggers\windbg.exe"); + assert_eq!( + format_debugger_template(&command.template, 1234, 5678).unwrap(), + r#""C:\Debuggers\windbg.exe" -p 1234 -e 5678 -g"# + ); + } + + #[test] + fn visual_studio_jit_template_and_literal_percent_are_supported() { + let template = r#""C:\Windows\System32\vsjitdebugger.exe" -p %ld -e %ld --label 100%%"#; + assert_eq!( + format_debugger_template(template, 42, 99).unwrap(), + r#""C:\Windows\System32\vsjitdebugger.exe" -p 42 -e 99 --label 100%"# + ); + } + + #[test] + fn unsupported_or_ambiguous_templates_are_rejected() { + assert_eq!( + parse_expanded_debugger_command(r#""C:\Debuggers\dbg.exe" -p %ld -e %ld -j 0x%p"#), + Err(ERROR_NOT_SUPPORTED) + ); + for template in [ + r#""C:\Debuggers\dbg.exe" -p %ld"#, + r#""C:\Debuggers\dbg.exe" -p %ld -e %ld -x %ld"#, + r#""C:\Debuggers\dbg.exe" -p %q -e %ld"#, + ] { + assert!(parse_expanded_debugger_command(template).is_err()); + } + assert!( + parse_expanded_debugger_command(r#""relative\dbg.exe" -p %ld -e %ld"#) + .unwrap() + .is_none() + ); + } + + #[test] + fn target_machine_selects_the_matching_registry_view() { + assert_eq!( + debugger_registry_view_for_machines(IMAGE_FILE_MACHINE_I386, IMAGE_FILE_MACHINE_AMD64,) + .unwrap(), + DebuggerRegistryView::Registry32 + ); + assert_eq!( + debugger_registry_view_for_machines( + IMAGE_FILE_MACHINE_UNKNOWN, + IMAGE_FILE_MACHINE_AMD64, + ) + .unwrap(), + DebuggerRegistryView::Registry64 + ); + assert_eq!( + debugger_registry_view_for_machines( + IMAGE_FILE_MACHINE_UNKNOWN, + IMAGE_FILE_MACHINE_I386, + ) + .unwrap(), + DebuggerRegistryView::Native + ); + assert_eq!( + debugger_registry_view_for_machines( + IMAGE_FILE_MACHINE_ARMNT, + IMAGE_FILE_MACHINE_ARM64, + ) + .unwrap(), + DebuggerRegistryView::Registry32 + ); + } +}