sysinfo/linux/
disk.rs

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("/proc/mounts", 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// }