pub fn detect_logical_disks(include_fuse: bool) -> Vec<(String, u64, u64, String)> {
#[cfg(target_os = "linux")]
{
detect_logical_linux(include_fuse)
}
#[cfg(not(target_os = "linux"))]
{
let _ = include_fuse;
detect_logical_sysinfo()
}
}
#[cfg(target_os = "linux")]
fn is_skip_fs(fs_type: &str, include_fuse: bool) -> bool {
const SKIP: &[&str] = &[
"sysfs",
"proc",
"devtmpfs",
"tmpfs",
"devpts",
"cgroup",
"cgroup2",
"pstore",
"bpf",
"tracefs",
"debugfs",
"securityfs",
"hugetlbfs",
"mqueue",
"fusectl",
"rpc_pipefs",
"configfs",
"autofs",
"efivarfs",
"binfmt_misc",
"squashfs",
"overlay",
"ramfs",
"rootfs",
"nsfs",
"pipefs",
"sockfs",
"anon_inodefs",
"cpuset",
];
SKIP.contains(&fs_type) || (fs_type.starts_with("fuse.") && !include_fuse)
}
#[cfg(target_os = "linux")]
fn detect_logical_linux(include_fuse: bool) -> Vec<(String, u64, u64, String)> {
use std::collections::HashSet;
use std::ffi::CString;
let mounts = std::fs::read_to_string("/proc/mounts").unwrap_or_default();
let mut results = Vec::new();
let mut seen_devs: HashSet<String> = HashSet::new();
for line in mounts.lines() {
let parts: Vec<&str> = line.splitn(4, ' ').collect();
if parts.len() < 3 {
continue;
}
let device = parts[0];
let mount_point = parts[1];
let fs_type = parts[2];
if is_skip_fs(fs_type, include_fuse) {
continue;
}
if device.starts_with('/') && !seen_devs.insert(device.to_string()) {
continue;
}
let Ok(mp_c) = CString::new(mount_point) else {
continue;
};
let mut stat: libc::statvfs = unsafe { std::mem::zeroed() };
if unsafe { libc::statvfs(mp_c.as_ptr(), &mut stat) } != 0 {
continue;
}
let total = (stat.f_blocks as u64).saturating_mul(stat.f_frsize as u64);
let avail = (stat.f_bavail as u64).saturating_mul(stat.f_frsize as u64);
if total == 0 {
continue;
}
results.push((mount_point.to_string(), total, avail, fs_type.to_string()));
}
results
}
#[cfg(not(target_os = "linux"))]
fn detect_logical_sysinfo() -> Vec<(String, u64, u64, String)> {
use sysinfo::Disks;
Disks::new_with_refreshed_list()
.iter()
.filter(|d| d.total_space() > 0)
.map(|d| {
(
d.mount_point().to_string_lossy().to_string(),
d.total_space(),
d.available_space(),
d.file_system().to_string_lossy().to_string(),
)
})
.collect()
}
pub fn detect_physical_disks() -> Vec<String> {
#[cfg(target_os = "linux")]
return detect_linux();
#[cfg(target_os = "macos")]
return detect_macos();
#[cfg(target_os = "windows")]
return detect_windows();
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
return Vec::new();
}
#[cfg(target_os = "linux")]
fn detect_linux() -> Vec<String> {
use std::fs;
let Ok(entries) = fs::read_dir("/sys/class/block") else {
return Vec::new();
};
let mut disks = Vec::new();
for entry in entries.flatten() {
let name = entry.file_name();
let name = name.to_string_lossy();
if name.starts_with("loop")
|| name.starts_with("ram")
|| name.starts_with("zram")
|| name.starts_with("dm-")
|| name.starts_with("md")
{
continue;
}
let dev_path = entry.path();
if dev_path.join("partition").exists() {
continue;
}
if !dev_path.join("queue").exists() {
continue;
}
let model = fs::read_to_string(dev_path.join("device/model"))
.map(|s| strip_embedded_size(s.trim()).to_string())
.unwrap_or_default();
let size_bytes = fs::read_to_string(dev_path.join("size"))
.ok()
.and_then(|s| s.trim().parse::<u64>().ok())
.map(|sectors| sectors * 512);
let rotational = fs::read_to_string(dev_path.join("queue/rotational"))
.map(|s| s.trim() == "1")
.unwrap_or(false);
let is_nvme = name.starts_with("nvme");
let kind = if is_nvme {
"NVMe SSD"
} else if rotational {
"HDD"
} else {
"SSD"
};
let size_str = size_bytes.map(format_size).unwrap_or_default();
let label = if model.is_empty() {
format!("{} [{}]", size_str, kind)
} else {
format!("{} {} [{}]", model.trim(), size_str, kind)
};
let label = label.trim().to_string();
if !label.is_empty() {
disks.push(label);
}
}
disks.sort();
disks
}
#[cfg(target_os = "macos")]
fn detect_macos() -> Vec<String> {
let output = std::process::Command::new("diskutil")
.args(["list", "-plist"])
.output();
let Ok(out) = output else {
return Vec::new();
};
if !out.status.success() {
return Vec::new();
}
let text = String::from_utf8_lossy(&out.stdout);
let mut disk_ids: Vec<String> = Vec::new();
let mut in_whole_disks = false;
for line in text.lines() {
let trimmed = line.trim();
if trimmed == "<key>WholeDisks</key>" {
in_whole_disks = true;
continue;
}
if in_whole_disks {
if trimmed == "</array>" {
break;
}
if let Some(inner) = trimmed
.strip_prefix("<string>")
.and_then(|s| s.strip_suffix("</string>"))
{
disk_ids.push(inner.to_string());
}
}
}
let mut disks = Vec::new();
for id in disk_ids {
if let Some(entry) = diskutil_info(&id) {
disks.push(entry);
}
}
disks
}
#[cfg(target_os = "macos")]
fn diskutil_info(disk_id: &str) -> Option<String> {
let output = std::process::Command::new("diskutil")
.args(["info", "-plist", disk_id])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let text = String::from_utf8_lossy(&output.stdout);
parse_diskutil_info_plist(&text)
}
#[cfg(target_os = "macos")]
pub fn parse_diskutil_info_plist(text: &str) -> Option<String> {
let mut model = String::new();
let mut size_bytes: Option<u64> = None;
let mut is_ssd = false;
let mut protocol = String::new();
let mut virtual_or_physical = String::new();
let mut last_key = String::new();
for line in text.lines() {
let trimmed = line.trim();
if let Some(key) = trimmed
.strip_prefix("<key>")
.and_then(|s| s.strip_suffix("</key>"))
{
last_key = key.to_string();
continue;
}
if let Some(val) = trimmed
.strip_prefix("<string>")
.and_then(|s| s.strip_suffix("</string>"))
{
match last_key.as_str() {
"MediaName" => {
if !val.is_empty() {
model = val.to_string();
}
}
"IORegistryEntryName" => {
if model.is_empty() && !val.is_empty() {
model = val.to_string();
}
}
"BusProtocol" => protocol = val.to_string(),
"VirtualOrPhysical" => virtual_or_physical = val.to_string(),
_ => {}
}
}
if let Some(val) = trimmed
.strip_prefix("<integer>")
.and_then(|s| s.strip_suffix("</integer>"))
{
if last_key == "TotalSize" {
size_bytes = val.parse().ok();
}
}
if trimmed == "<true/>" && last_key == "SolidState" {
is_ssd = true;
}
}
if virtual_or_physical == "Virtual" {
return None;
}
let kind =
if protocol.to_lowercase().contains("pcie") || protocol.to_lowercase().contains("nvme") {
"NVMe SSD"
} else if is_ssd {
"SSD"
} else {
"HDD"
};
let size_str = size_bytes.map(format_size).unwrap_or_default();
let label = if model.is_empty() {
format!("{} [{}]", size_str, kind)
} else {
format!("{} {} [{}]", model.trim(), size_str, kind)
};
Some(label.trim().to_string())
}
#[cfg(target_os = "linux")]
fn strip_embedded_size(model: &str) -> &str {
let bytes = model.as_bytes();
let mut i = bytes.len();
while i > 0 && bytes[i - 1] == b' ' {
i -= 1;
}
if i >= 2 {
let suffix = &bytes[i - 2..i];
if matches!(suffix, b"GB" | b"TB" | b"MB") {
i -= 2;
let digits_end = i;
while i > 0 && bytes[i - 1].is_ascii_digit() {
i -= 1;
}
if i < digits_end {
if i > 0 && bytes[i - 1] == b' ' {
i -= 1;
}
return model[..i].trim_end();
}
}
}
model
}
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
fn format_size(bytes: u64) -> String {
const TB: u64 = 1_000_000_000_000;
const GB: u64 = 1_000_000_000;
if bytes >= TB {
format!("{:.1} TB", bytes as f64 / TB as f64)
} else {
format!("{:.0} GB", bytes as f64 / GB as f64)
}
}
#[cfg(target_os = "windows")]
fn detect_windows() -> Vec<String> {
const MAX_DRIVES: u32 = 32;
(0..MAX_DRIVES)
.filter_map(win_ffi::query_physical_drive)
.collect()
}
#[cfg(target_os = "windows")]
fn format_disk_label(
model: &str,
size_bytes: Option<u64>,
bus_type: u32,
incurs_seek_penalty: Option<bool>,
) -> String {
let kind = if bus_type == win_ffi::BUS_TYPE_NVME {
"NVMe SSD"
} else {
match incurs_seek_penalty {
Some(true) => "HDD",
Some(false) | None => "SSD",
}
};
let name = model.trim();
let size_str = size_bytes.map(format_size).unwrap_or_default();
let label = if name.is_empty() {
format!("{} [{}]", size_str, kind)
} else {
format!("{} {} [{}]", name, size_str, kind)
};
label.trim().to_string()
}
#[cfg(target_os = "windows")]
fn combine_model(vendor: &str, product: &str) -> String {
let v = vendor.trim();
let p = product.trim();
if p.is_empty() {
return v.to_string();
}
if v.is_empty()
|| v.eq_ignore_ascii_case("ATA")
|| p.to_ascii_lowercase().contains(&v.to_ascii_lowercase())
{
p.to_string()
} else {
format!("{} {}", v, p)
}
}
#[cfg(target_os = "windows")]
mod win_ffi {
use super::{combine_model, format_disk_label};
use std::ffi::{c_void, OsStr};
use std::mem::size_of;
use std::os::windows::ffi::OsStrExt;
use std::ptr;
#[allow(clippy::upper_case_acronyms)]
type HANDLE = *mut c_void;
const INVALID_HANDLE_VALUE: HANDLE = -1isize as HANDLE;
const FILE_SHARE_READ: u32 = 0x0000_0001;
const FILE_SHARE_WRITE: u32 = 0x0000_0002;
const OPEN_EXISTING: u32 = 3;
const IOCTL_STORAGE_QUERY_PROPERTY: u32 = 0x002D_1400;
const IOCTL_DISK_GET_DRIVE_GEOMETRY_EX: u32 = 0x0007_00A0;
const STORAGE_DEVICE_PROPERTY: u32 = 0;
const STORAGE_DEVICE_SEEK_PENALTY_PROPERTY: u32 = 7;
const PROPERTY_STANDARD_QUERY: u32 = 0;
pub const BUS_TYPE_NVME: u32 = 17;
#[repr(C)]
struct StoragePropertyQuery {
property_id: u32,
query_type: u32,
additional_parameters: [u8; 1],
}
#[repr(C)]
struct StorageDeviceDescriptor {
version: u32,
size: u32,
device_type: u8,
device_type_modifier: u8,
removable_media: u8,
command_queueing: u8,
vendor_id_offset: u32,
product_id_offset: u32,
product_revision_offset: u32,
serial_number_offset: u32,
bus_type: u32,
raw_properties_length: u32,
raw_device_properties: [u8; 1],
}
#[repr(C)]
struct DeviceSeekPenaltyDescriptor {
version: u32,
size: u32,
incurs_seek_penalty: u8,
}
#[repr(C)]
struct DiskGeometry {
cylinders: i64,
media_type: u32,
tracks_per_cylinder: u32,
sectors_per_track: u32,
bytes_per_sector: u32,
}
#[repr(C)]
struct DiskGeometryEx {
geometry: DiskGeometry,
disk_size: i64,
data: [u8; 1],
}
extern "system" {
fn CreateFileW(
lp_file_name: *const u16,
dw_desired_access: u32,
dw_share_mode: u32,
lp_security_attributes: *mut c_void,
dw_creation_disposition: u32,
dw_flags_and_attributes: u32,
h_template_file: HANDLE,
) -> HANDLE;
fn DeviceIoControl(
h_device: HANDLE,
dw_io_control_code: u32,
lp_in_buffer: *const c_void,
n_in_buffer_size: u32,
lp_out_buffer: *mut c_void,
n_out_buffer_size: u32,
lp_bytes_returned: *mut u32,
lp_overlapped: *mut c_void,
) -> i32;
fn CloseHandle(h_object: HANDLE) -> i32;
}
pub fn query_physical_drive(index: u32) -> Option<String> {
let path = format!(r"\\.\PhysicalDrive{index}");
let path_w: Vec<u16> = OsStr::new(&path).encode_wide().chain(Some(0)).collect();
let handle = unsafe {
CreateFileW(
path_w.as_ptr(),
0,
FILE_SHARE_READ | FILE_SHARE_WRITE,
ptr::null_mut(),
OPEN_EXISTING,
0,
ptr::null_mut(),
)
};
if handle == INVALID_HANDLE_VALUE || handle.is_null() {
return None;
}
let descriptor = query_device_descriptor(handle);
let result = descriptor.map(|(bus_type, model)| {
let size = query_disk_size(handle);
let seek = query_seek_penalty(handle);
format_disk_label(&model, size, bus_type, seek)
});
unsafe {
CloseHandle(handle);
}
result
}
fn read_ansi_at(buf: &[u8], offset: usize) -> String {
if offset == 0 || offset >= buf.len() {
return String::new();
}
let bytes = &buf[offset..];
let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
String::from_utf8_lossy(&bytes[..end]).trim().to_string()
}
fn query_device_descriptor(handle: HANDLE) -> Option<(u32, String)> {
let query = StoragePropertyQuery {
property_id: STORAGE_DEVICE_PROPERTY,
query_type: PROPERTY_STANDARD_QUERY,
additional_parameters: [0; 1],
};
let mut buf = [0u8; 1024];
let mut returned: u32 = 0;
let ok = unsafe {
DeviceIoControl(
handle,
IOCTL_STORAGE_QUERY_PROPERTY,
&query as *const _ as *const c_void,
size_of::<StoragePropertyQuery>() as u32,
buf.as_mut_ptr() as *mut c_void,
buf.len() as u32,
&mut returned,
ptr::null_mut(),
)
};
if ok == 0 || (returned as usize) < size_of::<StorageDeviceDescriptor>() {
return None;
}
let desc = unsafe { &*(buf.as_ptr() as *const StorageDeviceDescriptor) };
let bus_type = desc.bus_type;
let vendor = read_ansi_at(&buf, desc.vendor_id_offset as usize);
let product = read_ansi_at(&buf, desc.product_id_offset as usize);
Some((bus_type, combine_model(&vendor, &product)))
}
fn query_disk_size(handle: HANDLE) -> Option<u64> {
let mut geo = DiskGeometryEx {
geometry: DiskGeometry {
cylinders: 0,
media_type: 0,
tracks_per_cylinder: 0,
sectors_per_track: 0,
bytes_per_sector: 0,
},
disk_size: 0,
data: [0; 1],
};
let mut returned: u32 = 0;
let ok = unsafe {
DeviceIoControl(
handle,
IOCTL_DISK_GET_DRIVE_GEOMETRY_EX,
ptr::null(),
0,
&mut geo as *mut _ as *mut c_void,
size_of::<DiskGeometryEx>() as u32,
&mut returned,
ptr::null_mut(),
)
};
if ok == 0 || geo.disk_size <= 0 {
None
} else {
Some(geo.disk_size as u64)
}
}
fn query_seek_penalty(handle: HANDLE) -> Option<bool> {
let query = StoragePropertyQuery {
property_id: STORAGE_DEVICE_SEEK_PENALTY_PROPERTY,
query_type: PROPERTY_STANDARD_QUERY,
additional_parameters: [0; 1],
};
let mut desc = DeviceSeekPenaltyDescriptor {
version: 0,
size: 0,
incurs_seek_penalty: 0,
};
let mut returned: u32 = 0;
let ok = unsafe {
DeviceIoControl(
handle,
IOCTL_STORAGE_QUERY_PROPERTY,
&query as *const _ as *const c_void,
size_of::<StoragePropertyQuery>() as u32,
&mut desc as *mut _ as *mut c_void,
size_of::<DeviceSeekPenaltyDescriptor>() as u32,
&mut returned,
ptr::null_mut(),
)
};
if ok == 0 || (returned as usize) < size_of::<DeviceSeekPenaltyDescriptor>() {
None
} else {
Some(desc.incurs_seek_penalty != 0)
}
}
#[cfg(test)]
mod layout {
use std::mem::{offset_of, size_of};
#[test]
fn ffi_struct_layout() {
assert_eq!(size_of::<super::StoragePropertyQuery>(), 12);
assert_eq!(size_of::<super::StorageDeviceDescriptor>(), 40);
assert_eq!(
offset_of!(super::StorageDeviceDescriptor, vendor_id_offset),
12
);
assert_eq!(
offset_of!(super::StorageDeviceDescriptor, product_id_offset),
16
);
assert_eq!(offset_of!(super::StorageDeviceDescriptor, bus_type), 28);
assert_eq!(size_of::<super::DeviceSeekPenaltyDescriptor>(), 12);
assert_eq!(size_of::<super::DiskGeometryEx>(), 40);
assert_eq!(offset_of!(super::DiskGeometryEx, disk_size), 24);
}
}
}
#[cfg(test)]
mod tests {
#[cfg(any(target_os = "linux", target_os = "macos"))]
use super::format_size;
#[cfg(target_os = "linux")]
use super::{is_skip_fs, strip_embedded_size};
#[cfg(target_os = "linux")]
#[test]
fn test_is_skip_fs_pseudo() {
assert!(is_skip_fs("sysfs", false));
assert!(is_skip_fs("proc", false));
assert!(is_skip_fs("tmpfs", false));
assert!(is_skip_fs("fusectl", false)); assert!(is_skip_fs("fusectl", true)); }
#[cfg(target_os = "linux")]
#[test]
fn test_is_skip_fs_fuse_excluded_by_default() {
assert!(is_skip_fs("fuse.gvfsd-fuse", false));
assert!(is_skip_fs("fuse.sshfs", false));
assert!(is_skip_fs("fuse.cryfs", false));
}
#[cfg(target_os = "linux")]
#[test]
fn test_is_skip_fs_fuse_included_in_full() {
assert!(!is_skip_fs("fuse.gvfsd-fuse", true));
assert!(!is_skip_fs("fuse.sshfs", true));
assert!(!is_skip_fs("fuse.cryfs", true));
}
#[cfg(target_os = "linux")]
#[test]
fn test_is_skip_fs_real_fs() {
assert!(!is_skip_fs("ext4", false));
assert!(!is_skip_fs("btrfs", false));
assert!(!is_skip_fs("vfat", false));
}
#[cfg(target_os = "linux")]
#[test]
fn test_strip_embedded_size() {
assert_eq!(
strip_embedded_size("BC901 NVMe SK hynix 1024GB"),
"BC901 NVMe SK hynix"
);
assert_eq!(
strip_embedded_size("Samsung SSD 970 EVO 500GB"),
"Samsung SSD 970 EVO"
);
assert_eq!(strip_embedded_size("WD Blue 2TB"), "WD Blue");
assert_eq!(strip_embedded_size("CT500MX500SSD1"), "CT500MX500SSD1"); assert_eq!(
strip_embedded_size("SAMSUNG MZQL23T8HCLS"),
"SAMSUNG MZQL23T8HCLS"
); assert_eq!(strip_embedded_size("Some Drive 256GB"), "Some Drive");
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn test_format_size_gb() {
assert_eq!(format_size(512_110_190_592), "512 GB");
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn test_format_size_tb() {
assert_eq!(format_size(1_000_204_886_016), "1.0 TB");
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn test_format_size_2tb() {
assert_eq!(format_size(2_000_398_934_016), "2.0 TB");
}
#[cfg(target_os = "macos")]
#[test]
fn test_parse_diskutil_info_plist_apple_silicon() {
let plist = r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BusProtocol</key>
<string>Apple Fabric</string>
<key>IORegistryEntryName</key>
<string>APPLE SSD AP1024Z Media</string>
<key>MediaName</key>
<string>APPLE SSD AP1024Z</string>
<key>SolidState</key>
<true/>
<key>TotalSize</key>
<integer>1000555581440</integer>
<key>VirtualOrPhysical</key>
<string>Unknown</string>
</dict>
</plist>"#;
let result = super::parse_diskutil_info_plist(plist);
assert_eq!(result, Some("APPLE SSD AP1024Z 1.0 TB [SSD]".to_string()));
}
#[cfg(target_os = "macos")]
#[test]
fn test_parse_diskutil_info_plist_nvme() {
let plist = r#"<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
<key>BusProtocol</key>
<string>PCIe</string>
<key>MediaName</key>
<string>Samsung SSD 990 Pro</string>
<key>SolidState</key>
<true/>
<key>TotalSize</key>
<integer>2000398934016</integer>
<key>VirtualOrPhysical</key>
<string>Physical</string>
</dict>
</plist>"#;
let result = super::parse_diskutil_info_plist(plist);
assert_eq!(
result,
Some("Samsung SSD 990 Pro 2.0 TB [NVMe SSD]".to_string())
);
}
#[cfg(target_os = "macos")]
#[test]
fn test_parse_diskutil_info_plist_virtual_skipped() {
let plist = r#"<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
<key>MediaName</key>
<string>APFS Container Disk</string>
<key>TotalSize</key>
<integer>500000000000</integer>
<key>VirtualOrPhysical</key>
<string>Virtual</string>
</dict>
</plist>"#;
let result = super::parse_diskutil_info_plist(plist);
assert_eq!(result, None);
}
#[cfg(target_os = "windows")]
use super::win_ffi::BUS_TYPE_NVME;
#[cfg(target_os = "windows")]
use super::{combine_model, format_disk_label};
#[cfg(target_os = "windows")]
#[test]
fn test_format_disk_label_nvme() {
let label = format_disk_label(
"Samsung SSD 980 Pro",
Some(1_000_204_886_016),
BUS_TYPE_NVME,
Some(false),
);
assert_eq!(label, "Samsung SSD 980 Pro 1.0 TB [NVMe SSD]");
}
#[cfg(target_os = "windows")]
#[test]
fn test_format_disk_label_hdd() {
let label = format_disk_label("WD Blue", Some(2_000_398_934_016), 11, Some(true));
assert_eq!(label, "WD Blue 2.0 TB [HDD]");
}
#[cfg(target_os = "windows")]
#[test]
fn test_format_disk_label_sata_ssd() {
let label = format_disk_label(
"Crucial CT500MX500SSD1",
Some(500_107_862_016),
11,
Some(false),
);
assert_eq!(label, "Crucial CT500MX500SSD1 500 GB [SSD]");
}
#[cfg(target_os = "windows")]
#[test]
fn test_format_disk_label_unknown_seek_penalty_defaults_to_ssd() {
let label = format_disk_label("Some eMMC", Some(64_000_000_000), 13, None);
assert_eq!(label, "Some eMMC 64 GB [SSD]");
}
#[cfg(target_os = "windows")]
#[test]
fn test_format_disk_label_empty_model() {
let label = format_disk_label("", Some(500_107_862_016), 11, Some(false));
assert_eq!(label, "500 GB [SSD]");
}
#[cfg(target_os = "windows")]
#[test]
fn test_combine_model_generic_ata_vendor_suppressed() {
assert_eq!(
combine_model("ATA", "Samsung SSD 860 EVO"),
"Samsung SSD 860 EVO"
);
}
#[cfg(target_os = "windows")]
#[test]
fn test_combine_model_empty_vendor() {
assert_eq!(
combine_model("", "Samsung SSD 980 Pro"),
"Samsung SSD 980 Pro"
);
}
#[cfg(target_os = "windows")]
#[test]
fn test_combine_model_vendor_already_in_product() {
assert_eq!(
combine_model("Samsung", "Samsung SSD 980 Pro"),
"Samsung SSD 980 Pro"
);
}
#[cfg(target_os = "windows")]
#[test]
fn test_combine_model_distinct_vendor_prepended() {
assert_eq!(combine_model("Kingston", "A400 SSD"), "Kingston A400 SSD");
}
#[cfg(target_os = "windows")]
#[test]
fn test_combine_model_empty_product_falls_back_to_vendor() {
assert_eq!(combine_model("SomeVendor", ""), "SomeVendor");
}
}