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