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