1pub fn detect_logical_disks(include_fuse: bool) -> Vec<(String, u64, u64, String)> {
16 #[cfg(target_os = "linux")]
17 {
18 detect_logical_linux(include_fuse)
19 }
20
21 #[cfg(target_os = "macos")]
22 {
23 let _ = include_fuse;
24 crate::macos_ffi::get_mounts()
25 .iter()
26 .filter_map(macos_volume_entry)
27 .collect()
28 }
29
30 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
31 {
32 let _ = include_fuse;
33 detect_logical_sysinfo()
34 }
35}
36
37#[cfg(target_os = "macos")]
49pub fn macos_volume_entry(m: &crate::macos_ffi::MacMount) -> Option<(String, u64, u64, String)> {
50 if !m.browsable || !m.local || m.total_bytes == 0 {
51 return None;
52 }
53 Some((
54 m.mount_point.clone(),
55 m.total_bytes,
56 m.avail_bytes,
57 m.fs_type.clone(),
58 ))
59}
60
61#[cfg(target_os = "linux")]
63fn is_skip_fs(fs_type: &str, include_fuse: bool) -> bool {
64 const SKIP: &[&str] = &[
65 "sysfs",
66 "proc",
67 "devtmpfs",
68 "tmpfs",
69 "devpts",
70 "cgroup",
71 "cgroup2",
72 "pstore",
73 "bpf",
74 "tracefs",
75 "debugfs",
76 "securityfs",
77 "hugetlbfs",
78 "mqueue",
79 "fusectl",
80 "rpc_pipefs",
81 "configfs",
82 "autofs",
83 "efivarfs",
84 "binfmt_misc",
85 "squashfs",
86 "overlay",
87 "ramfs",
88 "rootfs",
89 "nsfs",
90 "pipefs",
91 "sockfs",
92 "anon_inodefs",
93 "cpuset",
94 ];
95 SKIP.contains(&fs_type) || (fs_type.starts_with("fuse.") && !include_fuse)
97}
98
99#[cfg(target_os = "linux")]
100fn detect_logical_linux(include_fuse: bool) -> Vec<(String, u64, u64, String)> {
101 use std::collections::HashSet;
102 use std::ffi::CString;
103
104 let mounts = std::fs::read_to_string("/proc/mounts").unwrap_or_default();
105 let mut results = Vec::new();
106 let mut seen_devs: HashSet<String> = HashSet::new();
107
108 for line in mounts.lines() {
109 let parts: Vec<&str> = line.splitn(4, ' ').collect();
110 if parts.len() < 3 {
111 continue;
112 }
113 let device = parts[0];
114 let mount_point = parts[1];
115 let fs_type = parts[2];
116
117 if is_skip_fs(fs_type, include_fuse) {
118 continue;
119 }
120
121 if device.starts_with('/') && !seen_devs.insert(device.to_string()) {
123 continue;
124 }
125
126 let Ok(mp_c) = CString::new(mount_point) else {
127 continue;
128 };
129
130 let mut stat: libc::statvfs = unsafe { std::mem::zeroed() };
131 if unsafe { libc::statvfs(mp_c.as_ptr(), &mut stat) } != 0 {
132 continue;
133 }
134
135 let total = (stat.f_blocks as u64).saturating_mul(stat.f_frsize as u64);
136 let avail = (stat.f_bavail as u64).saturating_mul(stat.f_frsize as u64);
137
138 if total == 0 {
139 continue;
140 }
141
142 results.push((mount_point.to_string(), total, avail, fs_type.to_string()));
143 }
144
145 results
146}
147
148#[cfg(not(any(target_os = "linux", target_os = "macos")))]
149fn detect_logical_sysinfo() -> Vec<(String, u64, u64, String)> {
150 use sysinfo::Disks;
151 Disks::new_with_refreshed_list()
152 .iter()
153 .filter(|d| d.total_space() > 0)
154 .map(|d| {
155 (
156 d.mount_point().to_string_lossy().to_string(),
157 d.total_space(),
158 d.available_space(),
159 d.file_system().to_string_lossy().to_string(),
160 )
161 })
162 .collect()
163}
164
165pub fn detect_physical_disks() -> Vec<String> {
166 #[cfg(target_os = "linux")]
167 return detect_linux();
168
169 #[cfg(target_os = "macos")]
170 return detect_macos();
171
172 #[cfg(target_os = "windows")]
173 return detect_windows();
174
175 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
176 return Vec::new();
177}
178
179#[cfg(target_os = "linux")]
185pub(crate) fn is_virtual_block_name(name: &str) -> bool {
186 name.starts_with("loop")
187 || name.starts_with("ram")
188 || name.starts_with("zram")
189 || name.starts_with("dm-")
190 || name.starts_with("md")
191}
192
193#[cfg(target_os = "linux")]
194fn detect_linux() -> Vec<String> {
195 use std::fs;
196
197 let Ok(entries) = fs::read_dir("/sys/class/block") else {
198 return Vec::new();
199 };
200
201 let mut disks = Vec::new();
202
203 for entry in entries.flatten() {
204 let name = entry.file_name();
205 let name = name.to_string_lossy();
206
207 if is_virtual_block_name(&name) {
209 continue;
210 }
211
212 let dev_path = entry.path();
213
214 if dev_path.join("partition").exists() {
216 continue;
217 }
218
219 if !dev_path.join("queue").exists() {
221 continue;
222 }
223
224 let model = fs::read_to_string(dev_path.join("device/model"))
225 .map(|s| strip_embedded_size(s.trim()).to_string())
226 .unwrap_or_default();
227
228 let size_bytes = fs::read_to_string(dev_path.join("size"))
230 .ok()
231 .and_then(|s| s.trim().parse::<u64>().ok())
232 .map(|sectors| sectors * 512);
233
234 let rotational = fs::read_to_string(dev_path.join("queue/rotational"))
235 .map(|s| s.trim() == "1")
236 .unwrap_or(false);
237
238 let is_nvme = name.starts_with("nvme");
239
240 let kind = if is_nvme {
241 "NVMe SSD"
242 } else if rotational {
243 "HDD"
244 } else {
245 "SSD"
246 };
247
248 let size_str = size_bytes.map(format_size).unwrap_or_default();
249
250 let label = if model.is_empty() {
251 format!("{} [{}]", size_str, kind)
252 } else {
253 format!("{} {} [{}]", model.trim(), size_str, kind)
254 };
255
256 let label = label.trim().to_string();
257 if !label.is_empty() {
258 disks.push(label);
259 }
260 }
261
262 disks.sort();
263 disks
264}
265
266#[cfg(target_os = "macos")]
273fn detect_macos() -> Vec<String> {
274 crate::macos_ffi::get_physical_disks()
275 .iter()
276 .filter_map(format_macos_disk)
277 .collect()
278}
279
280#[cfg(target_os = "macos")]
288pub fn format_macos_disk(disk: &crate::macos_ffi::MacDiskRaw) -> Option<String> {
289 if disk.interconnect == "Virtual Interface" {
290 return None;
291 }
292
293 let protocol = disk.interconnect.to_lowercase();
294 let kind = if protocol.contains("pcie") || protocol.contains("nvme") {
295 "NVMe SSD"
296 } else if disk.solid_state {
297 "SSD"
298 } else {
299 "HDD"
300 };
301
302 let size_str = disk.size_bytes.map(format_size).unwrap_or_default();
303
304 let label = if disk.model.trim().is_empty() {
305 format!("{} [{}]", size_str, kind)
306 } else {
307 format!("{} {} [{}]", disk.model.trim(), size_str, kind)
308 };
309
310 Some(label.trim().to_string())
311}
312
313#[cfg(target_os = "linux")]
316fn strip_embedded_size(model: &str) -> &str {
317 let bytes = model.as_bytes();
318 let mut i = bytes.len();
320 while i > 0 && bytes[i - 1] == b' ' {
322 i -= 1;
323 }
324 if i >= 2 {
326 let suffix = &bytes[i - 2..i];
327 if matches!(suffix, b"GB" | b"TB" | b"MB") {
328 i -= 2;
329 let digits_end = i;
331 while i > 0 && bytes[i - 1].is_ascii_digit() {
332 i -= 1;
333 }
334 if i < digits_end {
335 if i > 0 && bytes[i - 1] == b' ' {
337 i -= 1;
338 }
339 return model[..i].trim_end();
340 }
341 }
342 }
343 model
344}
345
346#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
347fn format_size(bytes: u64) -> String {
348 const TB: u64 = 1_000_000_000_000;
349 const GB: u64 = 1_000_000_000;
350 if bytes >= TB {
351 format!("{:.1} TB", bytes as f64 / TB as f64)
352 } else {
353 format!("{:.0} GB", bytes as f64 / GB as f64)
354 }
355}
356
357#[cfg(target_os = "windows")]
364fn detect_windows() -> Vec<String> {
365 (0..MAX_PHYSICAL_DRIVES)
366 .filter_map(win_ffi::query_physical_drive)
367 .collect()
368}
369
370#[cfg(target_os = "windows")]
382pub(crate) const MAX_PHYSICAL_DRIVES: u32 = 32;
383
384#[cfg(target_os = "windows")]
391fn format_disk_label(
392 model: &str,
393 size_bytes: Option<u64>,
394 bus_type: u32,
395 incurs_seek_penalty: Option<bool>,
396) -> String {
397 let kind = if bus_type == win_ffi::BUS_TYPE_NVME {
398 "NVMe SSD"
399 } else {
400 match incurs_seek_penalty {
401 Some(true) => "HDD",
402 Some(false) | None => "SSD",
405 }
406 };
407
408 let name = model.trim();
409 let size_str = size_bytes.map(format_size).unwrap_or_default();
410 let label = if name.is_empty() {
411 format!("{} [{}]", size_str, kind)
412 } else {
413 format!("{} {} [{}]", name, size_str, kind)
414 };
415 label.trim().to_string()
416}
417
418#[cfg(target_os = "windows")]
426fn combine_model(vendor: &str, product: &str) -> String {
427 let v = vendor.trim();
428 let p = product.trim();
429 if p.is_empty() {
430 return v.to_string();
431 }
432 if v.is_empty()
433 || v.eq_ignore_ascii_case("ATA")
434 || p.to_ascii_lowercase().contains(&v.to_ascii_lowercase())
435 {
436 p.to_string()
437 } else {
438 format!("{} {}", v, p)
439 }
440}
441
442#[cfg(target_os = "windows")]
447mod win_ffi {
448 use super::{combine_model, format_disk_label};
449 use std::ffi::{c_void, OsStr};
450 use std::mem::size_of;
451 use std::os::windows::ffi::OsStrExt;
452 use std::ptr;
453
454 #[allow(clippy::upper_case_acronyms)]
455 type HANDLE = *mut c_void;
456 const INVALID_HANDLE_VALUE: HANDLE = -1isize as HANDLE;
457 const FILE_SHARE_READ: u32 = 0x0000_0001;
458 const FILE_SHARE_WRITE: u32 = 0x0000_0002;
459 const OPEN_EXISTING: u32 = 3;
460
461 const IOCTL_STORAGE_QUERY_PROPERTY: u32 = 0x002D_1400;
464 const IOCTL_DISK_GET_DRIVE_GEOMETRY_EX: u32 = 0x0007_00A0;
465
466 const STORAGE_DEVICE_PROPERTY: u32 = 0;
468 const STORAGE_DEVICE_SEEK_PENALTY_PROPERTY: u32 = 7;
469 const PROPERTY_STANDARD_QUERY: u32 = 0;
471
472 pub const BUS_TYPE_NVME: u32 = 17;
474
475 #[repr(C)]
476 struct StoragePropertyQuery {
477 property_id: u32,
478 query_type: u32,
479 additional_parameters: [u8; 1],
480 }
481
482 #[repr(C)]
483 struct StorageDeviceDescriptor {
484 version: u32,
485 size: u32,
486 device_type: u8,
487 device_type_modifier: u8,
488 removable_media: u8,
489 command_queueing: u8,
490 vendor_id_offset: u32,
491 product_id_offset: u32,
492 product_revision_offset: u32,
493 serial_number_offset: u32,
494 bus_type: u32,
495 raw_properties_length: u32,
496 raw_device_properties: [u8; 1],
497 }
498
499 #[repr(C)]
500 struct DeviceSeekPenaltyDescriptor {
501 version: u32,
502 size: u32,
503 incurs_seek_penalty: u8,
504 }
505
506 #[repr(C)]
507 struct DiskGeometry {
508 cylinders: i64,
509 media_type: u32,
510 tracks_per_cylinder: u32,
511 sectors_per_track: u32,
512 bytes_per_sector: u32,
513 }
514
515 #[repr(C)]
516 struct DiskGeometryEx {
517 geometry: DiskGeometry,
518 disk_size: i64,
519 data: [u8; 1],
520 }
521
522 extern "system" {
523 fn CreateFileW(
524 lp_file_name: *const u16,
525 dw_desired_access: u32,
526 dw_share_mode: u32,
527 lp_security_attributes: *mut c_void,
528 dw_creation_disposition: u32,
529 dw_flags_and_attributes: u32,
530 h_template_file: HANDLE,
531 ) -> HANDLE;
532
533 fn DeviceIoControl(
534 h_device: HANDLE,
535 dw_io_control_code: u32,
536 lp_in_buffer: *const c_void,
537 n_in_buffer_size: u32,
538 lp_out_buffer: *mut c_void,
539 n_out_buffer_size: u32,
540 lp_bytes_returned: *mut u32,
541 lp_overlapped: *mut c_void,
542 ) -> i32;
543
544 fn CloseHandle(h_object: HANDLE) -> i32;
545 }
546
547 pub fn query_physical_drive(index: u32) -> Option<String> {
550 let path = format!(r"\\.\PhysicalDrive{index}");
551 let path_w: Vec<u16> = OsStr::new(&path).encode_wide().chain(Some(0)).collect();
552
553 let handle = unsafe {
556 CreateFileW(
557 path_w.as_ptr(),
558 0,
559 FILE_SHARE_READ | FILE_SHARE_WRITE,
560 ptr::null_mut(),
561 OPEN_EXISTING,
562 0,
563 ptr::null_mut(),
564 )
565 };
566 if handle == INVALID_HANDLE_VALUE || handle.is_null() {
567 return None;
568 }
569
570 let descriptor = query_device_descriptor(handle);
571 let result = descriptor.map(|(bus_type, model)| {
572 let size = query_disk_size(handle);
573 let seek = query_seek_penalty(handle);
574 format_disk_label(&model, size, bus_type, seek)
575 });
576
577 unsafe {
579 CloseHandle(handle);
580 }
581 result
582 }
583
584 fn read_ansi_at(buf: &[u8], offset: usize) -> String {
587 if offset == 0 || offset >= buf.len() {
588 return String::new();
589 }
590 let bytes = &buf[offset..];
591 let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
592 String::from_utf8_lossy(&bytes[..end]).trim().to_string()
593 }
594
595 fn query_device_descriptor(handle: HANDLE) -> Option<(u32, String)> {
598 let query = StoragePropertyQuery {
599 property_id: STORAGE_DEVICE_PROPERTY,
600 query_type: PROPERTY_STANDARD_QUERY,
601 additional_parameters: [0; 1],
602 };
603 let mut buf = [0u8; 1024];
606 let mut returned: u32 = 0;
607 let ok = unsafe {
610 DeviceIoControl(
611 handle,
612 IOCTL_STORAGE_QUERY_PROPERTY,
613 &query as *const _ as *const c_void,
614 size_of::<StoragePropertyQuery>() as u32,
615 buf.as_mut_ptr() as *mut c_void,
616 buf.len() as u32,
617 &mut returned,
618 ptr::null_mut(),
619 )
620 };
621 if ok == 0 || (returned as usize) < size_of::<StorageDeviceDescriptor>() {
622 return None;
623 }
624 let desc = unsafe { &*(buf.as_ptr() as *const StorageDeviceDescriptor) };
628 let bus_type = desc.bus_type;
629 let vendor = read_ansi_at(&buf, desc.vendor_id_offset as usize);
630 let product = read_ansi_at(&buf, desc.product_id_offset as usize);
631 Some((bus_type, combine_model(&vendor, &product)))
632 }
633
634 fn query_disk_size(handle: HANDLE) -> Option<u64> {
636 let mut geo = DiskGeometryEx {
637 geometry: DiskGeometry {
638 cylinders: 0,
639 media_type: 0,
640 tracks_per_cylinder: 0,
641 sectors_per_track: 0,
642 bytes_per_sector: 0,
643 },
644 disk_size: 0,
645 data: [0; 1],
646 };
647 let mut returned: u32 = 0;
648 let ok = unsafe {
650 DeviceIoControl(
651 handle,
652 IOCTL_DISK_GET_DRIVE_GEOMETRY_EX,
653 ptr::null(),
654 0,
655 &mut geo as *mut _ as *mut c_void,
656 size_of::<DiskGeometryEx>() as u32,
657 &mut returned,
658 ptr::null_mut(),
659 )
660 };
661 if ok == 0 || geo.disk_size <= 0 {
662 None
663 } else {
664 Some(geo.disk_size as u64)
665 }
666 }
667
668 fn query_seek_penalty(handle: HANDLE) -> Option<bool> {
672 let query = StoragePropertyQuery {
673 property_id: STORAGE_DEVICE_SEEK_PENALTY_PROPERTY,
674 query_type: PROPERTY_STANDARD_QUERY,
675 additional_parameters: [0; 1],
676 };
677 let mut desc = DeviceSeekPenaltyDescriptor {
678 version: 0,
679 size: 0,
680 incurs_seek_penalty: 0,
681 };
682 let mut returned: u32 = 0;
683 let ok = unsafe {
685 DeviceIoControl(
686 handle,
687 IOCTL_STORAGE_QUERY_PROPERTY,
688 &query as *const _ as *const c_void,
689 size_of::<StoragePropertyQuery>() as u32,
690 &mut desc as *mut _ as *mut c_void,
691 size_of::<DeviceSeekPenaltyDescriptor>() as u32,
692 &mut returned,
693 ptr::null_mut(),
694 )
695 };
696 if ok == 0 || (returned as usize) < size_of::<DeviceSeekPenaltyDescriptor>() {
697 None
698 } else {
699 Some(desc.incurs_seek_penalty != 0)
700 }
701 }
702
703 #[cfg(test)]
704 mod layout {
705 use std::mem::{offset_of, size_of};
706
707 #[test]
710 fn ffi_struct_layout() {
711 assert_eq!(size_of::<super::StoragePropertyQuery>(), 12);
712 assert_eq!(size_of::<super::StorageDeviceDescriptor>(), 40);
713 assert_eq!(
714 offset_of!(super::StorageDeviceDescriptor, vendor_id_offset),
715 12
716 );
717 assert_eq!(
718 offset_of!(super::StorageDeviceDescriptor, product_id_offset),
719 16
720 );
721 assert_eq!(offset_of!(super::StorageDeviceDescriptor, bus_type), 28);
722 assert_eq!(size_of::<super::DeviceSeekPenaltyDescriptor>(), 12);
723 assert_eq!(size_of::<super::DiskGeometryEx>(), 40);
724 assert_eq!(offset_of!(super::DiskGeometryEx, disk_size), 24);
725 }
726 }
727}
728
729#[cfg(test)]
730mod tests {
731 #[cfg(any(target_os = "linux", target_os = "macos"))]
732 use super::format_size;
733 #[cfg(target_os = "linux")]
734 use super::{is_skip_fs, strip_embedded_size};
735
736 #[cfg(target_os = "linux")]
737 #[test]
738 fn test_is_skip_fs_pseudo() {
739 assert!(is_skip_fs("sysfs", false));
740 assert!(is_skip_fs("proc", false));
741 assert!(is_skip_fs("tmpfs", false));
742 assert!(is_skip_fs("fusectl", false)); assert!(is_skip_fs("fusectl", true)); }
745
746 #[cfg(target_os = "linux")]
747 #[test]
748 fn test_is_skip_fs_fuse_excluded_by_default() {
749 assert!(is_skip_fs("fuse.gvfsd-fuse", false));
750 assert!(is_skip_fs("fuse.sshfs", false));
751 assert!(is_skip_fs("fuse.cryfs", false));
752 }
753
754 #[cfg(target_os = "linux")]
755 #[test]
756 fn test_is_skip_fs_fuse_included_in_full() {
757 assert!(!is_skip_fs("fuse.gvfsd-fuse", true));
758 assert!(!is_skip_fs("fuse.sshfs", true));
759 assert!(!is_skip_fs("fuse.cryfs", true));
760 }
761
762 #[cfg(target_os = "linux")]
763 #[test]
764 fn test_is_skip_fs_real_fs() {
765 assert!(!is_skip_fs("ext4", false));
766 assert!(!is_skip_fs("btrfs", false));
767 assert!(!is_skip_fs("vfat", false));
768 }
769
770 #[cfg(target_os = "linux")]
771 #[test]
772 fn test_strip_embedded_size() {
773 assert_eq!(
774 strip_embedded_size("BC901 NVMe SK hynix 1024GB"),
775 "BC901 NVMe SK hynix"
776 );
777 assert_eq!(
778 strip_embedded_size("Samsung SSD 970 EVO 500GB"),
779 "Samsung SSD 970 EVO"
780 );
781 assert_eq!(strip_embedded_size("WD Blue 2TB"), "WD Blue");
782 assert_eq!(strip_embedded_size("CT500MX500SSD1"), "CT500MX500SSD1"); assert_eq!(
784 strip_embedded_size("SAMSUNG MZQL23T8HCLS"),
785 "SAMSUNG MZQL23T8HCLS"
786 ); assert_eq!(strip_embedded_size("Some Drive 256GB"), "Some Drive");
788 }
789
790 #[cfg(any(target_os = "linux", target_os = "macos"))]
791 #[test]
792 fn test_format_size_gb() {
793 assert_eq!(format_size(512_110_190_592), "512 GB");
794 }
795
796 #[cfg(any(target_os = "linux", target_os = "macos"))]
797 #[test]
798 fn test_format_size_tb() {
799 assert_eq!(format_size(1_000_204_886_016), "1.0 TB");
800 }
801
802 #[cfg(any(target_os = "linux", target_os = "macos"))]
803 #[test]
804 fn test_format_size_2tb() {
805 assert_eq!(format_size(2_000_398_934_016), "2.0 TB");
806 }
807
808 #[cfg(target_os = "macos")]
810 fn mac_disk(
811 model: &str,
812 size: Option<u64>,
813 ssd: bool,
814 bus: &str,
815 ) -> crate::macos_ffi::MacDiskRaw {
816 crate::macos_ffi::MacDiskRaw {
817 bsd_name: "disk0".to_string(),
818 model: model.to_string(),
819 size_bytes: size,
820 solid_state: ssd,
821 interconnect: bus.to_string(),
822 }
823 }
824
825 #[cfg(target_os = "macos")]
826 #[test]
827 fn test_format_macos_disk_apple_silicon() {
828 let d = mac_disk(
831 "APPLE SSD AP1024Z",
832 Some(1_000_555_581_440),
833 true,
834 "Apple Fabric",
835 );
836 assert_eq!(
837 super::format_macos_disk(&d),
838 Some("APPLE SSD AP1024Z 1.0 TB [SSD]".to_string())
839 );
840 }
841
842 #[cfg(target_os = "macos")]
843 #[test]
844 fn test_format_macos_disk_nvme() {
845 let d = mac_disk("Samsung SSD 990 Pro", Some(2_000_398_934_016), true, "PCIe");
846 assert_eq!(
847 super::format_macos_disk(&d),
848 Some("Samsung SSD 990 Pro 2.0 TB [NVMe SSD]".to_string())
849 );
850 }
851
852 #[cfg(target_os = "macos")]
853 #[test]
854 fn test_format_macos_disk_image_skipped() {
855 let d = mac_disk("Disk Image", Some(10_485_760), false, "Virtual Interface");
858 assert_eq!(super::format_macos_disk(&d), None);
859 }
860
861 #[cfg(target_os = "macos")]
862 #[test]
863 fn test_format_macos_disk_no_medium_type_is_hdd() {
864 let d = mac_disk("", Some(500_107_862_016), false, "USB");
866 assert_eq!(
867 super::format_macos_disk(&d),
868 Some("500 GB [HDD]".to_string())
869 );
870 }
871
872 #[cfg(target_os = "macos")]
874 fn mac_mount(
875 mp: &str,
876 fs: &str,
877 total: u64,
878 browsable: bool,
879 local: bool,
880 ) -> crate::macos_ffi::MacMount {
881 crate::macos_ffi::MacMount {
882 mount_point: mp.to_string(),
883 fs_type: fs.to_string(),
884 total_bytes: total,
885 avail_bytes: total / 3,
886 browsable,
887 local,
888 }
889 }
890
891 #[cfg(target_os = "macos")]
892 #[test]
893 fn test_macos_volume_entry_keeps_what_sysinfo_kept() {
894 let mounts = [
899 mac_mount("/", "apfs", 994_662_584_320, true, true),
900 mac_mount("/dev", "devfs", 220_160, false, true),
901 mac_mount("/System/Volumes/VM", "apfs", 994_662_584_320, false, true),
902 mac_mount(
903 "/System/Volumes/Preboot",
904 "apfs",
905 994_662_584_320,
906 false,
907 true,
908 ),
909 mac_mount(
910 "/System/Volumes/Update",
911 "apfs",
912 994_662_584_320,
913 false,
914 true,
915 ),
916 mac_mount("/System/Volumes/xarts", "apfs", 524_288_000, false, true),
917 mac_mount(
918 "/System/Volumes/iSCPreboot",
919 "apfs",
920 524_288_000,
921 false,
922 true,
923 ),
924 mac_mount("/System/Volumes/Hardware", "apfs", 524_288_000, false, true),
925 mac_mount("/System/Volumes/Data", "apfs", 994_662_584_320, true, true),
926 mac_mount("/System/Volumes/Data/home", "autofs", 0, false, false),
927 mac_mount("/Volumes/Recovery", "apfs", 994_662_584_320, false, true),
928 mac_mount(
929 "/System/Volumes/Update/SFR/mnt1",
930 "apfs",
931 5_368_709_120,
932 false,
933 true,
934 ),
935 mac_mount(
936 "/System/Volumes/Update/mnt1",
937 "apfs",
938 994_662_584_320,
939 false,
940 true,
941 ),
942 ];
943 let kept: Vec<String> = mounts
944 .iter()
945 .filter_map(super::macos_volume_entry)
946 .map(|(mp, ..)| mp)
947 .collect();
948 assert_eq!(kept, vec!["/", "/System/Volumes/Data"]);
949 }
950
951 #[cfg(target_os = "macos")]
952 #[test]
953 fn test_macos_volume_entry_drops_remote_and_empty() {
954 let smb = mac_mount("/Volumes/share", "smbfs", 4_000_000_000_000, true, false);
956 assert_eq!(super::macos_volume_entry(&smb), None);
957 let empty = mac_mount("/Volumes/X", "apfs", 0, true, true);
959 assert_eq!(super::macos_volume_entry(&empty), None);
960 let ext = mac_mount("/Volumes/USB", "exfat", 128_000_000_000, true, true);
962 assert_eq!(
963 super::macos_volume_entry(&ext),
964 Some((
965 "/Volumes/USB".to_string(),
966 128_000_000_000,
967 128_000_000_000 / 3,
968 "exfat".to_string()
969 ))
970 );
971 }
972
973 #[cfg(target_os = "macos")]
974 #[test]
975 fn test_bsd_disk_sort_key_is_numeric() {
976 use crate::macos_ffi::bsd_disk_sort_key;
977 let mut names = vec!["disk10", "disk2", "disk0", "disk1"];
978 names.sort_by_key(|n| bsd_disk_sort_key(n));
979 assert_eq!(names, vec!["disk0", "disk1", "disk2", "disk10"]);
980 }
981
982 #[cfg(target_os = "windows")]
983 use super::win_ffi::BUS_TYPE_NVME;
984 #[cfg(target_os = "windows")]
985 use super::{combine_model, format_disk_label};
986
987 #[cfg(target_os = "windows")]
988 #[test]
989 fn test_format_disk_label_nvme() {
990 let label = format_disk_label(
992 "Samsung SSD 980 Pro",
993 Some(1_000_204_886_016),
994 BUS_TYPE_NVME,
995 Some(false),
996 );
997 assert_eq!(label, "Samsung SSD 980 Pro 1.0 TB [NVMe SSD]");
998 }
999
1000 #[cfg(target_os = "windows")]
1001 #[test]
1002 fn test_format_disk_label_hdd() {
1003 let label = format_disk_label("WD Blue", Some(2_000_398_934_016), 11, Some(true));
1005 assert_eq!(label, "WD Blue 2.0 TB [HDD]");
1006 }
1007
1008 #[cfg(target_os = "windows")]
1009 #[test]
1010 fn test_format_disk_label_sata_ssd() {
1011 let label = format_disk_label(
1013 "Crucial CT500MX500SSD1",
1014 Some(500_107_862_016),
1015 11,
1016 Some(false),
1017 );
1018 assert_eq!(label, "Crucial CT500MX500SSD1 500 GB [SSD]");
1019 }
1020
1021 #[cfg(target_os = "windows")]
1022 #[test]
1023 fn test_format_disk_label_unknown_seek_penalty_defaults_to_ssd() {
1024 let label = format_disk_label("Some eMMC", Some(64_000_000_000), 13, None);
1026 assert_eq!(label, "Some eMMC 64 GB [SSD]");
1027 }
1028
1029 #[cfg(target_os = "windows")]
1030 #[test]
1031 fn test_format_disk_label_empty_model() {
1032 let label = format_disk_label("", Some(500_107_862_016), 11, Some(false));
1033 assert_eq!(label, "500 GB [SSD]");
1034 }
1035
1036 #[cfg(target_os = "windows")]
1037 #[test]
1038 fn test_combine_model_generic_ata_vendor_suppressed() {
1039 assert_eq!(
1041 combine_model("ATA", "Samsung SSD 860 EVO"),
1042 "Samsung SSD 860 EVO"
1043 );
1044 }
1045
1046 #[cfg(target_os = "windows")]
1047 #[test]
1048 fn test_combine_model_empty_vendor() {
1049 assert_eq!(
1050 combine_model("", "Samsung SSD 980 Pro"),
1051 "Samsung SSD 980 Pro"
1052 );
1053 }
1054
1055 #[cfg(target_os = "windows")]
1056 #[test]
1057 fn test_combine_model_vendor_already_in_product() {
1058 assert_eq!(
1060 combine_model("Samsung", "Samsung SSD 980 Pro"),
1061 "Samsung SSD 980 Pro"
1062 );
1063 }
1064
1065 #[cfg(target_os = "windows")]
1066 #[test]
1067 fn test_combine_model_distinct_vendor_prepended() {
1068 assert_eq!(combine_model("Kingston", "A400 SSD"), "Kingston A400 SSD");
1069 }
1070
1071 #[cfg(target_os = "windows")]
1072 #[test]
1073 fn test_combine_model_empty_product_falls_back_to_vendor() {
1074 assert_eq!(combine_model("SomeVendor", ""), "SomeVendor");
1075 }
1076}