ferrix_lib/
parts.rs

1/* parts.rs
2 *
3 * Copyright 2025 Michail Krasnov <mskrasnov07@ya.ru>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17 *
18 * SPDX-License-Identifier: GPL-3.0-or-later
19 */
20
21//! Get information about mounted partitions
22
23use anyhow::{Result, anyhow};
24use libc::statvfs;
25use serde::{Deserialize, Serialize};
26use std::ffi::{CString, c_char};
27use std::fs::{read_dir, read_to_string};
28use std::path::{Path, PathBuf};
29
30use crate::traits::ToJson;
31use crate::utils::Size;
32
33/// List of partitions from `/proc/partitions` file
34#[derive(Debug, Deserialize, Serialize, Clone)]
35pub struct Partitions {
36    pub parts: Vec<Partition>,
37}
38
39impl Partitions {
40    pub fn new() -> Result<Self> {
41        let contents = read_to_string("/proc/partitions")?;
42        Self::from_str(&contents)
43    }
44
45    fn from_str(s: &str) -> Result<Self> {
46        let lines = s.lines().skip(1).filter(|s| {
47            !s.is_empty() && !s.starts_with('m') && !s.contains("loop") && !s.contains("ram")
48        });
49
50        let mut parts = Vec::new();
51        for line in lines {
52            match Partition::try_from(line) {
53                Ok(part) => parts.push(part),
54                Err(why) => return Err(anyhow!("{why}")),
55            }
56        }
57
58        Ok(Self { parts })
59    }
60}
61
62impl ToJson for Partitions {}
63
64#[derive(Debug, Deserialize, Serialize, Clone)]
65pub struct Partition {
66    pub major: usize,
67    pub minor: usize,
68    pub blocks: u64,
69    pub name: String,
70    pub dev_info: DeviceInfo,
71    pub statvfs: Option<FileSystemStats>,
72}
73
74impl Partition {
75    pub fn get_logical_size(&self) -> Option<Size> {
76        let lbsize = self.dev_info.logical_block_size;
77        match lbsize {
78            Some(lbsize) => {
79                let blocks = self.blocks;
80                Some(Size::B(blocks * lbsize))
81            }
82            None => None,
83        }
84    }
85}
86
87impl TryFrom<&str> for Partition {
88    type Error = String;
89    fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
90        let mut chs = value.split_whitespace();
91
92        match (chs.next(), chs.next(), chs.next(), chs.next()) {
93            (Some(major), Some(minor), Some(blocks), Some(name)) => {
94                let major = major.parse::<usize>().map_err(|err| format!("{err}"))?;
95                let minor = minor.parse::<usize>().map_err(|err| format!("{err}"))?;
96                let blocks = blocks.parse::<u64>().map_err(|err| format!("{err}"))?;
97
98                Ok(Self {
99                    major,
100                    minor,
101                    blocks,
102                    name: name.to_string(),
103                    dev_info: DeviceInfo::get(name),
104                    statvfs: FileSystemStats::from_path(Path::new("/dev/").join(name)).ok(), // .map_err(|err| format!("Failed to get file system statistics for device {name}: {err}"))?,
105                })
106            }
107            _ => Err(format!("String '{value}' parsing error")),
108        }
109    }
110}
111
112#[derive(Debug, Deserialize, Serialize, Clone)]
113pub struct DeviceInfo {
114    pub model: Option<String>,
115    pub vendor: Option<String>,
116    pub serial: Option<String>,
117    pub logical_block_size: Option<u64>,
118}
119
120impl DeviceInfo {
121    pub fn get(devname: &str) -> Self {
122        let path = Path::new("/sys/block/").join(devname);
123        let device = path.join("device");
124        let queue = path.join("queue");
125
126        let model = device.join("model");
127        let vendor = device.join("vendor");
128        let serial = device.join("serial");
129
130        let logical_block_size = queue.join("logical_block_size");
131        let logical_block_size = match read_to_string(logical_block_size) {
132            Ok(lbs) => lbs.trim().parse::<u64>().ok(),
133            Err(_) => None,
134        };
135
136        Self {
137            model: read_to_string(model)
138                .ok()
139                .and_then(|m| Some(m.trim().to_string())),
140            vendor: read_to_string(vendor)
141                .ok()
142                .and_then(|v| Some(v.trim().to_string())),
143            serial: read_to_string(serial)
144                .ok()
145                .and_then(|s| Some(s.trim().to_string())),
146            logical_block_size,
147        }
148    }
149
150    pub fn is_none(&self) -> bool {
151        self.model.is_none()
152            && self.vendor.is_none()
153            && self.serial.is_none()
154            && self.logical_block_size.is_none()
155    }
156}
157
158/// Physical disk drives from `/sys/block/` directory
159#[derive(Debug, Deserialize, Serialize, Clone)]
160pub struct Storages {
161    pub storages: Vec<Storage>,
162}
163
164impl Storages {
165    pub fn new() -> Result<Self> {
166        let dir_contents = read_dir("/sys/block")?.filter(|entry| {
167            if entry.is_err() {
168                false
169            } else {
170                let entry = entry.as_ref().unwrap();
171                let s = entry.path().to_string_lossy().to_string();
172                !(s.contains("loop") || s.contains("zram"))
173            }
174        });
175
176        let mut storages = vec![];
177        for dir in dir_contents {
178            let dir = dir?.path();
179            storages.push(Storage::from_pathbuf(&dir)?);
180        }
181        Ok(Self { storages })
182    }
183}
184
185#[derive(Debug, Deserialize, Serialize, Clone)]
186pub struct Storage {
187    /// `/sys/block/` subdirectory name (e.g. `sda`, `mmcblk0`, `nvme0n1`, etc.)
188    pub devname: String,
189
190    pub removable: bool,
191
192    /// Readonly
193    pub ro: bool,
194
195    /// Total disk size, bytes
196    pub size: Size,
197
198    pub hidden: bool,
199
200    pub uuid: Option<String>,
201
202    /// Device model
203    pub model: Option<String>,
204
205    /// Device vendor
206    pub vendor: Option<String>,
207
208    /// Device serial number
209    pub serial: Option<String>,
210
211    /// Firmware revision
212    pub revision: Option<String>,
213
214    pub wwid_eui: Option<String>,
215
216    pub transport: Option<String>,
217}
218
219impl Storage {
220    pub fn from_pathbuf(path: &PathBuf) -> Result<Self> {
221        let read = |file: &str| read_to_string(path.join(file));
222
223        let devname = path
224            .strip_prefix("/sys/block/")?
225            .to_string_lossy()
226            .to_string();
227        let removable = {
228            let data = read("removable")?;
229            if data.trim() == "0" { false } else { true }
230        };
231        let ro = {
232            let data = read("ro")?;
233            if data.trim() == "0" { false } else { true }
234        };
235        let hidden = {
236            let data = read("hidden")?;
237            if data.trim() == "0" { false } else { true }
238        };
239        let size = {
240            let data = read("size")?;
241            Size::B(data.trim().parse()?)
242        };
243        let uuid = read("uuid").and_then(|a| Ok(a.trim().to_string())).ok();
244        let model = read("device/model")
245            .and_then(|a| Ok(a.trim().to_string()))
246            .ok();
247        let vendor = read("device/vendor")
248            .and_then(|a| Ok(a.trim().to_string()))
249            .ok();
250        let serial = read("device/serial")
251            .and_then(|a| Ok(a.trim().to_string()))
252            .ok();
253        let revision = read("device/firmware_rev")
254            .and_then(|a| Ok(a.trim().to_string()))
255            .ok();
256        let transport = read("device/transport")
257            .and_then(|a| Ok(a.trim().to_string()))
258            .ok();
259        let wwid_eui = read("wwid").and_then(|a| Ok(a.replace("eui.", ""))).ok();
260
261        Ok(Self {
262            devname,
263            removable,
264            ro,
265            size,
266            hidden,
267            uuid,
268            model,
269            vendor,
270            serial,
271            transport,
272            wwid_eui,
273            revision,
274        })
275    }
276}
277
278/// Mounted filesystems list from `/proc/mounts` file
279#[derive(Debug, Deserialize, Serialize, Clone)]
280pub struct Mounts {
281    pub mounts: Vec<MountEntry>,
282}
283
284#[derive(Debug, Deserialize, Serialize, Clone)]
285pub struct MountEntry {
286    pub device: String,
287    pub mount_point: String,
288    pub filesystem: String,
289    pub options: String,
290    pub dump: u8,
291    pub pass: u8,
292    pub fstats: Option<FileSystemStats>,
293}
294
295impl TryFrom<&str> for MountEntry {
296    type Error = anyhow::Error;
297
298    fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
299        let values = value.split_whitespace().collect::<Vec<_>>();
300        if values.len() != 6 {
301            return Err(anyhow!(
302                "Format of mount string is incorrect\n(string: \"{value}\")",
303            ));
304        }
305
306        Ok(Self {
307            device: values[0].to_string(),
308            mount_point: values[1].to_string(),
309            filesystem: values[2].to_string(),
310            options: values[3].to_string(),
311            dump: values[4].parse()?,
312            pass: values[5].parse()?,
313            fstats: FileSystemStats::from_path(values[1]).ok(),
314        })
315    }
316}
317
318impl Mounts {
319    pub fn new() -> Result<Self> {
320        let contents = read_to_string("/proc/mounts")?;
321        let lines = contents.lines();
322        let mut mounts = vec![];
323
324        for line in lines {
325            if line.starts_with("/")
326                || line.starts_with("udev")
327                || line.starts_with("sysfs")
328                || line.starts_with("tmpfs")
329            {
330                mounts.push(MountEntry::try_from(line)?);
331            }
332        }
333        Ok(Self { mounts })
334    }
335}
336
337#[derive(Debug, Deserialize, Serialize, Clone, Copy)]
338pub struct FileSystemStats {
339    pub block_size: u64,
340    pub fragment_size: u64,
341    pub total_blocks: u64,
342    pub free_blocks: u64,
343    pub available_blocks: u64,
344    pub total_inodes: u64,
345    pub free_inodes: u64,
346}
347
348impl FileSystemStats {
349    pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self> {
350        let path_str = path
351            .as_ref()
352            .to_str()
353            .ok_or_else(|| anyhow!("Invalid characters in path ()"))?;
354        let c_path = CString::new(path_str)
355            .map_err(|err| anyhow!("Failed to convert Rust string into C string: {err}"))?;
356
357        unsafe { Self::statvfs(c_path.as_ptr()) }
358    }
359
360    unsafe fn statvfs(path: *const c_char) -> Result<Self> {
361        let mut stats: libc::statvfs = unsafe { std::mem::zeroed() };
362        let result = unsafe { statvfs(path, &mut stats) };
363
364        if result == 0 {
365            Ok(Self {
366                block_size: stats.f_bsize as u64,
367                fragment_size: stats.f_frsize as u64,
368                total_blocks: stats.f_blocks as u64,
369                free_blocks: stats.f_bfree as u64,
370                available_blocks: stats.f_bavail as u64,
371                total_inodes: stats.f_files as u64,
372                free_inodes: stats.f_ffree as u64,
373            })
374        } else {
375            Err(anyhow!(
376                "statvfs() failed: errno {}",
377                std::io::Error::last_os_error()
378            ))
379        }
380    }
381
382    pub fn total_bytes(&self) -> u64 {
383        self.total_blocks * self.fragment_size
384    }
385
386    pub fn total_size(&self) -> Size {
387        Size::B(self.total_bytes())
388    }
389
390    pub fn free_bytes(&self) -> u64 {
391        self.free_blocks * self.fragment_size
392    }
393
394    pub fn free_size(&self) -> Size {
395        Size::B(self.free_bytes())
396    }
397
398    pub fn avail_bytes(&self) -> u64 {
399        self.available_blocks * self.fragment_size
400    }
401
402    pub fn avail_size(&self) -> Size {
403        Size::B(self.avail_bytes())
404    }
405
406    pub fn used_bytes(&self) -> u64 {
407        if self.total_bytes() == 0 {
408            return 0;
409        }
410        self.total_bytes() - self.free_bytes()
411    }
412
413    pub fn used_size(&self) -> Size {
414        Size::B(self.used_bytes())
415    }
416
417    pub fn usage_percent(&self) -> f64 {
418        if self.total_bytes() == 0 {
419            return 0.;
420        }
421        let used = self.used_bytes() as f64;
422        let total = self.total_bytes() as f64;
423        (used / total) * 100.
424    }
425}
426
427#[cfg(test)]
428mod tests {
429    use super::*;
430
431    const PARTITIONS: &str = "major minor  #blocks  name
432
433 259        0  250059096 nvme0n1
434 259        1     102400 nvme0n1p1
435 259        2      16384 nvme0n1p2
436 259        3  249068548 nvme0n1p3
437 259        4     866304 nvme0n1p4
438   8        0  468851544 sda
439   8        1     614400 sda1
440   8        2   73138176 sda2
441   8        3  337163264 sda3
442   8        4   57933824 sda4
443 253        0    3976960 zram0";
444
445    #[test]
446    fn partitions_from_str_test() {
447        let parts = Partitions::from_str(PARTITIONS).unwrap();
448        dbg!(&parts);
449        assert_eq!(parts.parts.len(), 10);
450        assert_eq!(&parts.parts[0].name, "nvme0n1");
451        assert_eq!(parts.parts[0].major, 259);
452        assert_eq!(parts.parts[0].minor, 0);
453        assert_eq!(parts.parts[0].blocks, 250059096);
454        let _ = std::fs::write("./test-filesystems.json", parts.to_json_pretty().unwrap());
455    }
456
457    #[test]
458    fn partition_invalid_str_test() {
459        let s = "256 0 nvme";
460        let part = Partition::try_from(s);
461        assert!(part.is_err());
462    }
463
464    #[test]
465    fn partition_valid_str_test() {
466        let s = "255 4 666 sda";
467        let part = Partition::try_from(s);
468        assert!(part.is_ok());
469    }
470}