1mod types;
6
7use std::borrow::Cow;
8use std::collections::HashSet;
9use std::net::{Ipv4Addr, Ipv6Addr};
10
11use windows::Win32::Foundation::HANDLE;
12use windows::Win32::Foundation::{ERROR_ACCESS_DENIED, ERROR_NOT_ALL_ASSIGNED, GetLastError};
13use windows::Win32::NetworkManagement::IpHelper::{
14 GAA_FLAG_INCLUDE_PREFIX, GET_ADAPTERS_ADDRESSES_FLAGS, GetAdaptersAddresses,
15 IP_ADAPTER_ADDRESSES_LH,
16};
17use windows::Win32::Networking::WinSock::{
18 AF_INET, AF_INET6, AF_UNSPEC, SOCKADDR_IN, SOCKADDR_IN6,
19};
20use windows::Win32::Security::{
21 AdjustTokenPrivileges, LookupPrivilegeValueW, SE_PRIVILEGE_ENABLED, TOKEN_ADJUST_PRIVILEGES,
22 TOKEN_PRIVILEGES, TOKEN_QUERY,
23};
24use windows::Win32::Storage::FileSystem::{
25 CreateFileW, FILE_FLAGS_AND_ATTRIBUTES, FILE_SHARE_MODE, FILE_SHARE_READ, FILE_SHARE_WRITE,
26 GetDiskFreeSpaceExW, GetLogicalDriveStringsW, GetVolumeInformationW, OPEN_EXISTING,
27};
28use windows::Win32::System::IO::DeviceIoControl;
29use windows::Win32::System::Ioctl::{GET_LENGTH_INFORMATION, IOCTL_DISK_GET_LENGTH_INFO};
30use windows::Win32::System::Shutdown::{
31 InitiateSystemShutdownExW, SHTDN_REASON_FLAG_PLANNED, SHTDN_REASON_MAJOR_OTHER,
32 SHTDN_REASON_MINOR_OTHER, SHUTDOWN_REASON,
33};
34use windows::Win32::System::SystemInformation::{
35 FIRMWARE_TABLE_PROVIDER, GetSystemFirmwareTable, OSVERSIONINFOW,
36};
37use windows::Win32::System::Threading::GetCurrentProcess;
38use windows::Win32::System::Threading::OpenProcessToken;
39use windows::core::{HRESULT, PCWSTR, PWSTR};
40
41use crate::error::{AccessDeniedError, Error, Result, WindowsApiError};
42use crate::registry::{self, Hive};
43use crate::utils::{OwnedHandle, pwstr_to_string, to_utf16_nul};
44
45pub use types::{
46 BiosInfo, GuidInfo, HostIdentity, HostSnapshot, LogicalDiskInfo, MachineGuid,
47 NetworkInterfaceInfo, OsInfo, PhysicalDiskInfo, PowerAction, PowerActionOptions,
48 SnapshotSection, SnapshotSectionError, UserInfo,
49};
50
51pub fn snapshot() -> HostSnapshot {
56 let mut section_errors = Vec::new();
57
58 let identity = match hostname() {
59 Ok(hostname) => HostIdentity { hostname },
60 Err(err) => {
61 section_errors.push(section_error(SnapshotSection::Identity, err));
62 HostIdentity {
63 hostname: "unknown".to_string(),
64 }
65 }
66 };
67
68 let os = match os_info() {
69 Ok(os) => os,
70 Err(err) => {
71 section_errors.push(section_error(SnapshotSection::Os, err));
72 OsInfo {
73 product_name: None,
74 release_label: None,
75 build_number: 0,
76 major_version: 0,
77 minor_version: 0,
78 }
79 }
80 };
81
82 let guids = match guid_info() {
83 Ok(guids) => guids,
84 Err(err) => {
85 section_errors.push(section_error(SnapshotSection::Guids, err));
86 GuidInfo {
87 machine_guid: None,
88 firmware_guid: None,
89 }
90 }
91 };
92
93 let bios = match bios_info() {
94 Ok(bios) => bios,
95 Err(err) => {
96 section_errors.push(section_error(SnapshotSection::Bios, err));
97 None
98 }
99 };
100
101 let logical_disks = match logical_disks() {
102 Ok(disks) => disks,
103 Err(err) => {
104 section_errors.push(section_error(SnapshotSection::LogicalDisks, err));
105 Vec::new()
106 }
107 };
108
109 let physical_disks = match physical_disks() {
110 Ok(disks) => disks,
111 Err(err) => {
112 section_errors.push(section_error(SnapshotSection::PhysicalDisks, err));
113 Vec::new()
114 }
115 };
116
117 let networks = match network_interfaces() {
118 Ok(interfaces) => interfaces,
119 Err(err) => {
120 section_errors.push(section_error(SnapshotSection::Network, err));
121 Vec::new()
122 }
123 };
124
125 let users = match users() {
126 Ok(users) => users,
127 Err(err) => {
128 section_errors.push(section_error(SnapshotSection::Users, err));
129 Vec::new()
130 }
131 };
132
133 HostSnapshot {
134 identity,
135 os,
136 guids,
137 bios,
138 logical_disks,
139 physical_disks,
140 networks,
141 users,
142 section_errors,
143 }
144}
145
146pub fn hostname() -> Result<String> {
148 if let Ok(value) = std::env::var("COMPUTERNAME")
149 && !value.trim().is_empty()
150 {
151 return Ok(value);
152 }
153
154 let value = registry::read_string(
155 Hive::LocalMachine,
156 r"SYSTEM\CurrentControlSet\Control\ComputerName\ActiveComputerName",
157 "ComputerName",
158 )?;
159
160 if value.trim().is_empty() {
161 return Err(Error::Other(crate::error::OtherError::new(
162 "hostname is empty",
163 )));
164 }
165
166 Ok(value)
167}
168
169pub fn os_info() -> Result<OsInfo> {
171 let mut version = OSVERSIONINFOW {
172 dwOSVersionInfoSize: std::mem::size_of::<OSVERSIONINFOW>() as u32,
173 ..Default::default()
174 };
175
176 query_real_os_version(&mut version)?;
177
178 let product_name = resolve_product_name(
179 version.dwMajorVersion,
180 version.dwMinorVersion,
181 version.dwBuildNumber,
182 );
183
184 let release_label = derive_release_label(
185 version.dwMajorVersion,
186 version.dwMinorVersion,
187 version.dwBuildNumber,
188 );
189
190 Ok(OsInfo {
191 product_name,
192 release_label,
193 build_number: version.dwBuildNumber,
194 major_version: version.dwMajorVersion,
195 minor_version: version.dwMinorVersion,
196 })
197}
198
199fn query_real_os_version(out_version: &mut OSVERSIONINFOW) -> Result<()> {
200 #[link(name = "ntdll")]
201 unsafe extern "system" {
202 fn RtlGetVersion(lpVersionInformation: *mut OSVERSIONINFOW) -> i32;
203 }
204
205 let status = unsafe { RtlGetVersion(out_version as *mut OSVERSIONINFOW) };
206 if status < 0 {
207 return Err(Error::Other(crate::error::OtherError::new(Cow::Owned(
208 format!("RtlGetVersion failed with NTSTATUS 0x{status:08X}"),
209 ))));
210 }
211
212 Ok(())
213}
214
215pub fn guid_info() -> Result<GuidInfo> {
217 let machine_guid = machine_guid().ok();
218 let firmware_guid = firmware_guid_from_smbios().ok().flatten();
219
220 Ok(GuidInfo {
221 machine_guid,
222 firmware_guid,
223 })
224}
225
226pub fn machine_guid() -> Result<MachineGuid> {
228 let guid = registry::read_string(
229 Hive::LocalMachine,
230 r"SOFTWARE\Microsoft\Cryptography",
231 "MachineGuid",
232 )?;
233
234 if guid.trim().is_empty() {
235 return Err(Error::Other(crate::error::OtherError::new(
236 "MachineGuid registry value is empty",
237 )));
238 }
239
240 Ok(MachineGuid::new(guid))
241}
242
243pub fn bios_info() -> Result<Option<BiosInfo>> {
245 let path = r"HARDWARE\DESCRIPTION\System\BIOS";
246
247 let vendor = registry::read_string(Hive::LocalMachine, path, "BIOSVendor").ok();
248 let version = registry::read_string(Hive::LocalMachine, path, "BIOSVersion").ok();
249 let release_date = registry::read_string(Hive::LocalMachine, path, "BIOSReleaseDate").ok();
250 let system_manufacturer =
251 registry::read_string(Hive::LocalMachine, path, "SystemManufacturer").ok();
252 let system_product_name =
253 registry::read_string(Hive::LocalMachine, path, "SystemProductName").ok();
254
255 if vendor.is_none()
256 && version.is_none()
257 && release_date.is_none()
258 && system_manufacturer.is_none()
259 && system_product_name.is_none()
260 {
261 return Ok(None);
262 }
263
264 Ok(Some(BiosInfo {
265 vendor,
266 version,
267 release_date,
268 system_manufacturer,
269 system_product_name,
270 }))
271}
272
273pub fn logical_disks() -> Result<Vec<LogicalDiskInfo>> {
275 let mut out_disks = Vec::with_capacity(16);
276 logical_disks_with_filter(&mut out_disks, |_| true)?;
277 Ok(out_disks)
278}
279
280pub fn logical_disks_with_buffer(out_disks: &mut Vec<LogicalDiskInfo>) -> Result<usize> {
282 logical_disks_with_filter(out_disks, |_| true)
283}
284
285pub fn logical_disks_with_filter<F>(
287 out_disks: &mut Vec<LogicalDiskInfo>,
288 filter: F,
289) -> Result<usize>
290where
291 F: Fn(&LogicalDiskInfo) -> bool,
292{
293 out_disks.clear();
294
295 let mut work_buffer = vec![0u16; 512];
296 let chars = unsafe { GetLogicalDriveStringsW(Some(&mut work_buffer)) } as usize;
297
298 if chars == 0 {
299 return Err(Error::WindowsApi(WindowsApiError::with_context(
300 windows::core::Error::from_win32(),
301 "GetLogicalDriveStringsW",
302 )));
303 }
304
305 if chars > work_buffer.len() {
306 work_buffer.resize(chars, 0);
307 let second = unsafe { GetLogicalDriveStringsW(Some(&mut work_buffer)) } as usize;
308 if second == 0 {
309 return Err(Error::WindowsApi(WindowsApiError::with_context(
310 windows::core::Error::from_win32(),
311 "GetLogicalDriveStringsW",
312 )));
313 }
314 }
315
316 for root in parse_multi_sz(&work_buffer) {
317 let root_wide = to_utf16_nul(&root);
318
319 let mut free_bytes_available = 0u64;
320 let mut total_bytes = 0u64;
321 let mut total_free_bytes = 0u64;
322
323 unsafe {
324 GetDiskFreeSpaceExW(
325 windows::core::PCWSTR(root_wide.as_ptr()),
326 Some(&mut free_bytes_available),
327 Some(&mut total_bytes),
328 Some(&mut total_free_bytes),
329 )
330 }
331 .map_err(|e| Error::WindowsApi(WindowsApiError::with_context(e, "GetDiskFreeSpaceExW")))?;
332
333 let mut label = vec![0u16; 261];
334 let mut fs = vec![0u16; 261];
335 let mut serial = 0u32;
336 let mut max_comp_len = 0u32;
337 let mut flags = 0u32;
338
339 let _ = unsafe {
340 GetVolumeInformationW(
341 windows::core::PCWSTR(root_wide.as_ptr()),
342 Some(&mut label),
343 Some(&mut serial),
344 Some(&mut max_comp_len),
345 Some(&mut flags),
346 Some(&mut fs),
347 )
348 };
349
350 let disk = LogicalDiskInfo {
351 root,
352 volume_label: first_nul_terminated(&label),
353 file_system: first_nul_terminated(&fs),
354 total_bytes,
355 free_bytes_available,
356 total_free_bytes,
357 };
358
359 if filter(&disk) {
360 out_disks.push(disk);
361 }
362 }
363
364 Ok(out_disks.len())
365}
366
367pub fn physical_disks() -> Result<Vec<PhysicalDiskInfo>> {
372 let mut out_disks = Vec::with_capacity(8);
373 physical_disks_with_filter(&mut out_disks, |_| true)?;
374 Ok(out_disks)
375}
376
377pub fn physical_disks_with_buffer(out_disks: &mut Vec<PhysicalDiskInfo>) -> Result<usize> {
379 physical_disks_with_filter(out_disks, |_| true)
380}
381
382pub fn physical_disks_with_filter<F>(
384 out_disks: &mut Vec<PhysicalDiskInfo>,
385 filter: F,
386) -> Result<usize>
387where
388 F: Fn(&PhysicalDiskInfo) -> bool,
389{
390 out_disks.clear();
391
392 for index in 0..64 {
393 let path = format!(r"\\.\PhysicalDrive{}", index);
394 let path_wide = to_utf16_nul(&path);
395
396 let handle = match unsafe {
397 CreateFileW(
398 PCWSTR::from_raw(path_wide.as_ptr()),
399 windows::Win32::Foundation::GENERIC_READ.0,
400 FILE_SHARE_MODE(FILE_SHARE_READ.0 | FILE_SHARE_WRITE.0),
401 None,
402 OPEN_EXISTING,
403 FILE_FLAGS_AND_ATTRIBUTES(0),
404 None,
405 )
406 } {
407 Ok(handle) => OwnedHandle::new(handle),
408 Err(_) => continue,
409 };
410
411 let mut length = GET_LENGTH_INFORMATION::default();
412 let mut bytes_returned = 0u32;
413
414 let ok = unsafe {
415 DeviceIoControl(
416 handle.raw(),
417 IOCTL_DISK_GET_LENGTH_INFO,
418 None,
419 0,
420 Some(std::ptr::addr_of_mut!(length) as *mut _),
421 std::mem::size_of::<GET_LENGTH_INFORMATION>() as u32,
422 Some(&mut bytes_returned),
423 None,
424 )
425 }
426 .is_ok();
427
428 if !ok {
429 continue;
430 }
431
432 let disk = PhysicalDiskInfo {
433 path,
434 size_bytes: length.Length.max(0) as u64,
435 };
436
437 if filter(&disk) {
438 out_disks.push(disk);
439 }
440 }
441
442 Ok(out_disks.len())
443}
444
445pub fn network_interfaces() -> Result<Vec<NetworkInterfaceInfo>> {
449 let mut out_interfaces = Vec::with_capacity(8);
450 network_interfaces_with_filter(&mut out_interfaces, |_| true)?;
451 Ok(out_interfaces)
452}
453
454pub fn network_interfaces_with_buffer(
456 out_interfaces: &mut Vec<NetworkInterfaceInfo>,
457) -> Result<usize> {
458 network_interfaces_with_filter(out_interfaces, |_| true)
459}
460
461pub fn network_interfaces_with_filter<F>(
463 out_interfaces: &mut Vec<NetworkInterfaceInfo>,
464 filter: F,
465) -> Result<usize>
466where
467 F: Fn(&NetworkInterfaceInfo) -> bool,
468{
469 out_interfaces.clear();
470 let mut buffer_len = 16_384u32;
471 let mut work_buffer = vec![0u8; buffer_len as usize];
472
473 let flags = GET_ADAPTERS_ADDRESSES_FLAGS(GAA_FLAG_INCLUDE_PREFIX.0);
474 let mut status = unsafe {
475 GetAdaptersAddresses(
476 AF_UNSPEC.0 as u32,
477 flags,
478 None,
479 Some(work_buffer.as_mut_ptr() as *mut IP_ADAPTER_ADDRESSES_LH),
480 &mut buffer_len,
481 )
482 };
483
484 if status == 111 {
486 work_buffer.resize(buffer_len as usize, 0);
487 status = unsafe {
488 GetAdaptersAddresses(
489 AF_UNSPEC.0 as u32,
490 flags,
491 None,
492 Some(work_buffer.as_mut_ptr() as *mut IP_ADAPTER_ADDRESSES_LH),
493 &mut buffer_len,
494 )
495 };
496 }
497
498 if status != 0 {
499 return Err(Error::Other(crate::error::OtherError::new(Cow::Owned(
500 format!("GetAdaptersAddresses failed with status {}", status),
501 ))));
502 }
503
504 let mut adapter_ptr = work_buffer.as_mut_ptr() as *mut IP_ADAPTER_ADDRESSES_LH;
505
506 while !adapter_ptr.is_null() {
507 let adapter = unsafe { &*adapter_ptr };
508
509 let name = pwstr_to_string(adapter.FriendlyName)
510 .or_else(|| pwstr_to_string(adapter.Description))
511 .unwrap_or_else(|| "unknown".to_string());
512
513 let mac_address = format_mac(
514 &adapter.PhysicalAddress,
515 adapter.PhysicalAddressLength as usize,
516 );
517
518 let addresses = collect_unicast_addresses(adapter.FirstUnicastAddress);
519
520 let iface = NetworkInterfaceInfo {
521 name,
522 mac_address,
523 addresses,
524 };
525
526 if filter(&iface) {
527 out_interfaces.push(iface);
528 }
529
530 adapter_ptr = adapter.Next;
531 }
532
533 Ok(out_interfaces.len())
534}
535
536pub fn users() -> Result<Vec<UserInfo>> {
538 let mut out_users = Vec::new();
539 users_with_filter(&mut out_users, |_| true)?;
540 Ok(out_users)
541}
542
543pub fn users_with_buffer(out_users: &mut Vec<UserInfo>) -> Result<usize> {
545 users_with_filter(out_users, |_| true)
546}
547
548pub fn users_with_filter<F>(out_users: &mut Vec<UserInfo>, filter: F) -> Result<usize>
550where
551 F: Fn(&UserInfo) -> bool,
552{
553 out_users.clear();
554
555 let mut dedup = HashSet::new();
556
557 if let Ok(username) = std::env::var("USERNAME") {
558 let username = username.trim().to_string();
559 if !username.is_empty() {
560 let user = UserInfo {
561 username: username.clone(),
562 sid: None,
563 source: "current_user",
564 };
565 if dedup.insert(username) && filter(&user) {
566 out_users.push(user);
567 }
568 }
569 }
570
571 let profile_list = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList";
572 if let Ok(key) = crate::registry::RegistryKey::open(Hive::LocalMachine, profile_list)
573 && let Ok(subkeys) = key.subkeys()
574 {
575 for sid in subkeys {
576 if let Ok(profile_path) = crate::registry::read_string(
577 Hive::LocalMachine,
578 &format!(r"{}\{}", profile_list, sid),
579 "ProfileImagePath",
580 ) && let Some(username) = username_from_profile_path(&profile_path)
581 {
582 let user = UserInfo {
583 username: username.clone(),
584 sid: Some(sid),
585 source: "profile_list",
586 };
587 if dedup.insert(username) && filter(&user) {
588 out_users.push(user);
589 }
590 }
591 }
592 }
593
594 Ok(out_users.len())
595}
596
597pub fn shutdown(options: &PowerActionOptions) -> Result<()> {
599 enable_shutdown_privilege()?;
600 shutdown_with_enabled_privilege(options)
601}
602
603pub fn shutdown_with_enabled_privilege(options: &PowerActionOptions) -> Result<()> {
605 execute_power_action(PowerAction::Shutdown, options)
606}
607
608pub fn restart(options: &PowerActionOptions) -> Result<()> {
610 enable_shutdown_privilege()?;
611 restart_with_enabled_privilege(options)
612}
613
614pub fn restart_with_enabled_privilege(options: &PowerActionOptions) -> Result<()> {
616 execute_power_action(PowerAction::Restart, options)
617}
618
619fn execute_power_action(action: PowerAction, options: &PowerActionOptions) -> Result<()> {
620 let mut comment = options.comment.as_ref().and_then(|value| {
621 let trimmed = value.trim();
622 (!trimmed.is_empty()).then(|| to_utf16_nul(trimmed))
623 });
624
625 let message = comment
626 .as_mut()
627 .map_or(PWSTR::null(), |value| PWSTR(value.as_mut_ptr()));
628
629 unsafe {
630 InitiateSystemShutdownExW(
631 PWSTR::null(),
632 message,
633 options.timeout_secs,
634 options.force_apps_closed,
635 matches!(action, PowerAction::Restart),
636 shutdown_reason(options),
637 )
638 }
639 .map_err(|e| map_power_api_error(e, action, "InitiateSystemShutdownExW"))
640}
641
642fn enable_shutdown_privilege() -> Result<()> {
643 let mut token = HANDLE(std::ptr::null_mut());
644 unsafe {
645 OpenProcessToken(
646 GetCurrentProcess(),
647 TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,
648 &mut token,
649 )
650 }
651 .map_err(|e| {
652 map_power_api_error(
653 e,
654 PowerAction::Shutdown,
655 "OpenProcessToken for SeShutdownPrivilege",
656 )
657 })?;
658
659 let token = OwnedHandle::new(token);
660
661 let mut token_privileges = TOKEN_PRIVILEGES {
662 PrivilegeCount: 1,
663 Privileges: Default::default(),
664 };
665
666 let privilege_name = to_utf16_nul("SeShutdownPrivilege");
667 unsafe {
668 LookupPrivilegeValueW(
669 None,
670 PCWSTR(privilege_name.as_ptr()),
671 &mut token_privileges.Privileges[0].Luid,
672 )
673 }
674 .map_err(|e| {
675 map_power_api_error(
676 e,
677 PowerAction::Shutdown,
678 "LookupPrivilegeValueW for SeShutdownPrivilege",
679 )
680 })?;
681
682 token_privileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
683
684 unsafe { AdjustTokenPrivileges(token.raw(), false, Some(&token_privileges), 0, None, None) }
685 .map_err(|e| {
686 map_power_api_error(
687 e,
688 PowerAction::Shutdown,
689 "AdjustTokenPrivileges for SeShutdownPrivilege",
690 )
691 })?;
692
693 let last_error = unsafe { GetLastError() };
694 if last_error == ERROR_NOT_ALL_ASSIGNED {
695 return Err(Error::AccessDenied(AccessDeniedError::with_reason(
696 "local machine",
697 "shutdown privilege",
698 "Privilege 'SeShutdownPrivilege' was not assigned to current token",
699 )));
700 }
701
702 Ok(())
703}
704
705fn shutdown_reason(options: &PowerActionOptions) -> SHUTDOWN_REASON {
706 let mut reason = options
707 .reason_code
708 .unwrap_or(SHTDN_REASON_MAJOR_OTHER.0 | SHTDN_REASON_MINOR_OTHER.0);
709
710 if options.planned {
711 reason |= SHTDN_REASON_FLAG_PLANNED.0;
712 }
713
714 SHUTDOWN_REASON(reason)
715}
716
717fn map_power_api_error(err: windows::core::Error, action: PowerAction, context: &str) -> Error {
718 let code = err.code().0;
719 let access_denied_hresult = HRESULT::from_win32(ERROR_ACCESS_DENIED.0).0;
720
721 if code == access_denied_hresult || code == ERROR_ACCESS_DENIED.0 as i32 {
722 let operation = match action {
723 PowerAction::Shutdown => "shutdown",
724 PowerAction::Restart => "restart",
725 };
726
727 return Error::AccessDenied(AccessDeniedError::with_reason(
728 "local machine",
729 operation,
730 Cow::Owned(format!("Access denied while calling {context}")),
731 ));
732 }
733
734 Error::WindowsApi(WindowsApiError::with_context(
735 err,
736 Cow::Owned(context.to_string()),
737 ))
738}
739
740fn section_error(section: SnapshotSection, err: Error) -> SnapshotSectionError {
741 SnapshotSectionError {
742 section,
743 message: Cow::Owned(err.to_string()),
744 }
745}
746
747fn derive_release_label(major: u32, minor: u32, build: u32) -> Option<String> {
748 match (major, minor) {
749 (10, 0) if build >= 22000 => Some("Windows 11".to_string()),
750 (10, 0) => Some("Windows 10".to_string()),
751 (6, 3) => Some("Windows 8.1".to_string()),
752 (6, 2) => Some("Windows 8".to_string()),
753 (6, 1) => Some("Windows 7".to_string()),
754 _ => None,
755 }
756}
757
758fn resolve_product_name(major: u32, minor: u32, build: u32) -> Option<String> {
759 let current_version_key = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion";
760 let registry_name =
761 registry::read_string(Hive::LocalMachine, current_version_key, "ProductName").ok();
762 let edition_id =
763 registry::read_string(Hive::LocalMachine, current_version_key, "EditionID").ok();
764 let installation_type =
765 registry::read_string(Hive::LocalMachine, current_version_key, "InstallationType").ok();
766
767 resolve_product_name_from_registry(
768 major,
769 minor,
770 build,
771 registry_name,
772 edition_id,
773 installation_type,
774 )
775}
776
777fn resolve_product_name_from_registry(
778 major: u32,
779 minor: u32,
780 build: u32,
781 registry_name: Option<String>,
782 edition_id: Option<String>,
783 installation_type: Option<String>,
784) -> Option<String> {
785 let is_server = is_server_installation(installation_type.as_deref(), edition_id.as_deref());
786
787 if major == 10 && minor == 0 && build >= 22000 {
789 if let Some(name) = registry_name {
790 if let Some(rest) = name.strip_prefix("Windows 10") {
791 return Some(format!("Windows 11{}", rest));
792 }
793 return Some(name);
794 }
795
796 if let Some(edition_id) = edition_id {
797 let edition = normalize_edition_id(&edition_id);
798 let family = if is_server {
799 "Windows Server"
800 } else {
801 "Windows 11"
802 };
803 let edition_suffix = if is_server {
804 edition.strip_prefix("Server ").unwrap_or(&edition)
805 } else {
806 &edition
807 };
808
809 if edition.is_empty() {
810 return Some(family.to_string());
811 }
812
813 return Some(format!("{} {}", family, edition_suffix));
814 }
815
816 return Some(if is_server {
817 "Windows Server".to_string()
818 } else {
819 "Windows 11".to_string()
820 });
821 }
822
823 registry_name
824}
825
826fn is_server_installation(installation_type: Option<&str>, edition_id: Option<&str>) -> bool {
827 installation_type
828 .map(str::trim)
829 .is_some_and(|value| value.eq_ignore_ascii_case("server"))
830 || edition_id
831 .map(str::trim)
832 .is_some_and(|value| value.starts_with("Server"))
833}
834
835fn normalize_edition_id(edition_id: &str) -> String {
836 match edition_id.trim() {
837 "Core" => "Home".to_string(),
838 "Professional" => "Pro".to_string(),
839 "EnterpriseS" => "Enterprise LTSC".to_string(),
840 "ServerStandard" => "Server Standard".to_string(),
841 "ServerDatacenter" => "Server Datacenter".to_string(),
842 "IoTEnterprise" => "IoT Enterprise".to_string(),
843 value => value.to_string(),
844 }
845}
846
847fn firmware_guid_from_smbios() -> Result<Option<String>> {
848 let provider = FIRMWARE_TABLE_PROVIDER(u32::from_le_bytes(*b"RSMB"));
849
850 let required = unsafe { GetSystemFirmwareTable(provider, 0, None) } as usize;
851
852 if required == 0 {
853 return Ok(None);
854 }
855
856 let mut work_buffer = vec![0u8; required];
857 let written =
858 unsafe { GetSystemFirmwareTable(provider, 0, Some(work_buffer.as_mut_slice())) } as usize;
859
860 if written == 0 {
861 return Err(Error::WindowsApi(WindowsApiError::with_context(
862 windows::core::Error::from_win32(),
863 "GetSystemFirmwareTable",
864 )));
865 }
866
867 if written > work_buffer.len() {
868 return Err(Error::Other(crate::error::OtherError::new(
869 "GetSystemFirmwareTable returned larger payload than buffer",
870 )));
871 }
872
873 parse_firmware_uuid_from_raw_smbios(&work_buffer)
874}
875
876fn parse_firmware_uuid_from_raw_smbios(work_buffer: &[u8]) -> Result<Option<String>> {
877 if work_buffer.len() < 8 {
880 return Ok(None);
881 }
882
883 let table_len = u32::from_le_bytes([
884 work_buffer[4],
885 work_buffer[5],
886 work_buffer[6],
887 work_buffer[7],
888 ]) as usize;
889 if work_buffer.len() < 8 + table_len {
890 return Ok(None);
891 }
892
893 let mut cursor = 8usize;
894 let end = 8 + table_len;
895
896 while cursor + 4 <= end {
897 let ty = work_buffer[cursor];
898 let len = work_buffer[cursor + 1] as usize;
899
900 if len < 4 || cursor + len > end {
901 break;
902 }
903
904 if ty == 1 && len >= 0x19 {
905 let uuid = &work_buffer[cursor + 8..cursor + 24];
906 if let Some(formatted) = format_smbios_uuid(uuid) {
907 return Ok(Some(formatted));
908 }
909 }
910
911 cursor += len;
912 while cursor + 1 < end {
913 if work_buffer[cursor] == 0 && work_buffer[cursor + 1] == 0 {
914 cursor += 2;
915 break;
916 }
917 cursor += 1;
918 }
919 }
920
921 Ok(None)
922}
923
924fn format_smbios_uuid(raw: &[u8]) -> Option<String> {
925 if raw.len() != 16 {
926 return None;
927 }
928
929 let d1 = u32::from_le_bytes([raw[0], raw[1], raw[2], raw[3]]);
931 let d2 = u16::from_le_bytes([raw[4], raw[5]]);
932 let d3 = u16::from_le_bytes([raw[6], raw[7]]);
933
934 Some(format!(
935 "{d1:08x}-{d2:04x}-{d3:04x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
936 raw[8], raw[9], raw[10], raw[11], raw[12], raw[13], raw[14], raw[15]
937 ))
938}
939
940fn parse_multi_sz(work_buffer: &[u16]) -> Vec<String> {
941 let mut out = Vec::new();
942 let mut start = 0usize;
943
944 for i in 0..work_buffer.len() {
945 if work_buffer[i] == 0 {
946 if i == start {
947 break;
948 }
949 out.push(String::from_utf16_lossy(&work_buffer[start..i]));
950 start = i + 1;
951 }
952 }
953
954 out
955}
956
957fn first_nul_terminated(work_buffer: &[u16]) -> Option<String> {
958 let end = work_buffer
959 .iter()
960 .position(|c| *c == 0)
961 .unwrap_or(work_buffer.len());
962 if end == 0 {
963 return None;
964 }
965 Some(String::from_utf16_lossy(&work_buffer[..end]))
966}
967
968fn username_from_profile_path(path: &str) -> Option<String> {
969 let trimmed = path.trim_end_matches(['\\', '/']);
970 let username = trimmed
971 .rsplit(['\\', '/'])
972 .next()
973 .map(str::trim)
974 .unwrap_or_default();
975
976 if username.is_empty() {
977 None
978 } else {
979 Some(username.to_string())
980 }
981}
982
983fn collect_unicast_addresses(
984 mut unicast_ptr: *mut windows::Win32::NetworkManagement::IpHelper::IP_ADAPTER_UNICAST_ADDRESS_LH,
985) -> Vec<String> {
986 let mut addresses = Vec::with_capacity(4);
987 let mut dedup = HashSet::new();
988
989 while !unicast_ptr.is_null() {
990 let unicast = unsafe { &*unicast_ptr };
991 if let Some(value) = sockaddr_to_ip_string(unicast.Address)
992 && dedup.insert(value.clone())
993 {
994 addresses.push(value);
995 }
996 unicast_ptr = unicast.Next;
997 }
998
999 addresses
1000}
1001
1002fn sockaddr_to_ip_string(
1003 socket_address: windows::Win32::Networking::WinSock::SOCKET_ADDRESS,
1004) -> Option<String> {
1005 if socket_address.lpSockaddr.is_null() {
1006 return None;
1007 }
1008
1009 let family = unsafe { (*socket_address.lpSockaddr).sa_family };
1010
1011 if family == AF_INET {
1012 let v4 = unsafe { &*(socket_address.lpSockaddr as *const SOCKADDR_IN) };
1013 let octets = unsafe {
1014 let b = v4.sin_addr.S_un.S_un_b;
1015 [b.s_b1, b.s_b2, b.s_b3, b.s_b4]
1016 };
1017 return Some(Ipv4Addr::from(octets).to_string());
1018 }
1019
1020 if family == AF_INET6 {
1021 let v6 = unsafe { &*(socket_address.lpSockaddr as *const SOCKADDR_IN6) };
1022 let bytes = unsafe { v6.sin6_addr.u.Byte };
1023 return Some(Ipv6Addr::from(bytes).to_string());
1024 }
1025
1026 None
1027}
1028
1029fn format_mac(bytes: &[u8], len: usize) -> Option<String> {
1030 if len == 0 || len > bytes.len() {
1031 return None;
1032 }
1033
1034 let mut out = String::new();
1035 for (i, b) in bytes[..len].iter().enumerate() {
1036 if i > 0 {
1037 out.push(':');
1038 }
1039 out.push_str(&format!("{b:02x}"));
1040 }
1041 Some(out)
1042}
1043
1044#[cfg(test)]
1045mod tests {
1046 use super::{
1047 PowerActionOptions, format_smbios_uuid, parse_multi_sz, resolve_product_name_from_registry,
1048 shutdown_reason, username_from_profile_path,
1049 };
1050 use windows::Win32::System::Shutdown::{
1051 SHTDN_REASON_FLAG_PLANNED, SHTDN_REASON_MAJOR_OTHER, SHTDN_REASON_MINOR_OTHER,
1052 };
1053
1054 #[test]
1055 fn parse_multi_sz_extracts_entries() {
1056 let data = [
1057 'C' as u16,
1058 ':' as u16,
1059 '\\' as u16,
1060 0,
1061 'D' as u16,
1062 ':' as u16,
1063 '\\' as u16,
1064 0,
1065 0,
1066 ];
1067 let drives = parse_multi_sz(&data);
1068 assert_eq!(drives, vec!["C:\\".to_string(), "D:\\".to_string()]);
1069 }
1070
1071 #[test]
1072 fn username_from_profile_path_parses_tail_component() {
1073 let value = username_from_profile_path(r"C:\\Users\\alice");
1074 assert_eq!(value.as_deref(), Some("alice"));
1075 }
1076
1077 #[test]
1078 fn format_smbios_uuid_formats_expected_shape() {
1079 let raw = [
1080 0x33, 0x22, 0x11, 0x00, 0x55, 0x44, 0x77, 0x66, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd,
1081 0xee, 0xff,
1082 ];
1083 let value = format_smbios_uuid(&raw).expect("uuid");
1084 assert_eq!(value, "00112233-4455-6677-8899-aabbccddeeff");
1085 }
1086
1087 #[test]
1088 fn resolve_product_name_preserves_server_sku_names() {
1089 let value = resolve_product_name_from_registry(
1090 10,
1091 0,
1092 26100,
1093 Some("Windows Server 2025 Standard".to_string()),
1094 Some("ServerStandard".to_string()),
1095 Some("Server".to_string()),
1096 );
1097
1098 assert_eq!(value.as_deref(), Some("Windows Server 2025 Standard"));
1099 }
1100
1101 #[test]
1102 fn resolve_product_name_synthesizes_server_family_from_server_installation() {
1103 let value = resolve_product_name_from_registry(
1104 10,
1105 0,
1106 26100,
1107 None,
1108 Some("ServerStandard".to_string()),
1109 Some("Server".to_string()),
1110 );
1111
1112 assert_eq!(value.as_deref(), Some("Windows Server Standard"));
1113 }
1114
1115 #[test]
1116 fn resolve_product_name_rewrites_windows_10_desktop_name_on_windows_11() {
1117 let value = resolve_product_name_from_registry(
1118 10,
1119 0,
1120 26100,
1121 Some("Windows 10 Pro".to_string()),
1122 Some("Professional".to_string()),
1123 Some("Client".to_string()),
1124 );
1125
1126 assert_eq!(value.as_deref(), Some("Windows 11 Pro"));
1127 }
1128
1129 #[test]
1130 fn shutdown_reason_uses_default_other_reason() {
1131 let options = PowerActionOptions::default();
1132 assert_eq!(
1133 shutdown_reason(&options).0,
1134 SHTDN_REASON_MAJOR_OTHER.0 | SHTDN_REASON_MINOR_OTHER.0
1135 );
1136 }
1137
1138 #[test]
1139 fn shutdown_reason_sets_planned_flag() {
1140 let options = PowerActionOptions {
1141 planned: true,
1142 ..Default::default()
1143 };
1144
1145 assert_eq!(
1146 shutdown_reason(&options).0,
1147 (SHTDN_REASON_MAJOR_OTHER.0 | SHTDN_REASON_MINOR_OTHER.0) | SHTDN_REASON_FLAG_PLANNED.0
1148 );
1149 }
1150
1151 #[test]
1152 fn shutdown_reason_preserves_custom_reason_code() {
1153 let options = PowerActionOptions {
1154 reason_code: Some(0x0004_0000),
1155 ..Default::default()
1156 };
1157
1158 assert_eq!(shutdown_reason(&options).0, 0x0004_0000);
1159 }
1160}