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 (0..MAX_PHYSICAL_DRIVES)
430 .filter_map(win_ffi::query_physical_drive)
431 .collect()
432}
433
434#[cfg(target_os = "windows")]
446pub(crate) const MAX_PHYSICAL_DRIVES: u32 = 32;
447
448#[cfg(target_os = "windows")]
455fn format_disk_label(
456 model: &str,
457 size_bytes: Option<u64>,
458 bus_type: u32,
459 incurs_seek_penalty: Option<bool>,
460) -> String {
461 let kind = if bus_type == win_ffi::BUS_TYPE_NVME {
462 "NVMe SSD"
463 } else {
464 match incurs_seek_penalty {
465 Some(true) => "HDD",
466 Some(false) | None => "SSD",
469 }
470 };
471
472 let name = model.trim();
473 let size_str = size_bytes.map(format_size).unwrap_or_default();
474 let label = if name.is_empty() {
475 format!("{} [{}]", size_str, kind)
476 } else {
477 format!("{} {} [{}]", name, size_str, kind)
478 };
479 label.trim().to_string()
480}
481
482#[cfg(target_os = "windows")]
490fn combine_model(vendor: &str, product: &str) -> String {
491 let v = vendor.trim();
492 let p = product.trim();
493 if p.is_empty() {
494 return v.to_string();
495 }
496 if v.is_empty()
497 || v.eq_ignore_ascii_case("ATA")
498 || p.to_ascii_lowercase().contains(&v.to_ascii_lowercase())
499 {
500 p.to_string()
501 } else {
502 format!("{} {}", v, p)
503 }
504}
505
506#[cfg(target_os = "windows")]
511mod win_ffi {
512 use super::{combine_model, format_disk_label};
513 use std::ffi::{c_void, OsStr};
514 use std::mem::size_of;
515 use std::os::windows::ffi::OsStrExt;
516 use std::ptr;
517
518 #[allow(clippy::upper_case_acronyms)]
519 type HANDLE = *mut c_void;
520 const INVALID_HANDLE_VALUE: HANDLE = -1isize as HANDLE;
521 const FILE_SHARE_READ: u32 = 0x0000_0001;
522 const FILE_SHARE_WRITE: u32 = 0x0000_0002;
523 const OPEN_EXISTING: u32 = 3;
524
525 const IOCTL_STORAGE_QUERY_PROPERTY: u32 = 0x002D_1400;
528 const IOCTL_DISK_GET_DRIVE_GEOMETRY_EX: u32 = 0x0007_00A0;
529
530 const STORAGE_DEVICE_PROPERTY: u32 = 0;
532 const STORAGE_DEVICE_SEEK_PENALTY_PROPERTY: u32 = 7;
533 const PROPERTY_STANDARD_QUERY: u32 = 0;
535
536 pub const BUS_TYPE_NVME: u32 = 17;
538
539 #[repr(C)]
540 struct StoragePropertyQuery {
541 property_id: u32,
542 query_type: u32,
543 additional_parameters: [u8; 1],
544 }
545
546 #[repr(C)]
547 struct StorageDeviceDescriptor {
548 version: u32,
549 size: u32,
550 device_type: u8,
551 device_type_modifier: u8,
552 removable_media: u8,
553 command_queueing: u8,
554 vendor_id_offset: u32,
555 product_id_offset: u32,
556 product_revision_offset: u32,
557 serial_number_offset: u32,
558 bus_type: u32,
559 raw_properties_length: u32,
560 raw_device_properties: [u8; 1],
561 }
562
563 #[repr(C)]
564 struct DeviceSeekPenaltyDescriptor {
565 version: u32,
566 size: u32,
567 incurs_seek_penalty: u8,
568 }
569
570 #[repr(C)]
571 struct DiskGeometry {
572 cylinders: i64,
573 media_type: u32,
574 tracks_per_cylinder: u32,
575 sectors_per_track: u32,
576 bytes_per_sector: u32,
577 }
578
579 #[repr(C)]
580 struct DiskGeometryEx {
581 geometry: DiskGeometry,
582 disk_size: i64,
583 data: [u8; 1],
584 }
585
586 extern "system" {
587 fn CreateFileW(
588 lp_file_name: *const u16,
589 dw_desired_access: u32,
590 dw_share_mode: u32,
591 lp_security_attributes: *mut c_void,
592 dw_creation_disposition: u32,
593 dw_flags_and_attributes: u32,
594 h_template_file: HANDLE,
595 ) -> HANDLE;
596
597 fn DeviceIoControl(
598 h_device: HANDLE,
599 dw_io_control_code: u32,
600 lp_in_buffer: *const c_void,
601 n_in_buffer_size: u32,
602 lp_out_buffer: *mut c_void,
603 n_out_buffer_size: u32,
604 lp_bytes_returned: *mut u32,
605 lp_overlapped: *mut c_void,
606 ) -> i32;
607
608 fn CloseHandle(h_object: HANDLE) -> i32;
609 }
610
611 pub fn query_physical_drive(index: u32) -> Option<String> {
614 let path = format!(r"\\.\PhysicalDrive{index}");
615 let path_w: Vec<u16> = OsStr::new(&path).encode_wide().chain(Some(0)).collect();
616
617 let handle = unsafe {
620 CreateFileW(
621 path_w.as_ptr(),
622 0,
623 FILE_SHARE_READ | FILE_SHARE_WRITE,
624 ptr::null_mut(),
625 OPEN_EXISTING,
626 0,
627 ptr::null_mut(),
628 )
629 };
630 if handle == INVALID_HANDLE_VALUE || handle.is_null() {
631 return None;
632 }
633
634 let descriptor = query_device_descriptor(handle);
635 let result = descriptor.map(|(bus_type, model)| {
636 let size = query_disk_size(handle);
637 let seek = query_seek_penalty(handle);
638 format_disk_label(&model, size, bus_type, seek)
639 });
640
641 unsafe {
643 CloseHandle(handle);
644 }
645 result
646 }
647
648 fn read_ansi_at(buf: &[u8], offset: usize) -> String {
651 if offset == 0 || offset >= buf.len() {
652 return String::new();
653 }
654 let bytes = &buf[offset..];
655 let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
656 String::from_utf8_lossy(&bytes[..end]).trim().to_string()
657 }
658
659 fn query_device_descriptor(handle: HANDLE) -> Option<(u32, String)> {
662 let query = StoragePropertyQuery {
663 property_id: STORAGE_DEVICE_PROPERTY,
664 query_type: PROPERTY_STANDARD_QUERY,
665 additional_parameters: [0; 1],
666 };
667 let mut buf = [0u8; 1024];
670 let mut returned: u32 = 0;
671 let ok = unsafe {
674 DeviceIoControl(
675 handle,
676 IOCTL_STORAGE_QUERY_PROPERTY,
677 &query as *const _ as *const c_void,
678 size_of::<StoragePropertyQuery>() as u32,
679 buf.as_mut_ptr() as *mut c_void,
680 buf.len() as u32,
681 &mut returned,
682 ptr::null_mut(),
683 )
684 };
685 if ok == 0 || (returned as usize) < size_of::<StorageDeviceDescriptor>() {
686 return None;
687 }
688 let desc = unsafe { &*(buf.as_ptr() as *const StorageDeviceDescriptor) };
692 let bus_type = desc.bus_type;
693 let vendor = read_ansi_at(&buf, desc.vendor_id_offset as usize);
694 let product = read_ansi_at(&buf, desc.product_id_offset as usize);
695 Some((bus_type, combine_model(&vendor, &product)))
696 }
697
698 fn query_disk_size(handle: HANDLE) -> Option<u64> {
700 let mut geo = DiskGeometryEx {
701 geometry: DiskGeometry {
702 cylinders: 0,
703 media_type: 0,
704 tracks_per_cylinder: 0,
705 sectors_per_track: 0,
706 bytes_per_sector: 0,
707 },
708 disk_size: 0,
709 data: [0; 1],
710 };
711 let mut returned: u32 = 0;
712 let ok = unsafe {
714 DeviceIoControl(
715 handle,
716 IOCTL_DISK_GET_DRIVE_GEOMETRY_EX,
717 ptr::null(),
718 0,
719 &mut geo as *mut _ as *mut c_void,
720 size_of::<DiskGeometryEx>() as u32,
721 &mut returned,
722 ptr::null_mut(),
723 )
724 };
725 if ok == 0 || geo.disk_size <= 0 {
726 None
727 } else {
728 Some(geo.disk_size as u64)
729 }
730 }
731
732 fn query_seek_penalty(handle: HANDLE) -> Option<bool> {
736 let query = StoragePropertyQuery {
737 property_id: STORAGE_DEVICE_SEEK_PENALTY_PROPERTY,
738 query_type: PROPERTY_STANDARD_QUERY,
739 additional_parameters: [0; 1],
740 };
741 let mut desc = DeviceSeekPenaltyDescriptor {
742 version: 0,
743 size: 0,
744 incurs_seek_penalty: 0,
745 };
746 let mut returned: u32 = 0;
747 let ok = unsafe {
749 DeviceIoControl(
750 handle,
751 IOCTL_STORAGE_QUERY_PROPERTY,
752 &query as *const _ as *const c_void,
753 size_of::<StoragePropertyQuery>() as u32,
754 &mut desc as *mut _ as *mut c_void,
755 size_of::<DeviceSeekPenaltyDescriptor>() as u32,
756 &mut returned,
757 ptr::null_mut(),
758 )
759 };
760 if ok == 0 || (returned as usize) < size_of::<DeviceSeekPenaltyDescriptor>() {
761 None
762 } else {
763 Some(desc.incurs_seek_penalty != 0)
764 }
765 }
766
767 #[cfg(test)]
768 mod layout {
769 use std::mem::{offset_of, size_of};
770
771 #[test]
774 fn ffi_struct_layout() {
775 assert_eq!(size_of::<super::StoragePropertyQuery>(), 12);
776 assert_eq!(size_of::<super::StorageDeviceDescriptor>(), 40);
777 assert_eq!(
778 offset_of!(super::StorageDeviceDescriptor, vendor_id_offset),
779 12
780 );
781 assert_eq!(
782 offset_of!(super::StorageDeviceDescriptor, product_id_offset),
783 16
784 );
785 assert_eq!(offset_of!(super::StorageDeviceDescriptor, bus_type), 28);
786 assert_eq!(size_of::<super::DeviceSeekPenaltyDescriptor>(), 12);
787 assert_eq!(size_of::<super::DiskGeometryEx>(), 40);
788 assert_eq!(offset_of!(super::DiskGeometryEx, disk_size), 24);
789 }
790 }
791}
792
793#[cfg(test)]
794mod tests {
795 #[cfg(any(target_os = "linux", target_os = "macos"))]
796 use super::format_size;
797 #[cfg(target_os = "linux")]
798 use super::{is_skip_fs, strip_embedded_size};
799
800 #[cfg(target_os = "linux")]
801 #[test]
802 fn test_is_skip_fs_pseudo() {
803 assert!(is_skip_fs("sysfs", false));
804 assert!(is_skip_fs("proc", false));
805 assert!(is_skip_fs("tmpfs", false));
806 assert!(is_skip_fs("fusectl", false)); assert!(is_skip_fs("fusectl", true)); }
809
810 #[cfg(target_os = "linux")]
811 #[test]
812 fn test_is_skip_fs_fuse_excluded_by_default() {
813 assert!(is_skip_fs("fuse.gvfsd-fuse", false));
814 assert!(is_skip_fs("fuse.sshfs", false));
815 assert!(is_skip_fs("fuse.cryfs", false));
816 }
817
818 #[cfg(target_os = "linux")]
819 #[test]
820 fn test_is_skip_fs_fuse_included_in_full() {
821 assert!(!is_skip_fs("fuse.gvfsd-fuse", true));
822 assert!(!is_skip_fs("fuse.sshfs", true));
823 assert!(!is_skip_fs("fuse.cryfs", true));
824 }
825
826 #[cfg(target_os = "linux")]
827 #[test]
828 fn test_is_skip_fs_real_fs() {
829 assert!(!is_skip_fs("ext4", false));
830 assert!(!is_skip_fs("btrfs", false));
831 assert!(!is_skip_fs("vfat", false));
832 }
833
834 #[cfg(target_os = "linux")]
835 #[test]
836 fn test_strip_embedded_size() {
837 assert_eq!(
838 strip_embedded_size("BC901 NVMe SK hynix 1024GB"),
839 "BC901 NVMe SK hynix"
840 );
841 assert_eq!(
842 strip_embedded_size("Samsung SSD 970 EVO 500GB"),
843 "Samsung SSD 970 EVO"
844 );
845 assert_eq!(strip_embedded_size("WD Blue 2TB"), "WD Blue");
846 assert_eq!(strip_embedded_size("CT500MX500SSD1"), "CT500MX500SSD1"); assert_eq!(
848 strip_embedded_size("SAMSUNG MZQL23T8HCLS"),
849 "SAMSUNG MZQL23T8HCLS"
850 ); assert_eq!(strip_embedded_size("Some Drive 256GB"), "Some Drive");
852 }
853
854 #[cfg(any(target_os = "linux", target_os = "macos"))]
855 #[test]
856 fn test_format_size_gb() {
857 assert_eq!(format_size(512_110_190_592), "512 GB");
858 }
859
860 #[cfg(any(target_os = "linux", target_os = "macos"))]
861 #[test]
862 fn test_format_size_tb() {
863 assert_eq!(format_size(1_000_204_886_016), "1.0 TB");
864 }
865
866 #[cfg(any(target_os = "linux", target_os = "macos"))]
867 #[test]
868 fn test_format_size_2tb() {
869 assert_eq!(format_size(2_000_398_934_016), "2.0 TB");
870 }
871
872 #[cfg(target_os = "macos")]
873 #[test]
874 fn test_parse_diskutil_info_plist_apple_silicon() {
875 let plist = r#"<?xml version="1.0" encoding="UTF-8"?>
878<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
879<plist version="1.0">
880<dict>
881 <key>BusProtocol</key>
882 <string>Apple Fabric</string>
883 <key>IORegistryEntryName</key>
884 <string>APPLE SSD AP1024Z Media</string>
885 <key>MediaName</key>
886 <string>APPLE SSD AP1024Z</string>
887 <key>SolidState</key>
888 <true/>
889 <key>TotalSize</key>
890 <integer>1000555581440</integer>
891 <key>VirtualOrPhysical</key>
892 <string>Unknown</string>
893</dict>
894</plist>"#;
895 let result = super::parse_diskutil_info_plist(plist);
896 assert_eq!(result, Some("APPLE SSD AP1024Z 1.0 TB [SSD]".to_string()));
897 }
898
899 #[cfg(target_os = "macos")]
900 #[test]
901 fn test_parse_diskutil_info_plist_nvme() {
902 let plist = r#"<?xml version="1.0" encoding="UTF-8"?>
903<plist version="1.0">
904<dict>
905 <key>BusProtocol</key>
906 <string>PCIe</string>
907 <key>MediaName</key>
908 <string>Samsung SSD 990 Pro</string>
909 <key>SolidState</key>
910 <true/>
911 <key>TotalSize</key>
912 <integer>2000398934016</integer>
913 <key>VirtualOrPhysical</key>
914 <string>Physical</string>
915</dict>
916</plist>"#;
917 let result = super::parse_diskutil_info_plist(plist);
918 assert_eq!(
919 result,
920 Some("Samsung SSD 990 Pro 2.0 TB [NVMe SSD]".to_string())
921 );
922 }
923
924 #[cfg(target_os = "macos")]
925 #[test]
926 fn test_parse_diskutil_info_plist_virtual_skipped() {
927 let plist = r#"<?xml version="1.0" encoding="UTF-8"?>
928<plist version="1.0">
929<dict>
930 <key>MediaName</key>
931 <string>APFS Container Disk</string>
932 <key>TotalSize</key>
933 <integer>500000000000</integer>
934 <key>VirtualOrPhysical</key>
935 <string>Virtual</string>
936</dict>
937</plist>"#;
938 let result = super::parse_diskutil_info_plist(plist);
939 assert_eq!(result, None);
940 }
941
942 #[cfg(target_os = "windows")]
943 use super::win_ffi::BUS_TYPE_NVME;
944 #[cfg(target_os = "windows")]
945 use super::{combine_model, format_disk_label};
946
947 #[cfg(target_os = "windows")]
948 #[test]
949 fn test_format_disk_label_nvme() {
950 let label = format_disk_label(
952 "Samsung SSD 980 Pro",
953 Some(1_000_204_886_016),
954 BUS_TYPE_NVME,
955 Some(false),
956 );
957 assert_eq!(label, "Samsung SSD 980 Pro 1.0 TB [NVMe SSD]");
958 }
959
960 #[cfg(target_os = "windows")]
961 #[test]
962 fn test_format_disk_label_hdd() {
963 let label = format_disk_label("WD Blue", Some(2_000_398_934_016), 11, Some(true));
965 assert_eq!(label, "WD Blue 2.0 TB [HDD]");
966 }
967
968 #[cfg(target_os = "windows")]
969 #[test]
970 fn test_format_disk_label_sata_ssd() {
971 let label = format_disk_label(
973 "Crucial CT500MX500SSD1",
974 Some(500_107_862_016),
975 11,
976 Some(false),
977 );
978 assert_eq!(label, "Crucial CT500MX500SSD1 500 GB [SSD]");
979 }
980
981 #[cfg(target_os = "windows")]
982 #[test]
983 fn test_format_disk_label_unknown_seek_penalty_defaults_to_ssd() {
984 let label = format_disk_label("Some eMMC", Some(64_000_000_000), 13, None);
986 assert_eq!(label, "Some eMMC 64 GB [SSD]");
987 }
988
989 #[cfg(target_os = "windows")]
990 #[test]
991 fn test_format_disk_label_empty_model() {
992 let label = format_disk_label("", Some(500_107_862_016), 11, Some(false));
993 assert_eq!(label, "500 GB [SSD]");
994 }
995
996 #[cfg(target_os = "windows")]
997 #[test]
998 fn test_combine_model_generic_ata_vendor_suppressed() {
999 assert_eq!(
1001 combine_model("ATA", "Samsung SSD 860 EVO"),
1002 "Samsung SSD 860 EVO"
1003 );
1004 }
1005
1006 #[cfg(target_os = "windows")]
1007 #[test]
1008 fn test_combine_model_empty_vendor() {
1009 assert_eq!(
1010 combine_model("", "Samsung SSD 980 Pro"),
1011 "Samsung SSD 980 Pro"
1012 );
1013 }
1014
1015 #[cfg(target_os = "windows")]
1016 #[test]
1017 fn test_combine_model_vendor_already_in_product() {
1018 assert_eq!(
1020 combine_model("Samsung", "Samsung SSD 980 Pro"),
1021 "Samsung SSD 980 Pro"
1022 );
1023 }
1024
1025 #[cfg(target_os = "windows")]
1026 #[test]
1027 fn test_combine_model_distinct_vendor_prepended() {
1028 assert_eq!(combine_model("Kingston", "A400 SSD"), "Kingston A400 SSD");
1029 }
1030
1031 #[cfg(target_os = "windows")]
1032 #[test]
1033 fn test_combine_model_empty_product_falls_back_to_vendor() {
1034 assert_eq!(combine_model("SomeVendor", ""), "SomeVendor");
1035 }
1036}