1pub fn detect_logical_disks(include_fuse: bool) -> Vec<(String, u64, u64, String)> {
15 #[cfg(target_os = "linux")]
16 {
17 detect_logical_linux(include_fuse)
18 }
19
20 #[cfg(not(target_os = "linux"))]
21 {
22 let _ = include_fuse;
23 detect_logical_sysinfo()
24 }
25}
26
27#[cfg(target_os = "linux")]
29fn is_skip_fs(fs_type: &str, include_fuse: bool) -> bool {
30 const SKIP: &[&str] = &[
31 "sysfs",
32 "proc",
33 "devtmpfs",
34 "tmpfs",
35 "devpts",
36 "cgroup",
37 "cgroup2",
38 "pstore",
39 "bpf",
40 "tracefs",
41 "debugfs",
42 "securityfs",
43 "hugetlbfs",
44 "mqueue",
45 "fusectl",
46 "rpc_pipefs",
47 "configfs",
48 "autofs",
49 "efivarfs",
50 "binfmt_misc",
51 "squashfs",
52 "overlay",
53 "ramfs",
54 "rootfs",
55 "nsfs",
56 "pipefs",
57 "sockfs",
58 "anon_inodefs",
59 "cpuset",
60 ];
61 SKIP.contains(&fs_type) || (fs_type.starts_with("fuse.") && !include_fuse)
63}
64
65#[cfg(target_os = "linux")]
66fn detect_logical_linux(include_fuse: bool) -> Vec<(String, u64, u64, String)> {
67 use std::collections::HashSet;
68 use std::ffi::CString;
69
70 let mounts = std::fs::read_to_string("/proc/mounts").unwrap_or_default();
71 let mut results = Vec::new();
72 let mut seen_devs: HashSet<String> = HashSet::new();
73
74 for line in mounts.lines() {
75 let parts: Vec<&str> = line.splitn(4, ' ').collect();
76 if parts.len() < 3 {
77 continue;
78 }
79 let device = parts[0];
80 let mount_point = parts[1];
81 let fs_type = parts[2];
82
83 if is_skip_fs(fs_type, include_fuse) {
84 continue;
85 }
86
87 if device.starts_with('/') && !seen_devs.insert(device.to_string()) {
89 continue;
90 }
91
92 let Ok(mp_c) = CString::new(mount_point) else {
93 continue;
94 };
95
96 let mut stat: libc::statvfs = unsafe { std::mem::zeroed() };
97 if unsafe { libc::statvfs(mp_c.as_ptr(), &mut stat) } != 0 {
98 continue;
99 }
100
101 let total = (stat.f_blocks as u64).saturating_mul(stat.f_frsize as u64);
102 let avail = (stat.f_bavail as u64).saturating_mul(stat.f_frsize as u64);
103
104 if total == 0 {
105 continue;
106 }
107
108 results.push((mount_point.to_string(), total, avail, fs_type.to_string()));
109 }
110
111 results
112}
113
114#[cfg(not(target_os = "linux"))]
115fn detect_logical_sysinfo() -> Vec<(String, u64, u64, String)> {
116 use sysinfo::Disks;
117 Disks::new_with_refreshed_list()
118 .iter()
119 .filter(|d| d.total_space() > 0)
120 .map(|d| {
121 (
122 d.mount_point().to_string_lossy().to_string(),
123 d.total_space(),
124 d.available_space(),
125 d.file_system().to_string_lossy().to_string(),
126 )
127 })
128 .collect()
129}
130
131pub fn detect_physical_disks() -> Vec<String> {
132 #[cfg(target_os = "linux")]
133 return detect_linux();
134
135 #[cfg(target_os = "macos")]
136 return detect_macos();
137
138 #[cfg(target_os = "windows")]
139 return detect_windows();
140
141 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
142 return Vec::new();
143}
144
145#[cfg(target_os = "linux")]
151pub(crate) fn is_virtual_block_name(name: &str) -> bool {
152 name.starts_with("loop")
153 || name.starts_with("ram")
154 || name.starts_with("zram")
155 || name.starts_with("dm-")
156 || name.starts_with("md")
157}
158
159#[cfg(target_os = "linux")]
160fn detect_linux() -> Vec<String> {
161 use std::fs;
162
163 let Ok(entries) = fs::read_dir("/sys/class/block") else {
164 return Vec::new();
165 };
166
167 let mut disks = Vec::new();
168
169 for entry in entries.flatten() {
170 let name = entry.file_name();
171 let name = name.to_string_lossy();
172
173 if is_virtual_block_name(&name) {
175 continue;
176 }
177
178 let dev_path = entry.path();
179
180 if dev_path.join("partition").exists() {
182 continue;
183 }
184
185 if !dev_path.join("queue").exists() {
187 continue;
188 }
189
190 let model = fs::read_to_string(dev_path.join("device/model"))
191 .map(|s| strip_embedded_size(s.trim()).to_string())
192 .unwrap_or_default();
193
194 let size_bytes = fs::read_to_string(dev_path.join("size"))
196 .ok()
197 .and_then(|s| s.trim().parse::<u64>().ok())
198 .map(|sectors| sectors * 512);
199
200 let rotational = fs::read_to_string(dev_path.join("queue/rotational"))
201 .map(|s| s.trim() == "1")
202 .unwrap_or(false);
203
204 let is_nvme = name.starts_with("nvme");
205
206 let kind = if is_nvme {
207 "NVMe SSD"
208 } else if rotational {
209 "HDD"
210 } else {
211 "SSD"
212 };
213
214 let size_str = size_bytes.map(format_size).unwrap_or_default();
215
216 let label = if model.is_empty() {
217 format!("{} [{}]", size_str, kind)
218 } else {
219 format!("{} {} [{}]", model.trim(), size_str, kind)
220 };
221
222 let label = label.trim().to_string();
223 if !label.is_empty() {
224 disks.push(label);
225 }
226 }
227
228 disks.sort();
229 disks
230}
231
232#[cfg(target_os = "macos")]
233fn detect_macos() -> Vec<String> {
234 let output = std::process::Command::new("diskutil")
237 .args(["list", "-plist"])
238 .output();
239
240 let Ok(out) = output else {
241 return Vec::new();
242 };
243 if !out.status.success() {
244 return Vec::new();
245 }
246
247 let text = String::from_utf8_lossy(&out.stdout);
249
250 let mut disk_ids: Vec<String> = Vec::new();
253 let mut in_whole_disks = false;
254 for line in text.lines() {
255 let trimmed = line.trim();
256 if trimmed == "<key>WholeDisks</key>" {
257 in_whole_disks = true;
258 continue;
259 }
260 if in_whole_disks {
261 if trimmed == "</array>" {
262 break;
263 }
264 if let Some(inner) = trimmed
265 .strip_prefix("<string>")
266 .and_then(|s| s.strip_suffix("</string>"))
267 {
268 disk_ids.push(inner.to_string());
269 }
270 }
271 }
272
273 let mut disks = Vec::new();
274 for id in disk_ids {
275 if let Some(entry) = diskutil_info(&id) {
276 disks.push(entry);
277 }
278 }
279 disks
280}
281
282#[cfg(target_os = "macos")]
283fn diskutil_info(disk_id: &str) -> Option<String> {
284 let output = std::process::Command::new("diskutil")
285 .args(["info", "-plist", disk_id])
286 .output()
287 .ok()?;
288
289 if !output.status.success() {
290 return None;
291 }
292
293 let text = String::from_utf8_lossy(&output.stdout);
294 parse_diskutil_info_plist(&text)
295}
296
297#[cfg(target_os = "macos")]
300pub fn parse_diskutil_info_plist(text: &str) -> Option<String> {
301 let mut model = String::new();
302 let mut size_bytes: Option<u64> = None;
303 let mut is_ssd = false;
304 let mut protocol = String::new();
305 let mut virtual_or_physical = String::new();
306
307 let mut last_key = String::new();
308 for line in text.lines() {
309 let trimmed = line.trim();
310 if let Some(key) = trimmed
311 .strip_prefix("<key>")
312 .and_then(|s| s.strip_suffix("</key>"))
313 {
314 last_key = key.to_string();
315 continue;
316 }
317 if let Some(val) = trimmed
318 .strip_prefix("<string>")
319 .and_then(|s| s.strip_suffix("</string>"))
320 {
321 match last_key.as_str() {
322 "MediaName" => {
325 if !val.is_empty() {
326 model = val.to_string();
327 }
328 }
329 "IORegistryEntryName" => {
330 if model.is_empty() && !val.is_empty() {
331 model = val.to_string();
332 }
333 }
334 "BusProtocol" => protocol = val.to_string(),
335 "VirtualOrPhysical" => virtual_or_physical = val.to_string(),
336 _ => {}
337 }
338 }
339 if let Some(val) = trimmed
340 .strip_prefix("<integer>")
341 .and_then(|s| s.strip_suffix("</integer>"))
342 {
343 if last_key == "TotalSize" {
344 size_bytes = val.parse().ok();
345 }
346 }
347 if trimmed == "<true/>" && last_key == "SolidState" {
348 is_ssd = true;
349 }
350 }
351
352 if virtual_or_physical == "Virtual" {
354 return None;
355 }
356
357 let kind =
358 if protocol.to_lowercase().contains("pcie") || protocol.to_lowercase().contains("nvme") {
359 "NVMe SSD"
360 } else if is_ssd {
361 "SSD"
362 } else {
363 "HDD"
364 };
365
366 let size_str = size_bytes.map(format_size).unwrap_or_default();
367
368 let label = if model.is_empty() {
369 format!("{} [{}]", size_str, kind)
370 } else {
371 format!("{} {} [{}]", model.trim(), size_str, kind)
372 };
373
374 Some(label.trim().to_string())
375}
376
377#[cfg(target_os = "linux")]
380fn strip_embedded_size(model: &str) -> &str {
381 let bytes = model.as_bytes();
382 let mut i = bytes.len();
384 while i > 0 && bytes[i - 1] == b' ' {
386 i -= 1;
387 }
388 if i >= 2 {
390 let suffix = &bytes[i - 2..i];
391 if matches!(suffix, b"GB" | b"TB" | b"MB") {
392 i -= 2;
393 let digits_end = i;
395 while i > 0 && bytes[i - 1].is_ascii_digit() {
396 i -= 1;
397 }
398 if i < digits_end {
399 if i > 0 && bytes[i - 1] == b' ' {
401 i -= 1;
402 }
403 return model[..i].trim_end();
404 }
405 }
406 }
407 model
408}
409
410#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
411fn format_size(bytes: u64) -> String {
412 const TB: u64 = 1_000_000_000_000;
413 const GB: u64 = 1_000_000_000;
414 if bytes >= TB {
415 format!("{:.1} TB", bytes as f64 / TB as f64)
416 } else {
417 format!("{:.0} GB", bytes as f64 / GB as f64)
418 }
419}
420
421#[cfg(target_os = "windows")]
428fn detect_windows() -> Vec<String> {
429 const MAX_DRIVES: u32 = 32;
434 (0..MAX_DRIVES)
435 .filter_map(win_ffi::query_physical_drive)
436 .collect()
437}
438
439#[cfg(target_os = "windows")]
446fn format_disk_label(
447 model: &str,
448 size_bytes: Option<u64>,
449 bus_type: u32,
450 incurs_seek_penalty: Option<bool>,
451) -> String {
452 let kind = if bus_type == win_ffi::BUS_TYPE_NVME {
453 "NVMe SSD"
454 } else {
455 match incurs_seek_penalty {
456 Some(true) => "HDD",
457 Some(false) | None => "SSD",
460 }
461 };
462
463 let name = model.trim();
464 let size_str = size_bytes.map(format_size).unwrap_or_default();
465 let label = if name.is_empty() {
466 format!("{} [{}]", size_str, kind)
467 } else {
468 format!("{} {} [{}]", name, size_str, kind)
469 };
470 label.trim().to_string()
471}
472
473#[cfg(target_os = "windows")]
481fn combine_model(vendor: &str, product: &str) -> String {
482 let v = vendor.trim();
483 let p = product.trim();
484 if p.is_empty() {
485 return v.to_string();
486 }
487 if v.is_empty()
488 || v.eq_ignore_ascii_case("ATA")
489 || p.to_ascii_lowercase().contains(&v.to_ascii_lowercase())
490 {
491 p.to_string()
492 } else {
493 format!("{} {}", v, p)
494 }
495}
496
497#[cfg(target_os = "windows")]
502mod win_ffi {
503 use super::{combine_model, format_disk_label};
504 use std::ffi::{c_void, OsStr};
505 use std::mem::size_of;
506 use std::os::windows::ffi::OsStrExt;
507 use std::ptr;
508
509 #[allow(clippy::upper_case_acronyms)]
510 type HANDLE = *mut c_void;
511 const INVALID_HANDLE_VALUE: HANDLE = -1isize as HANDLE;
512 const FILE_SHARE_READ: u32 = 0x0000_0001;
513 const FILE_SHARE_WRITE: u32 = 0x0000_0002;
514 const OPEN_EXISTING: u32 = 3;
515
516 const IOCTL_STORAGE_QUERY_PROPERTY: u32 = 0x002D_1400;
519 const IOCTL_DISK_GET_DRIVE_GEOMETRY_EX: u32 = 0x0007_00A0;
520
521 const STORAGE_DEVICE_PROPERTY: u32 = 0;
523 const STORAGE_DEVICE_SEEK_PENALTY_PROPERTY: u32 = 7;
524 const PROPERTY_STANDARD_QUERY: u32 = 0;
526
527 pub const BUS_TYPE_NVME: u32 = 17;
529
530 #[repr(C)]
531 struct StoragePropertyQuery {
532 property_id: u32,
533 query_type: u32,
534 additional_parameters: [u8; 1],
535 }
536
537 #[repr(C)]
538 struct StorageDeviceDescriptor {
539 version: u32,
540 size: u32,
541 device_type: u8,
542 device_type_modifier: u8,
543 removable_media: u8,
544 command_queueing: u8,
545 vendor_id_offset: u32,
546 product_id_offset: u32,
547 product_revision_offset: u32,
548 serial_number_offset: u32,
549 bus_type: u32,
550 raw_properties_length: u32,
551 raw_device_properties: [u8; 1],
552 }
553
554 #[repr(C)]
555 struct DeviceSeekPenaltyDescriptor {
556 version: u32,
557 size: u32,
558 incurs_seek_penalty: u8,
559 }
560
561 #[repr(C)]
562 struct DiskGeometry {
563 cylinders: i64,
564 media_type: u32,
565 tracks_per_cylinder: u32,
566 sectors_per_track: u32,
567 bytes_per_sector: u32,
568 }
569
570 #[repr(C)]
571 struct DiskGeometryEx {
572 geometry: DiskGeometry,
573 disk_size: i64,
574 data: [u8; 1],
575 }
576
577 extern "system" {
578 fn CreateFileW(
579 lp_file_name: *const u16,
580 dw_desired_access: u32,
581 dw_share_mode: u32,
582 lp_security_attributes: *mut c_void,
583 dw_creation_disposition: u32,
584 dw_flags_and_attributes: u32,
585 h_template_file: HANDLE,
586 ) -> HANDLE;
587
588 fn DeviceIoControl(
589 h_device: HANDLE,
590 dw_io_control_code: u32,
591 lp_in_buffer: *const c_void,
592 n_in_buffer_size: u32,
593 lp_out_buffer: *mut c_void,
594 n_out_buffer_size: u32,
595 lp_bytes_returned: *mut u32,
596 lp_overlapped: *mut c_void,
597 ) -> i32;
598
599 fn CloseHandle(h_object: HANDLE) -> i32;
600 }
601
602 pub fn query_physical_drive(index: u32) -> Option<String> {
605 let path = format!(r"\\.\PhysicalDrive{index}");
606 let path_w: Vec<u16> = OsStr::new(&path).encode_wide().chain(Some(0)).collect();
607
608 let handle = unsafe {
611 CreateFileW(
612 path_w.as_ptr(),
613 0,
614 FILE_SHARE_READ | FILE_SHARE_WRITE,
615 ptr::null_mut(),
616 OPEN_EXISTING,
617 0,
618 ptr::null_mut(),
619 )
620 };
621 if handle == INVALID_HANDLE_VALUE || handle.is_null() {
622 return None;
623 }
624
625 let descriptor = query_device_descriptor(handle);
626 let result = descriptor.map(|(bus_type, model)| {
627 let size = query_disk_size(handle);
628 let seek = query_seek_penalty(handle);
629 format_disk_label(&model, size, bus_type, seek)
630 });
631
632 unsafe {
634 CloseHandle(handle);
635 }
636 result
637 }
638
639 fn read_ansi_at(buf: &[u8], offset: usize) -> String {
642 if offset == 0 || offset >= buf.len() {
643 return String::new();
644 }
645 let bytes = &buf[offset..];
646 let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
647 String::from_utf8_lossy(&bytes[..end]).trim().to_string()
648 }
649
650 fn query_device_descriptor(handle: HANDLE) -> Option<(u32, String)> {
653 let query = StoragePropertyQuery {
654 property_id: STORAGE_DEVICE_PROPERTY,
655 query_type: PROPERTY_STANDARD_QUERY,
656 additional_parameters: [0; 1],
657 };
658 let mut buf = [0u8; 1024];
661 let mut returned: u32 = 0;
662 let ok = unsafe {
665 DeviceIoControl(
666 handle,
667 IOCTL_STORAGE_QUERY_PROPERTY,
668 &query as *const _ as *const c_void,
669 size_of::<StoragePropertyQuery>() as u32,
670 buf.as_mut_ptr() as *mut c_void,
671 buf.len() as u32,
672 &mut returned,
673 ptr::null_mut(),
674 )
675 };
676 if ok == 0 || (returned as usize) < size_of::<StorageDeviceDescriptor>() {
677 return None;
678 }
679 let desc = unsafe { &*(buf.as_ptr() as *const StorageDeviceDescriptor) };
683 let bus_type = desc.bus_type;
684 let vendor = read_ansi_at(&buf, desc.vendor_id_offset as usize);
685 let product = read_ansi_at(&buf, desc.product_id_offset as usize);
686 Some((bus_type, combine_model(&vendor, &product)))
687 }
688
689 fn query_disk_size(handle: HANDLE) -> Option<u64> {
691 let mut geo = DiskGeometryEx {
692 geometry: DiskGeometry {
693 cylinders: 0,
694 media_type: 0,
695 tracks_per_cylinder: 0,
696 sectors_per_track: 0,
697 bytes_per_sector: 0,
698 },
699 disk_size: 0,
700 data: [0; 1],
701 };
702 let mut returned: u32 = 0;
703 let ok = unsafe {
705 DeviceIoControl(
706 handle,
707 IOCTL_DISK_GET_DRIVE_GEOMETRY_EX,
708 ptr::null(),
709 0,
710 &mut geo as *mut _ as *mut c_void,
711 size_of::<DiskGeometryEx>() as u32,
712 &mut returned,
713 ptr::null_mut(),
714 )
715 };
716 if ok == 0 || geo.disk_size <= 0 {
717 None
718 } else {
719 Some(geo.disk_size as u64)
720 }
721 }
722
723 fn query_seek_penalty(handle: HANDLE) -> Option<bool> {
727 let query = StoragePropertyQuery {
728 property_id: STORAGE_DEVICE_SEEK_PENALTY_PROPERTY,
729 query_type: PROPERTY_STANDARD_QUERY,
730 additional_parameters: [0; 1],
731 };
732 let mut desc = DeviceSeekPenaltyDescriptor {
733 version: 0,
734 size: 0,
735 incurs_seek_penalty: 0,
736 };
737 let mut returned: u32 = 0;
738 let ok = unsafe {
740 DeviceIoControl(
741 handle,
742 IOCTL_STORAGE_QUERY_PROPERTY,
743 &query as *const _ as *const c_void,
744 size_of::<StoragePropertyQuery>() as u32,
745 &mut desc as *mut _ as *mut c_void,
746 size_of::<DeviceSeekPenaltyDescriptor>() as u32,
747 &mut returned,
748 ptr::null_mut(),
749 )
750 };
751 if ok == 0 || (returned as usize) < size_of::<DeviceSeekPenaltyDescriptor>() {
752 None
753 } else {
754 Some(desc.incurs_seek_penalty != 0)
755 }
756 }
757
758 #[cfg(test)]
759 mod layout {
760 use std::mem::{offset_of, size_of};
761
762 #[test]
765 fn ffi_struct_layout() {
766 assert_eq!(size_of::<super::StoragePropertyQuery>(), 12);
767 assert_eq!(size_of::<super::StorageDeviceDescriptor>(), 40);
768 assert_eq!(
769 offset_of!(super::StorageDeviceDescriptor, vendor_id_offset),
770 12
771 );
772 assert_eq!(
773 offset_of!(super::StorageDeviceDescriptor, product_id_offset),
774 16
775 );
776 assert_eq!(offset_of!(super::StorageDeviceDescriptor, bus_type), 28);
777 assert_eq!(size_of::<super::DeviceSeekPenaltyDescriptor>(), 12);
778 assert_eq!(size_of::<super::DiskGeometryEx>(), 40);
779 assert_eq!(offset_of!(super::DiskGeometryEx, disk_size), 24);
780 }
781 }
782}
783
784#[cfg(test)]
785mod tests {
786 #[cfg(any(target_os = "linux", target_os = "macos"))]
787 use super::format_size;
788 #[cfg(target_os = "linux")]
789 use super::{is_skip_fs, strip_embedded_size};
790
791 #[cfg(target_os = "linux")]
792 #[test]
793 fn test_is_skip_fs_pseudo() {
794 assert!(is_skip_fs("sysfs", false));
795 assert!(is_skip_fs("proc", false));
796 assert!(is_skip_fs("tmpfs", false));
797 assert!(is_skip_fs("fusectl", false)); assert!(is_skip_fs("fusectl", true)); }
800
801 #[cfg(target_os = "linux")]
802 #[test]
803 fn test_is_skip_fs_fuse_excluded_by_default() {
804 assert!(is_skip_fs("fuse.gvfsd-fuse", false));
805 assert!(is_skip_fs("fuse.sshfs", false));
806 assert!(is_skip_fs("fuse.cryfs", false));
807 }
808
809 #[cfg(target_os = "linux")]
810 #[test]
811 fn test_is_skip_fs_fuse_included_in_full() {
812 assert!(!is_skip_fs("fuse.gvfsd-fuse", true));
813 assert!(!is_skip_fs("fuse.sshfs", true));
814 assert!(!is_skip_fs("fuse.cryfs", true));
815 }
816
817 #[cfg(target_os = "linux")]
818 #[test]
819 fn test_is_skip_fs_real_fs() {
820 assert!(!is_skip_fs("ext4", false));
821 assert!(!is_skip_fs("btrfs", false));
822 assert!(!is_skip_fs("vfat", false));
823 }
824
825 #[cfg(target_os = "linux")]
826 #[test]
827 fn test_strip_embedded_size() {
828 assert_eq!(
829 strip_embedded_size("BC901 NVMe SK hynix 1024GB"),
830 "BC901 NVMe SK hynix"
831 );
832 assert_eq!(
833 strip_embedded_size("Samsung SSD 970 EVO 500GB"),
834 "Samsung SSD 970 EVO"
835 );
836 assert_eq!(strip_embedded_size("WD Blue 2TB"), "WD Blue");
837 assert_eq!(strip_embedded_size("CT500MX500SSD1"), "CT500MX500SSD1"); assert_eq!(
839 strip_embedded_size("SAMSUNG MZQL23T8HCLS"),
840 "SAMSUNG MZQL23T8HCLS"
841 ); assert_eq!(strip_embedded_size("Some Drive 256GB"), "Some Drive");
843 }
844
845 #[cfg(any(target_os = "linux", target_os = "macos"))]
846 #[test]
847 fn test_format_size_gb() {
848 assert_eq!(format_size(512_110_190_592), "512 GB");
849 }
850
851 #[cfg(any(target_os = "linux", target_os = "macos"))]
852 #[test]
853 fn test_format_size_tb() {
854 assert_eq!(format_size(1_000_204_886_016), "1.0 TB");
855 }
856
857 #[cfg(any(target_os = "linux", target_os = "macos"))]
858 #[test]
859 fn test_format_size_2tb() {
860 assert_eq!(format_size(2_000_398_934_016), "2.0 TB");
861 }
862
863 #[cfg(target_os = "macos")]
864 #[test]
865 fn test_parse_diskutil_info_plist_apple_silicon() {
866 let plist = r#"<?xml version="1.0" encoding="UTF-8"?>
869<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
870<plist version="1.0">
871<dict>
872 <key>BusProtocol</key>
873 <string>Apple Fabric</string>
874 <key>IORegistryEntryName</key>
875 <string>APPLE SSD AP1024Z Media</string>
876 <key>MediaName</key>
877 <string>APPLE SSD AP1024Z</string>
878 <key>SolidState</key>
879 <true/>
880 <key>TotalSize</key>
881 <integer>1000555581440</integer>
882 <key>VirtualOrPhysical</key>
883 <string>Unknown</string>
884</dict>
885</plist>"#;
886 let result = super::parse_diskutil_info_plist(plist);
887 assert_eq!(result, Some("APPLE SSD AP1024Z 1.0 TB [SSD]".to_string()));
888 }
889
890 #[cfg(target_os = "macos")]
891 #[test]
892 fn test_parse_diskutil_info_plist_nvme() {
893 let plist = r#"<?xml version="1.0" encoding="UTF-8"?>
894<plist version="1.0">
895<dict>
896 <key>BusProtocol</key>
897 <string>PCIe</string>
898 <key>MediaName</key>
899 <string>Samsung SSD 990 Pro</string>
900 <key>SolidState</key>
901 <true/>
902 <key>TotalSize</key>
903 <integer>2000398934016</integer>
904 <key>VirtualOrPhysical</key>
905 <string>Physical</string>
906</dict>
907</plist>"#;
908 let result = super::parse_diskutil_info_plist(plist);
909 assert_eq!(
910 result,
911 Some("Samsung SSD 990 Pro 2.0 TB [NVMe SSD]".to_string())
912 );
913 }
914
915 #[cfg(target_os = "macos")]
916 #[test]
917 fn test_parse_diskutil_info_plist_virtual_skipped() {
918 let plist = r#"<?xml version="1.0" encoding="UTF-8"?>
919<plist version="1.0">
920<dict>
921 <key>MediaName</key>
922 <string>APFS Container Disk</string>
923 <key>TotalSize</key>
924 <integer>500000000000</integer>
925 <key>VirtualOrPhysical</key>
926 <string>Virtual</string>
927</dict>
928</plist>"#;
929 let result = super::parse_diskutil_info_plist(plist);
930 assert_eq!(result, None);
931 }
932
933 #[cfg(target_os = "windows")]
934 use super::win_ffi::BUS_TYPE_NVME;
935 #[cfg(target_os = "windows")]
936 use super::{combine_model, format_disk_label};
937
938 #[cfg(target_os = "windows")]
939 #[test]
940 fn test_format_disk_label_nvme() {
941 let label = format_disk_label(
943 "Samsung SSD 980 Pro",
944 Some(1_000_204_886_016),
945 BUS_TYPE_NVME,
946 Some(false),
947 );
948 assert_eq!(label, "Samsung SSD 980 Pro 1.0 TB [NVMe SSD]");
949 }
950
951 #[cfg(target_os = "windows")]
952 #[test]
953 fn test_format_disk_label_hdd() {
954 let label = format_disk_label("WD Blue", Some(2_000_398_934_016), 11, Some(true));
956 assert_eq!(label, "WD Blue 2.0 TB [HDD]");
957 }
958
959 #[cfg(target_os = "windows")]
960 #[test]
961 fn test_format_disk_label_sata_ssd() {
962 let label = format_disk_label(
964 "Crucial CT500MX500SSD1",
965 Some(500_107_862_016),
966 11,
967 Some(false),
968 );
969 assert_eq!(label, "Crucial CT500MX500SSD1 500 GB [SSD]");
970 }
971
972 #[cfg(target_os = "windows")]
973 #[test]
974 fn test_format_disk_label_unknown_seek_penalty_defaults_to_ssd() {
975 let label = format_disk_label("Some eMMC", Some(64_000_000_000), 13, None);
977 assert_eq!(label, "Some eMMC 64 GB [SSD]");
978 }
979
980 #[cfg(target_os = "windows")]
981 #[test]
982 fn test_format_disk_label_empty_model() {
983 let label = format_disk_label("", Some(500_107_862_016), 11, Some(false));
984 assert_eq!(label, "500 GB [SSD]");
985 }
986
987 #[cfg(target_os = "windows")]
988 #[test]
989 fn test_combine_model_generic_ata_vendor_suppressed() {
990 assert_eq!(
992 combine_model("ATA", "Samsung SSD 860 EVO"),
993 "Samsung SSD 860 EVO"
994 );
995 }
996
997 #[cfg(target_os = "windows")]
998 #[test]
999 fn test_combine_model_empty_vendor() {
1000 assert_eq!(
1001 combine_model("", "Samsung SSD 980 Pro"),
1002 "Samsung SSD 980 Pro"
1003 );
1004 }
1005
1006 #[cfg(target_os = "windows")]
1007 #[test]
1008 fn test_combine_model_vendor_already_in_product() {
1009 assert_eq!(
1011 combine_model("Samsung", "Samsung SSD 980 Pro"),
1012 "Samsung SSD 980 Pro"
1013 );
1014 }
1015
1016 #[cfg(target_os = "windows")]
1017 #[test]
1018 fn test_combine_model_distinct_vendor_prepended() {
1019 assert_eq!(combine_model("Kingston", "A400 SSD"), "Kingston A400 SSD");
1020 }
1021
1022 #[cfg(target_os = "windows")]
1023 #[test]
1024 fn test_combine_model_empty_product_falls_back_to_vendor() {
1025 assert_eq!(combine_model("SomeVendor", ""), "SomeVendor");
1026 }
1027}