Skip to main content

windows_erg/system/
mod.rs

1//! Host system inventory (native Windows API and registry).
2//!
3//! This module intentionally avoids WMI and uses native APIs plus registry reads.
4
5mod 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
39/// Collect a host inventory snapshot.
40///
41/// The snapshot is resilient by design: section failures are recorded in
42/// `section_errors` and collection continues.
43pub 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
134/// Get hostname via native API with environment fallback.
135pub 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
157/// Read OS version and product name.
158pub 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
203/// Get machine and firmware GUID information.
204pub 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
214/// Read machine GUID from registry.
215pub 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
231/// Read BIOS info from registry-provided firmware descriptors.
232pub 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
261/// List logical disks.
262pub 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
268/// Fill caller-provided logical disk buffer.
269pub fn logical_disks_with_buffer(out_disks: &mut Vec<LogicalDiskInfo>) -> Result<usize> {
270    logical_disks_with_filter(out_disks, |_| true)
271}
272
273/// Fill caller-provided logical disk buffer with in-enumeration filtering.
274pub 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
355/// Enumerate physical disks.
356///
357/// Initial implementation uses a conservative probe and returns disks with known size
358/// when available. This can be expanded to richer metadata in follow-up phases.
359pub 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
365/// Fill caller-provided physical disk buffer.
366pub fn physical_disks_with_buffer(out_disks: &mut Vec<PhysicalDiskInfo>) -> Result<usize> {
367    physical_disks_with_filter(out_disks, |_| true)
368}
369
370/// Fill caller-provided physical disk buffer with in-enumeration filtering.
371pub 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
433/// Enumerate network interfaces.
434///
435/// Native adapter traversal (IpHelper) is added in a follow-up patch.
436pub 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
442/// Fill caller-provided network interface buffer.
443pub fn network_interfaces_with_buffer(
444    out_interfaces: &mut Vec<NetworkInterfaceInfo>,
445) -> Result<usize> {
446    network_interfaces_with_filter(out_interfaces, |_| true)
447}
448
449/// Fill caller-provided network interface buffer with in-enumeration filtering.
450pub 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    // ERROR_BUFFER_OVERFLOW
473    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
524/// Enumerate users from profile list and current process context.
525pub 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
531/// Fill caller-provided user buffer.
532pub fn users_with_buffer(out_users: &mut Vec<UserInfo>) -> Result<usize> {
533    users_with_filter(out_users, |_| true)
534}
535
536/// Fill caller-provided user buffer with in-enumeration filtering.
537pub 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    // Microsoft often leaves ProductName as "Windows 10 ..." on Windows 11.
633    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    // Raw SMBIOS data starts with: Used20CallingMethod (1), SMBIOSMajor (1), SMBIOSMinor (1),
723    // DmiRevision (1), Length (4), then SMBIOS table bytes.
724    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    // SMBIOS UUID uses mixed endianness for the first 3 fields.
775    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}