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 | |