| 1 | // Take a look at the license at the top of the repository in the LICENSE file. | 
| 2 |  | 
|---|
| 3 | use crate::sys::utils::{get_all_utf8_data, to_cpath}; | 
|---|
| 4 | use crate::{Disk, DiskKind}; | 
|---|
| 5 |  | 
|---|
| 6 | use libc::statvfs; | 
|---|
| 7 | use std::ffi::{OsStr, OsString}; | 
|---|
| 8 | use std::fs; | 
|---|
| 9 | use std::mem; | 
|---|
| 10 | use std::os::unix::ffi::OsStrExt; | 
|---|
| 11 | use std::path::{Path, PathBuf}; | 
|---|
| 12 |  | 
|---|
| 13 | macro_rules! cast { | 
|---|
| 14 | ($x:expr) => { | 
|---|
| 15 | u64::from($x) | 
|---|
| 16 | }; | 
|---|
| 17 | } | 
|---|
| 18 |  | 
|---|
| 19 | pub(crate) struct DiskInner { | 
|---|
| 20 | type_: DiskKind, | 
|---|
| 21 | device_name: OsString, | 
|---|
| 22 | file_system: OsString, | 
|---|
| 23 | mount_point: PathBuf, | 
|---|
| 24 | total_space: u64, | 
|---|
| 25 | available_space: u64, | 
|---|
| 26 | is_removable: bool, | 
|---|
| 27 | } | 
|---|
| 28 |  | 
|---|
| 29 | impl DiskInner { | 
|---|
| 30 | pub(crate) fn kind(&self) -> DiskKind { | 
|---|
| 31 | self.type_ | 
|---|
| 32 | } | 
|---|
| 33 |  | 
|---|
| 34 | pub(crate) fn name(&self) -> &OsStr { | 
|---|
| 35 | &self.device_name | 
|---|
| 36 | } | 
|---|
| 37 |  | 
|---|
| 38 | pub(crate) fn file_system(&self) -> &OsStr { | 
|---|
| 39 | &self.file_system | 
|---|
| 40 | } | 
|---|
| 41 |  | 
|---|
| 42 | pub(crate) fn mount_point(&self) -> &Path { | 
|---|
| 43 | &self.mount_point | 
|---|
| 44 | } | 
|---|
| 45 |  | 
|---|
| 46 | pub(crate) fn total_space(&self) -> u64 { | 
|---|
| 47 | self.total_space | 
|---|
| 48 | } | 
|---|
| 49 |  | 
|---|
| 50 | pub(crate) fn available_space(&self) -> u64 { | 
|---|
| 51 | self.available_space | 
|---|
| 52 | } | 
|---|
| 53 |  | 
|---|
| 54 | pub(crate) fn is_removable(&self) -> bool { | 
|---|
| 55 | self.is_removable | 
|---|
| 56 | } | 
|---|
| 57 |  | 
|---|
| 58 | pub(crate) fn refresh(&mut self) -> bool { | 
|---|
| 59 | unsafe { | 
|---|
| 60 | let mut stat: statvfs = mem::zeroed(); | 
|---|
| 61 | let mount_point_cpath = to_cpath(&self.mount_point); | 
|---|
| 62 | if retry_eintr!(statvfs(mount_point_cpath.as_ptr() as *const _, &mut stat)) == 0 { | 
|---|
| 63 | let tmp = cast!(stat.f_bsize).saturating_mul(cast!(stat.f_bavail)); | 
|---|
| 64 | self.available_space = cast!(tmp); | 
|---|
| 65 | true | 
|---|
| 66 | } else { | 
|---|
| 67 | false | 
|---|
| 68 | } | 
|---|
| 69 | } | 
|---|
| 70 | } | 
|---|
| 71 | } | 
|---|
| 72 |  | 
|---|
| 73 | impl crate::DisksInner { | 
|---|
| 74 | pub(crate) fn new() -> Self { | 
|---|
| 75 | Self { | 
|---|
| 76 | disks: Vec::with_capacity(2), | 
|---|
| 77 | } | 
|---|
| 78 | } | 
|---|
| 79 |  | 
|---|
| 80 | pub(crate) fn refresh_list(&mut self) { | 
|---|
| 81 | get_all_list( | 
|---|
| 82 | &mut self.disks, | 
|---|
| 83 | &get_all_utf8_data(file_path: "/proc/mounts", size:16_385).unwrap_or_default(), | 
|---|
| 84 | ) | 
|---|
| 85 | } | 
|---|
| 86 |  | 
|---|
| 87 | pub(crate) fn list(&self) -> &[Disk] { | 
|---|
| 88 | &self.disks | 
|---|
| 89 | } | 
|---|
| 90 |  | 
|---|
| 91 | pub(crate) fn list_mut(&mut self) -> &mut [Disk] { | 
|---|
| 92 | &mut self.disks | 
|---|
| 93 | } | 
|---|
| 94 | } | 
|---|
| 95 |  | 
|---|
| 96 | fn new_disk( | 
|---|
| 97 | device_name: &OsStr, | 
|---|
| 98 | mount_point: &Path, | 
|---|
| 99 | file_system: &OsStr, | 
|---|
| 100 | removable_entries: &[PathBuf], | 
|---|
| 101 | ) -> Option<Disk> { | 
|---|
| 102 | let mount_point_cpath = to_cpath(mount_point); | 
|---|
| 103 | let type_ = find_type_for_device_name(device_name); | 
|---|
| 104 | let mut total = 0; | 
|---|
| 105 | let mut available = 0; | 
|---|
| 106 | unsafe { | 
|---|
| 107 | let mut stat: statvfs = mem::zeroed(); | 
|---|
| 108 | if retry_eintr!(statvfs(mount_point_cpath.as_ptr() as *const _, &mut stat)) == 0 { | 
|---|
| 109 | let bsize = cast!(stat.f_bsize); | 
|---|
| 110 | let blocks = cast!(stat.f_blocks); | 
|---|
| 111 | let bavail = cast!(stat.f_bavail); | 
|---|
| 112 | total = bsize.saturating_mul(blocks); | 
|---|
| 113 | available = bsize.saturating_mul(bavail); | 
|---|
| 114 | } | 
|---|
| 115 | if total == 0 { | 
|---|
| 116 | return None; | 
|---|
| 117 | } | 
|---|
| 118 | let mount_point = mount_point.to_owned(); | 
|---|
| 119 | let is_removable = removable_entries | 
|---|
| 120 | .iter() | 
|---|
| 121 | .any(|e| e.as_os_str() == device_name); | 
|---|
| 122 | Some(Disk { | 
|---|
| 123 | inner: DiskInner { | 
|---|
| 124 | type_, | 
|---|
| 125 | device_name: device_name.to_owned(), | 
|---|
| 126 | file_system: file_system.to_owned(), | 
|---|
| 127 | mount_point, | 
|---|
| 128 | total_space: cast!(total), | 
|---|
| 129 | available_space: cast!(available), | 
|---|
| 130 | is_removable, | 
|---|
| 131 | }, | 
|---|
| 132 | }) | 
|---|
| 133 | } | 
|---|
| 134 | } | 
|---|
| 135 |  | 
|---|
| 136 | #[ allow(clippy::manual_range_contains)] | 
|---|
| 137 | fn find_type_for_device_name(device_name: &OsStr) -> DiskKind { | 
|---|
| 138 | // The format of devices are as follows: | 
|---|
| 139 | //  - device_name is symbolic link in the case of /dev/mapper/ | 
|---|
| 140 | //     and /dev/root, and the target is corresponding device under | 
|---|
| 141 | //     /sys/block/ | 
|---|
| 142 | //  - In the case of /dev/sd, the format is /dev/sd[a-z][1-9], | 
|---|
| 143 | //     corresponding to /sys/block/sd[a-z] | 
|---|
| 144 | //  - In the case of /dev/nvme, the format is /dev/nvme[0-9]n[0-9]p[0-9], | 
|---|
| 145 | //     corresponding to /sys/block/nvme[0-9]n[0-9] | 
|---|
| 146 | //  - In the case of /dev/mmcblk, the format is /dev/mmcblk[0-9]p[0-9], | 
|---|
| 147 | //     corresponding to /sys/block/mmcblk[0-9] | 
|---|
| 148 | let device_name_path = device_name.to_str().unwrap_or_default(); | 
|---|
| 149 | let real_path = fs::canonicalize(device_name).unwrap_or_else(|_| PathBuf::from(device_name)); | 
|---|
| 150 | let mut real_path = real_path.to_str().unwrap_or_default(); | 
|---|
| 151 | if device_name_path.starts_with( "/dev/mapper/") { | 
|---|
| 152 | // Recursively solve, for example /dev/dm-0 | 
|---|
| 153 | if real_path != device_name_path { | 
|---|
| 154 | return find_type_for_device_name(OsStr::new(&real_path)); | 
|---|
| 155 | } | 
|---|
| 156 | } else if device_name_path.starts_with( "/dev/sd") || device_name_path.starts_with( "/dev/vd") { | 
|---|
| 157 | // Turn "sda1" into "sda" or "vda1" into "vda" | 
|---|
| 158 | real_path = real_path.trim_start_matches( "/dev/"); | 
|---|
| 159 | real_path = real_path.trim_end_matches(|c| c >= '0'&& c <= '9'); | 
|---|
| 160 | } else if device_name_path.starts_with( "/dev/nvme") { | 
|---|
| 161 | // Turn "nvme0n1p1" into "nvme0n1" | 
|---|
| 162 | real_path = match real_path.find( 'p') { | 
|---|
| 163 | Some(idx) => &real_path[ "/dev/".len()..idx], | 
|---|
| 164 | None => &real_path[ "/dev/".len()..], | 
|---|
| 165 | }; | 
|---|
| 166 | } else if device_name_path.starts_with( "/dev/root") { | 
|---|
| 167 | // Recursively solve, for example /dev/mmcblk0p1 | 
|---|
| 168 | if real_path != device_name_path { | 
|---|
| 169 | return find_type_for_device_name(OsStr::new(&real_path)); | 
|---|
| 170 | } | 
|---|
| 171 | } else if device_name_path.starts_with( "/dev/mmcblk") { | 
|---|
| 172 | // Turn "mmcblk0p1" into "mmcblk0" | 
|---|
| 173 | real_path = match real_path.find( 'p') { | 
|---|
| 174 | Some(idx) => &real_path[ "/dev/".len()..idx], | 
|---|
| 175 | None => &real_path[ "/dev/".len()..], | 
|---|
| 176 | }; | 
|---|
| 177 | } else { | 
|---|
| 178 | // Default case: remove /dev/ and expects the name presents under /sys/block/ | 
|---|
| 179 | // For example, /dev/dm-0 to dm-0 | 
|---|
| 180 | real_path = real_path.trim_start_matches( "/dev/"); | 
|---|
| 181 | } | 
|---|
| 182 |  | 
|---|
| 183 | let trimmed: &OsStr = OsStrExt::from_bytes(real_path.as_bytes()); | 
|---|
| 184 |  | 
|---|
| 185 | let path = Path::new( "/sys/block/") | 
|---|
| 186 | .to_owned() | 
|---|
| 187 | .join(trimmed) | 
|---|
| 188 | .join( "queue/rotational"); | 
|---|
| 189 | // Normally, this file only contains '0' or '1' but just in case, we get 8 bytes... | 
|---|
| 190 | match get_all_utf8_data(path, 8) | 
|---|
| 191 | .unwrap_or_default() | 
|---|
| 192 | .trim() | 
|---|
| 193 | .parse() | 
|---|
| 194 | .ok() | 
|---|
| 195 | { | 
|---|
| 196 | // The disk is marked as rotational so it's a HDD. | 
|---|
| 197 | Some(1) => DiskKind::HDD, | 
|---|
| 198 | // The disk is marked as non-rotational so it's very likely a SSD. | 
|---|
| 199 | Some(0) => DiskKind::SSD, | 
|---|
| 200 | // Normally it shouldn't happen but welcome to the wonderful world of IT! :D | 
|---|
| 201 | Some(x) => DiskKind::Unknown(x), | 
|---|
| 202 | // The information isn't available... | 
|---|
| 203 | None => DiskKind::Unknown(-1), | 
|---|
| 204 | } | 
|---|
| 205 | } | 
|---|
| 206 |  | 
|---|
| 207 | fn get_all_list(container: &mut Vec<Disk>, content: &str) { | 
|---|
| 208 | container.clear(); | 
|---|
| 209 | // The goal of this array is to list all removable devices (the ones whose name starts with | 
|---|
| 210 | // "usb-"). | 
|---|
| 211 | let removable_entries = match fs::read_dir( "/dev/disk/by-id/") { | 
|---|
| 212 | Ok(r) => r | 
|---|
| 213 | .filter_map(|res| Some(res.ok()?.path())) | 
|---|
| 214 | .filter_map(|e| { | 
|---|
| 215 | if e.file_name() | 
|---|
| 216 | .and_then(|x| Some(x.to_str()?.starts_with( "usb-"))) | 
|---|
| 217 | .unwrap_or_default() | 
|---|
| 218 | { | 
|---|
| 219 | e.canonicalize().ok() | 
|---|
| 220 | } else { | 
|---|
| 221 | None | 
|---|
| 222 | } | 
|---|
| 223 | }) | 
|---|
| 224 | .collect::<Vec<PathBuf>>(), | 
|---|
| 225 | _ => Vec::new(), | 
|---|
| 226 | }; | 
|---|
| 227 |  | 
|---|
| 228 | for disk in content | 
|---|
| 229 | .lines() | 
|---|
| 230 | .map(|line| { | 
|---|
| 231 | let line = line.trim_start(); | 
|---|
| 232 | // mounts format | 
|---|
| 233 | // http://man7.org/linux/man-pages/man5/fstab.5.html | 
|---|
| 234 | // fs_spec<tab>fs_file<tab>fs_vfstype<tab>other fields | 
|---|
| 235 | let mut fields = line.split_whitespace(); | 
|---|
| 236 | let fs_spec = fields.next().unwrap_or( ""); | 
|---|
| 237 | let fs_file = fields | 
|---|
| 238 | .next() | 
|---|
| 239 | .unwrap_or( "") | 
|---|
| 240 | .replace( "\\ 134", "\\ ") | 
|---|
| 241 | .replace( "\\ 040", " ") | 
|---|
| 242 | .replace( "\\ 011", "\t ") | 
|---|
| 243 | .replace( "\\ 012", "\n "); | 
|---|
| 244 | let fs_vfstype = fields.next().unwrap_or( ""); | 
|---|
| 245 | (fs_spec, fs_file, fs_vfstype) | 
|---|
| 246 | }) | 
|---|
| 247 | .filter(|(fs_spec, fs_file, fs_vfstype)| { | 
|---|
| 248 | // Check if fs_vfstype is one of our 'ignored' file systems. | 
|---|
| 249 | let filtered = match *fs_vfstype { | 
|---|
| 250 | "rootfs"| // https://www.kernel.org/doc/Documentation/filesystems/ramfs-rootfs-initramfs.txt | 
|---|
| 251 | "sysfs"| // pseudo file system for kernel objects | 
|---|
| 252 | "proc"|  // another pseudo file system | 
|---|
| 253 | "devtmpfs"| | 
|---|
| 254 | "cgroup"| | 
|---|
| 255 | "cgroup2"| | 
|---|
| 256 | "pstore"| // https://www.kernel.org/doc/Documentation/ABI/testing/pstore | 
|---|
| 257 | "squashfs"| // squashfs is a compressed read-only file system (for snaps) | 
|---|
| 258 | "rpc_pipefs"| // The pipefs pseudo file system service | 
|---|
| 259 | "iso9660"// optical media | 
|---|
| 260 | => true, | 
|---|
| 261 | "tmpfs"=> !cfg!(feature = "linux-tmpfs"), | 
|---|
| 262 | // calling statvfs on a mounted CIFS or NFS may hang, when they are mounted with option: hard | 
|---|
| 263 | "cifs"| "nfs"| "nfs4"=> !cfg!(feature = "linux-netdevs"), | 
|---|
| 264 | _ => false, | 
|---|
| 265 | }; | 
|---|
| 266 |  | 
|---|
| 267 | !(filtered || | 
|---|
| 268 | fs_file.starts_with( "/sys") || // check if fs_file is an 'ignored' mount point | 
|---|
| 269 | fs_file.starts_with( "/proc") || | 
|---|
| 270 | (fs_file.starts_with( "/run") && !fs_file.starts_with( "/run/media")) || | 
|---|
| 271 | fs_spec.starts_with( "sunrpc")) | 
|---|
| 272 | }) | 
|---|
| 273 | .filter_map(|(fs_spec, fs_file, fs_vfstype)| { | 
|---|
| 274 | new_disk( | 
|---|
| 275 | fs_spec.as_ref(), | 
|---|
| 276 | Path::new(&fs_file), | 
|---|
| 277 | fs_vfstype.as_ref(), | 
|---|
| 278 | &removable_entries, | 
|---|
| 279 | ) | 
|---|
| 280 | }) | 
|---|
| 281 | { | 
|---|
| 282 | container.push(disk); | 
|---|
| 283 | } | 
|---|
| 284 | } | 
|---|
| 285 |  | 
|---|
| 286 | // #[test] | 
|---|
| 287 | // fn check_all_list() { | 
|---|
| 288 | //     let disks = get_all_disks_inner( | 
|---|
| 289 | //         r#"tmpfs /proc tmpfs rw,seclabel,relatime 0 0 | 
|---|
| 290 | // proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0 | 
|---|
| 291 | // systemd-1 /proc/sys/fs/binfmt_misc autofs rw,relatime,fd=29,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=17771 0 0 | 
|---|
| 292 | // tmpfs /sys tmpfs rw,seclabel,relatime 0 0 | 
|---|
| 293 | // sysfs /sys sysfs rw,seclabel,nosuid,nodev,noexec,relatime 0 0 | 
|---|
| 294 | // securityfs /sys/kernel/security securityfs rw,nosuid,nodev,noexec,relatime 0 0 | 
|---|
| 295 | // cgroup2 /sys/fs/cgroup cgroup2 rw,seclabel,nosuid,nodev,noexec,relatime,nsdelegate 0 0 | 
|---|
| 296 | // pstore /sys/fs/pstore pstore rw,seclabel,nosuid,nodev,noexec,relatime 0 0 | 
|---|
| 297 | // none /sys/fs/bpf bpf rw,nosuid,nodev,noexec,relatime,mode=700 0 0 | 
|---|
| 298 | // configfs /sys/kernel/config configfs rw,nosuid,nodev,noexec,relatime 0 0 | 
|---|
| 299 | // selinuxfs /sys/fs/selinux selinuxfs rw,relatime 0 0 | 
|---|
| 300 | // debugfs /sys/kernel/debug debugfs rw,seclabel,nosuid,nodev,noexec,relatime 0 0 | 
|---|
| 301 | // tmpfs /dev/shm tmpfs rw,seclabel,relatime 0 0 | 
|---|
| 302 | // devpts /dev/pts devpts rw,seclabel,relatime,gid=5,mode=620,ptmxmode=666 0 0 | 
|---|
| 303 | // tmpfs /sys/fs/selinux tmpfs rw,seclabel,relatime 0 0 | 
|---|
| 304 | // /dev/vda2 /proc/filesystems xfs rw,seclabel,relatime,attr2,inode64,logbufs=8,logbsize=32k,noquota 0 0 | 
|---|
| 305 | // "#, | 
|---|
| 306 | //     ); | 
|---|
| 307 | //     assert_eq!(disks.len(), 1); | 
|---|
| 308 | //     assert_eq!( | 
|---|
| 309 | //         disks[0], | 
|---|
| 310 | //         Disk { | 
|---|
| 311 | //             type_: DiskType::Unknown(-1), | 
|---|
| 312 | //             name: OsString::from("devpts"), | 
|---|
| 313 | //             file_system: vec![100, 101, 118, 112, 116, 115], | 
|---|
| 314 | //             mount_point: PathBuf::from("/dev/pts"), | 
|---|
| 315 | //             total_space: 0, | 
|---|
| 316 | //             available_space: 0, | 
|---|
| 317 | //         } | 
|---|
| 318 | //     ); | 
|---|
| 319 | // } | 
|---|
| 320 |  | 
|---|