winbrew_windows\system/
mod.rs

1use std::ffi::{OsStr, OsString};
2use std::fmt;
3use std::os::windows::ffi::{OsStrExt, OsStringExt};
4use std::path::PathBuf;
5
6use crate::models::domains::install::Architecture;
7use crate::registry::read_product_type;
8use windows_sys::Win32::Foundation::CloseHandle;
9use windows_sys::Win32::Security::{
10    GetTokenInformation, TOKEN_ELEVATION, TOKEN_QUERY, TokenElevation,
11};
12use windows_sys::Win32::Storage::FileSystem::SearchPathW;
13use windows_sys::Win32::System::SystemInformation::{
14    GetNativeSystemInfo, PROCESSOR_ARCHITECTURE_AMD64, PROCESSOR_ARCHITECTURE_ARM64,
15    PROCESSOR_ARCHITECTURE_INTEL, SYSTEM_INFO,
16};
17use windows_sys::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
18
19mod version;
20
21pub use version::windows_version_string;
22
23const NORMAL_PLATFORM_TAGS: &[&str] = &["windows.desktop", "windows.ltsc", "windows.universal"];
24const SERVER_PLATFORM_TAGS: &[&str] = &["windows.server"];
25
26/// Combined host family and native architecture snapshot used for installer selection.
27#[must_use]
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub struct HostProfile {
30    /// `true` when Windows reports a server-class product type.
31    pub is_server: bool,
32    /// Native CPU architecture reported by Windows.
33    pub architecture: Architecture,
34}
35
36impl HostProfile {
37    /// Return the Winget platform labels accepted for this host profile.
38    pub fn platform_tags(self) -> &'static [&'static str] {
39        if self.is_server {
40            SERVER_PLATFORM_TAGS
41        } else {
42            NORMAL_PLATFORM_TAGS
43        }
44    }
45}
46
47impl fmt::Display for HostProfile {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        let host_family = if self.is_server { "server" } else { "normal" };
50
51        write!(f, "{host_family} {}", self.architecture)
52    }
53}
54
55/// Return the current Windows host profile for platform-aware catalog selection.
56///
57/// The helper is best-effort: if the Windows product-type registry key cannot
58/// be read, the host family falls back to `normal` instead of blocking install
59/// flows.
60pub fn host_profile() -> HostProfile {
61    HostProfile {
62        is_server: read_product_type()
63            .as_deref()
64            .map(classify_product_type)
65            .unwrap_or(false),
66        architecture: native_architecture(),
67    }
68}
69
70/// Return `true` when the current process is running elevated.
71pub fn is_elevated() -> bool {
72    let mut token = core::ptr::null_mut();
73
74    let token_opened =
75        unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) } != 0;
76    if !token_opened {
77        return false;
78    }
79
80    let mut elevation = TOKEN_ELEVATION { TokenIsElevated: 0 };
81    let mut return_length = 0u32;
82    let queried = unsafe {
83        GetTokenInformation(
84            token,
85            TokenElevation,
86            &mut elevation as *mut TOKEN_ELEVATION as *mut core::ffi::c_void,
87            core::mem::size_of::<TOKEN_ELEVATION>() as u32,
88            &mut return_length,
89        )
90    } != 0;
91
92    unsafe {
93        CloseHandle(token);
94    }
95
96    queried && elevation.TokenIsElevated != 0
97}
98
99/// Search the current Windows search path for a file name.
100///
101/// This uses `SearchPathW`, so it checks the standard current-directory, system
102/// directory, and PATH search order on Windows.
103pub fn search_path_file(file_name: &str) -> Option<PathBuf> {
104    search_path_file_with_path(file_name, None)
105}
106
107fn search_path_file_with_path(file_name: &str, search_path: Option<&OsStr>) -> Option<PathBuf> {
108    let file_name_wide: Vec<u16> = OsStr::new(file_name).encode_wide().chain(Some(0)).collect();
109    let search_path_wide =
110        search_path.map(|path| path.encode_wide().chain(Some(0)).collect::<Vec<u16>>());
111
112    let mut buffer_len = 260u32;
113
114    loop {
115        let mut buffer = vec![0u16; buffer_len as usize];
116        let written = unsafe {
117            SearchPathW(
118                search_path_wide
119                    .as_ref()
120                    .map_or(core::ptr::null(), |path| path.as_ptr()),
121                file_name_wide.as_ptr(),
122                core::ptr::null(),
123                buffer_len,
124                buffer.as_mut_ptr(),
125                core::ptr::null_mut(),
126            )
127        };
128
129        if written == 0 {
130            return None;
131        }
132
133        if written as usize >= buffer.len() {
134            buffer_len = written + 1;
135            continue;
136        }
137
138        buffer.truncate(written as usize);
139        return Some(PathBuf::from(OsString::from_wide(&buffer)));
140    }
141}
142
143fn classify_product_type(product_type: &str) -> bool {
144    let trimmed = product_type.trim();
145
146    trimmed.eq_ignore_ascii_case("servernt") || trimmed.eq_ignore_ascii_case("lanmannt")
147}
148
149fn native_architecture() -> Architecture {
150    let mut system_info: SYSTEM_INFO = unsafe { std::mem::zeroed() };
151
152    unsafe {
153        GetNativeSystemInfo(&mut system_info as *mut SYSTEM_INFO);
154    }
155
156    architecture_from_native_system_info(system_info)
157}
158
159fn architecture_from_native_system_info(system_info: SYSTEM_INFO) -> Architecture {
160    let processor_architecture = unsafe { system_info.Anonymous.Anonymous.wProcessorArchitecture };
161
162    match processor_architecture {
163        PROCESSOR_ARCHITECTURE_AMD64 => Architecture::X64,
164        PROCESSOR_ARCHITECTURE_INTEL => Architecture::X86,
165        PROCESSOR_ARCHITECTURE_ARM64 => Architecture::Arm64,
166        _ => Architecture::Any,
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::{
173        HostProfile, architecture_from_native_system_info, classify_product_type,
174        search_path_file_with_path,
175    };
176    use crate::models::domains::install::Architecture;
177    use std::fs;
178    use tempfile::tempdir;
179    use windows_sys::Win32::System::SystemInformation::{
180        PROCESSOR_ARCHITECTURE_AMD64, PROCESSOR_ARCHITECTURE_ARM64, PROCESSOR_ARCHITECTURE_INTEL,
181        SYSTEM_INFO,
182    };
183
184    fn system_info_for(architecture: u16) -> SYSTEM_INFO {
185        let mut system_info = unsafe { std::mem::zeroed::<SYSTEM_INFO>() };
186        system_info.Anonymous.Anonymous.wProcessorArchitecture = architecture;
187        system_info
188    }
189
190    fn host_profile(is_server: bool, architecture: Architecture) -> HostProfile {
191        HostProfile {
192            is_server,
193            architecture,
194        }
195    }
196
197    #[test]
198    fn classifies_server_product_types() {
199        assert!(classify_product_type("ServerNT"));
200        assert!(classify_product_type("LanmanNT"));
201        assert!(!classify_product_type("WinNT"));
202    }
203
204    #[test]
205    fn classifies_server_product_types_case_insensitively_and_with_whitespace() {
206        assert!(classify_product_type("  SERVERNT  "));
207        assert!(classify_product_type("  lanmannt"));
208        assert!(!classify_product_type(""));
209        assert!(!classify_product_type("Unknown"));
210    }
211
212    #[test]
213    fn host_profile_exposes_platform_tags_by_family() {
214        assert_eq!(
215            host_profile(false, Architecture::X64).platform_tags(),
216            &["windows.desktop", "windows.ltsc", "windows.universal"]
217        );
218        assert_eq!(
219            host_profile(true, Architecture::Arm64).platform_tags(),
220            &["windows.server"]
221        );
222    }
223
224    #[test]
225    fn maps_native_processor_architectures() {
226        assert_eq!(
227            architecture_from_native_system_info(system_info_for(PROCESSOR_ARCHITECTURE_AMD64)),
228            Architecture::X64
229        );
230        assert_eq!(
231            architecture_from_native_system_info(system_info_for(PROCESSOR_ARCHITECTURE_INTEL)),
232            Architecture::X86
233        );
234        assert_eq!(
235            architecture_from_native_system_info(system_info_for(PROCESSOR_ARCHITECTURE_ARM64)),
236            Architecture::Arm64
237        );
238        assert_eq!(
239            architecture_from_native_system_info(system_info_for(0xffff)),
240            Architecture::Any
241        );
242    }
243
244    #[test]
245    fn search_path_file_uses_explicit_search_path_list() {
246        let temp_dir = tempdir().expect("temp dir");
247        let file_path = temp_dir.path().join("7z.exe");
248
249        fs::write(&file_path, b"placeholder").expect("fake binary");
250
251        let found = search_path_file_with_path("7z.exe", Some(temp_dir.path().as_os_str()))
252            .expect("file should be found via explicit search path");
253
254        assert_eq!(found, file_path);
255    }
256}