1// Take a look at the license at the top of the repository in the LICENSE file.
2
3use crate::sys::utils::{get_all_utf8_data, to_cpath};
4use crate::{Disk, DiskKind};
5
6use libc::statvfs;
7use std::ffi::{OsStr, OsString};
8use std::fs;
9use std::mem;
10use std::os::unix::ffi::OsStrExt;
11use std::path::{Path, PathBuf};
12
13macro_rules! cast {
14 ($x:expr) => {
15 u64::from($x)
16 };
17}
18
19pub(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
29impl 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
73impl 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
96fn 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)]
137fn 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
207fn 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