Skip to main content

ferrix_lib/
parts.rs

1/* parts.rs
2 *
3 * Copyright 2025-2026 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//!
23//! - [`Partitions`] - parses and represents entries from `/proc/partitions`;
24//! - [`Storages`] - represents physical block devices from `/sys/block`;
25//! - [`Mounts`] - represents currently mounted fs from `/proc/mounts`;
26//! - [`FileSystemStats`] - provides calculated metrics: used space, usage
27//!   percentage based on raw `statvfs` data.
28
29use anyhow::{Result, anyhow};
30use libc::statvfs;
31use serde::{Deserialize, Serialize};
32use std::ffi::{CString, c_char};
33use std::fs::{read_dir, read_to_string};
34use std::path::{Path, PathBuf};
35
36use crate::traits::ToJson;
37use crate::utils::Size;
38
39// NOTE: Is this structure really necessary, since there are `Mounts`?
40/// List of partitions from `/proc/partitions` file
41///
42/// > **Note:** this structure filters out virtual devices like `loop` and
43/// > `ram` to focus on actual block devices and their partitions.
44#[derive(Debug, Deserialize, Serialize, Clone)]
45pub struct Partitions {
46    pub parts: Vec<Partition>,
47}
48
49impl Partitions {
50    pub fn new() -> Result<Self> {
51        let contents = read_to_string("/proc/partitions")?;
52        Self::from_str(&contents)
53    }
54
55    /// Parse a raw string representation of `/proc/partitions` into a `Self`
56    /// structure
57    fn from_str(s: &str) -> Result<Self> {
58        let lines = s.lines().skip(1).filter(|s| {
59            !s.is_empty() && !s.starts_with('m') && !s.contains("loop") && !s.contains("ram")
60        });
61
62        let mut parts = Vec::new();
63        for line in lines {
64            match Partition::try_from(line) {
65                Ok(part) => parts.push(part),
66                Err(why) => return Err(anyhow!("{why}")),
67            }
68        }
69
70        Ok(Self { parts })
71    }
72}
73
74impl ToJson for Partitions {}
75
76/// Single block device partition
77#[derive(Debug, Deserialize, Serialize, Clone)]
78pub struct Partition {
79    /// Major device number
80    pub major: usize,
81
82    /// Minor device num
83    pub minor: usize,
84
85    /// Size of the partition in 1K-blocks
86    pub blocks: u64,
87
88    /// Name of the device (e.g. `sda1`, `nvme0n1p2`)
89    pub name: String,
90
91    /// Hardware-level metadata retrieved from `/sys/block/`
92    pub dev_info: DeviceInfo,
93
94    /// Filesystem statistics, if applicable and retrievable
95    pub statvfs: Option<FileSystemStats>,
96}
97
98impl Partition {
99    /// Calculate the logical sise of the partition
100    ///
101    /// Multiplies the number of blocks by the logical block size. Returns
102    /// `None` of the logical size is unknown
103    pub fn get_logical_size(&self) -> Option<Size> {
104        let lbsize = self.dev_info.logical_block_size;
105        match lbsize {
106            Some(lbsize) => {
107                let blocks = self.blocks;
108                Some(Size::B(blocks * lbsize))
109            }
110            None => None,
111        }
112    }
113}
114
115impl TryFrom<&str> for Partition {
116    type Error = String;
117    fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
118        let mut chs = value.split_whitespace();
119
120        match (chs.next(), chs.next(), chs.next(), chs.next()) {
121            (Some(major), Some(minor), Some(blocks), Some(name)) => {
122                let major = major.parse::<usize>().map_err(|err| format!("{err}"))?;
123                let minor = minor.parse::<usize>().map_err(|err| format!("{err}"))?;
124                let blocks = blocks.parse::<u64>().map_err(|err| format!("{err}"))?;
125
126                Ok(Self {
127                    major,
128                    minor,
129                    blocks,
130                    name: name.to_string(),
131                    dev_info: DeviceInfo::get(name),
132                    statvfs: FileSystemStats::from_path(Path::new("/dev/").join(name)).ok(), // .map_err(|err| format!("Failed to get file system statistics for device {name}: {err}"))?,
133                })
134            }
135            _ => Err(format!("String '{value}' parsing error")),
136        }
137    }
138}
139
140/// Hardware-level metadata for a block device
141///
142/// This data is read directly from the `/sys/block/<device>/device/` and
143/// `/sys/block/<device>/queue/` dirs
144#[derive(Debug, Deserialize, Serialize, Clone)]
145pub struct DeviceInfo {
146    /// The model name of this device
147    pub model: Option<String>,
148
149    /// Manufacturer of the device
150    pub vendor: Option<String>,
151
152    /// The unique serial number of the device
153    pub serial: Option<String>,
154
155    /// The logical block size in bytes (typically 510 or 4096)
156    pub logical_block_size: Option<u64>,
157}
158
159impl DeviceInfo {
160    /// Get device information for the given device name (e.g. `sda`)
161    pub fn get(devname: &str) -> Self {
162        let path = Path::new("/sys/block/").join(devname);
163        let device = path.join("device");
164        let queue = path.join("queue");
165
166        let model = device.join("model");
167        let vendor = device.join("vendor");
168        let serial = device.join("serial");
169
170        let logical_block_size = queue.join("logical_block_size");
171        let logical_block_size = match read_to_string(logical_block_size) {
172            Ok(lbs) => lbs.trim().parse::<u64>().ok(),
173            Err(_) => None,
174        };
175
176        Self {
177            model: read_to_string(model)
178                .ok()
179                .and_then(|m| Some(m.trim().to_string())),
180            vendor: read_to_string(vendor)
181                .ok()
182                .and_then(|v| Some(v.trim().to_string())),
183            serial: read_to_string(serial)
184                .ok()
185                .and_then(|s| Some(s.trim().to_string())),
186            logical_block_size,
187        }
188    }
189
190    /// Returns `true` if all fields in the `DeviceInfo` are `None`
191    pub fn is_none(&self) -> bool {
192        self.model.is_none()
193            && self.vendor.is_none()
194            && self.serial.is_none()
195            && self.logical_block_size.is_none()
196    }
197}
198
199/// Physical disk drives from `/sys/block/` directory
200#[derive(Debug, Deserialize, Serialize, Clone)]
201pub struct Storages {
202    pub storages: Vec<Storage>,
203}
204
205impl Storages {
206    /// Scan `/sys/block/` and populate the list of physical storage devices
207    ///
208    /// > **Note:** this method filters out virtual devies like `loop` and
209    /// > `zram`
210    pub fn new() -> Result<Self> {
211        let dir_contents = read_dir("/sys/block")?.filter(|entry| {
212            if entry.is_err() {
213                false
214            } else {
215                let entry = entry.as_ref().unwrap();
216                let s = entry.path().to_string_lossy().to_string();
217                !(s.contains("loop") || s.contains("zram"))
218            }
219        });
220
221        let mut storages = vec![];
222        for dir in dir_contents {
223            let dir = dir?.path();
224            storages.push(Storage::from_pathbuf(&dir)?);
225        }
226        Ok(Self { storages })
227    }
228}
229
230/// Physical storage device info (e.g. `sda`, `mmcblk0`, `nvme0n1`, etc.)
231#[derive(Debug, Deserialize, Serialize, Clone)]
232pub struct Storage {
233    /// `/sys/block/` subdirectory name (e.g. `sda`, `mmcblk0`, `nvme0n1`, etc.)
234    ///
235    /// (the device name as it appears in `/sys/block/` directory)
236    pub devname: String,
237
238    /// Indicates if the device is removable
239    pub removable: bool,
240
241    /// Indicates if the device is currently mounted or configured as read-only
242    pub ro: bool,
243
244    /// Total disk size, bytes
245    pub size: Size,
246
247    /// Indicates if the device is hidden from the system
248    pub hidden: bool,
249
250    /// The filesystem UUID, if applicable
251    pub uuid: Option<String>,
252
253    /// Device model
254    pub model: Option<String>,
255
256    /// Device vendor
257    pub vendor: Option<String>,
258
259    /// Device serial number
260    pub serial: Option<String>,
261
262    /// Firmware revision
263    pub revision: Option<String>,
264
265    /// The World Wide Name (WWN) of EUI of the device, stripped of the
266    /// `eui.` prefix
267    pub wwid_eui: Option<String>,
268
269    /// The transport protocol used (e.g. `sata`, `nvme`, `usb`, etc.)
270    pub transport: Option<String>,
271}
272
273impl Storage {
274    /// Get a `Storage` instance from a given `/sys/block/` path
275    pub fn from_pathbuf(path: &PathBuf) -> Result<Self> {
276        let read = |file: &str| read_to_string(path.join(file));
277
278        let devname = path
279            .strip_prefix("/sys/block/")?
280            .to_string_lossy()
281            .to_string();
282        let removable = {
283            let data = read("removable")?;
284            if data.trim() == "0" { false } else { true }
285        };
286        let ro = {
287            let data = read("ro")?;
288            if data.trim() == "0" { false } else { true }
289        };
290        let hidden = {
291            let data = read("hidden")?;
292            if data.trim() == "0" { false } else { true }
293        };
294        let size = {
295            let data = read("size")?;
296            Size::B(data.trim().parse()?)
297        };
298        let uuid = read("uuid").and_then(|a| Ok(a.trim().to_string())).ok();
299        let model = read("device/model")
300            .and_then(|a| Ok(a.trim().to_string()))
301            .ok();
302        let vendor = read("device/vendor")
303            .and_then(|a| Ok(a.trim().to_string()))
304            .ok();
305        let serial = read("device/serial")
306            .and_then(|a| Ok(a.trim().to_string()))
307            .ok();
308        let revision = read("device/firmware_rev")
309            .and_then(|a| Ok(a.trim().to_string()))
310            .ok();
311        let transport = read("device/transport")
312            .and_then(|a| Ok(a.trim().to_string()))
313            .ok();
314        let wwid_eui = read("wwid").and_then(|a| Ok(a.replace("eui.", ""))).ok();
315
316        Ok(Self {
317            devname,
318            removable,
319            ro,
320            size,
321            hidden,
322            uuid,
323            model,
324            vendor,
325            serial,
326            transport,
327            wwid_eui,
328            revision,
329        })
330    }
331}
332
333/// Mounted filesystems list from `/proc/mounts` file
334#[derive(Debug, Deserialize, Serialize, Clone)]
335pub struct Mounts {
336    pub mounts: Vec<MountEntry>,
337}
338
339/// Single mounted filesystem entry
340#[derive(Debug, Deserialize, Serialize, Clone)]
341pub struct MountEntry {
342    /// The block device or a virtual fs src (e.g. `/dev/sda1`, `tmpfs`)
343    pub device: String,
344
345    /// The directory where the fs is mounted
346    pub mount_point: String,
347
348    /// The type of the fs (e.g. `ext4`, `btrfs`, `vfat`)
349    pub filesystem: String,
350
351    /// Comma-separated mount options (e.g. `rw,realtime`)
352    pub options: String,
353
354    /// Dump flag (used by the `dump` utility, usually 0)
355    pub dump: u8,
356
357    /// Pass number (used by the `fsck` to determine check order)
358    pub pass: u8,
359
360    /// Filesystem usage statistics, if retrievable
361    pub fstats: Option<FileSystemStats>,
362}
363
364impl TryFrom<&str> for MountEntry {
365    type Error = anyhow::Error;
366
367    fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
368        let values = value.split_whitespace().collect::<Vec<_>>();
369        if values.len() != 6 {
370            return Err(anyhow!(
371                "Format of mount string is incorrect\n(string: \"{value}\")",
372            ));
373        }
374
375        Ok(Self {
376            device: values[0].to_string(),
377            mount_point: values[1].to_string(),
378            filesystem: values[2].to_string(),
379            options: values[3].to_string(),
380            dump: values[4].parse()?,
381            pass: values[5].parse()?,
382            fstats: FileSystemStats::from_path(values[1]).ok(),
383        })
384    }
385}
386
387impl Mounts {
388    pub fn new() -> Result<Self> {
389        let contents = read_to_string("/proc/mounts")?;
390        let lines = contents.lines();
391        let mut mounts = vec![];
392
393        for line in lines {
394            if line.starts_with("/")
395                || line.starts_with("udev")
396                || line.starts_with("sysfs")
397                || line.starts_with("tmpfs")
398            {
399                mounts.push(MountEntry::try_from(line)?);
400            }
401        }
402        Ok(Self { mounts })
403    }
404}
405
406/// Filesystem usage statistics (via `statvfs` C function)
407#[derive(Debug, Deserialize, Serialize, Clone, Copy)]
408pub struct FileSystemStats {
409    /// Block size, bytes
410    pub block_size: u64,
411
412    /// Fragment size, bytes
413    pub fragment_size: u64,
414
415    /// Total number of blocks in this fs
416    pub total_blocks: u64,
417
418    /// Total number of free blocks
419    pub free_blocks: u64,
420
421    /// Total number of free blocks available to non-privileged
422    /// processes
423    pub available_blocks: u64,
424
425    /// Total number of inodes
426    pub total_inodes: u64,
427
428    /// Total number of free inodes
429    pub free_inodes: u64,
430}
431
432impl FileSystemStats {
433    /// Get fs stats for the given path
434    pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self> {
435        let path_str = path
436            .as_ref()
437            .to_str()
438            .ok_or_else(|| anyhow!("Invalid characters in path ()"))?;
439        let c_path = CString::new(path_str)
440            .map_err(|err| anyhow!("Failed to convert Rust string into C string: {err}"))?;
441
442        // SAFETY: we are passing a valid null-terminated C-string to statvfs,
443        // and providing a valid zeroed mutable pointer for the output
444        unsafe { Self::statvfs(c_path.as_ptr()) }
445    }
446
447    /// Unsafe wrapper for the `libc::statvfs` system call
448    unsafe fn statvfs(path: *const c_char) -> Result<Self> {
449        let mut stats: libc::statvfs = unsafe { std::mem::zeroed() };
450        let result = unsafe { statvfs(path, &mut stats) };
451
452        if result == 0 {
453            Ok(Self {
454                block_size: stats.f_bsize as u64,
455                fragment_size: stats.f_frsize as u64,
456                total_blocks: stats.f_blocks as u64,
457                free_blocks: stats.f_bfree as u64,
458                available_blocks: stats.f_bavail as u64,
459                total_inodes: stats.f_files as u64,
460                free_inodes: stats.f_ffree as u64,
461            })
462        } else {
463            Err(anyhow!(
464                "statvfs() failed: errno {}",
465                std::io::Error::last_os_error()
466            ))
467        }
468    }
469
470    /// Calculate the total capacity of the fs in bytes
471    pub fn total_bytes(&self) -> u64 {
472        self.total_blocks * self.fragment_size
473    }
474
475    /// Calculate the total capacity of the fs as a [`Size`] enum
476    pub fn total_size(&self) -> Size {
477        Size::B(self.total_bytes())
478    }
479
480    /// Calculate the free space in bytes
481    pub fn free_bytes(&self) -> u64 {
482        self.free_blocks * self.fragment_size
483    }
484
485    /// Calculate the free space as a [`Size`] enum
486    pub fn free_size(&self) -> Size {
487        Size::B(self.free_bytes())
488    }
489
490    /// Calculate the space available to non-privileged users in bytes
491    pub fn avail_bytes(&self) -> u64 {
492        self.available_blocks * self.fragment_size
493    }
494
495    pub fn avail_size(&self) -> Size {
496        Size::B(self.avail_bytes())
497    }
498
499    /// Calculate the used space in bytes
500    pub fn used_bytes(&self) -> u64 {
501        if self.total_bytes() == 0 {
502            return 0;
503        }
504        self.total_bytes() - self.free_bytes()
505    }
506
507    pub fn used_size(&self) -> Size {
508        Size::B(self.used_bytes())
509    }
510
511    /// Calculate the percentage of the fs that is currently used (0.0 to 100.0)
512    pub fn usage_percent(&self) -> f64 {
513        if self.total_bytes() == 0 {
514            return 0.;
515        }
516        let used = self.used_bytes() as f64;
517        let total = self.total_bytes() as f64;
518        (used / total) * 100.
519    }
520}
521
522#[cfg(test)]
523mod tests {
524    use super::*;
525
526    const PARTITIONS: &str = "major minor  #blocks  name
527
528 259        0  250059096 nvme0n1
529 259        1     102400 nvme0n1p1
530 259        2      16384 nvme0n1p2
531 259        3  249068548 nvme0n1p3
532 259        4     866304 nvme0n1p4
533   8        0  468851544 sda
534   8        1     614400 sda1
535   8        2   73138176 sda2
536   8        3  337163264 sda3
537   8        4   57933824 sda4
538 253        0    3976960 zram0";
539
540    #[test]
541    fn partitions_from_str_test() {
542        let parts = Partitions::from_str(PARTITIONS).unwrap();
543        dbg!(&parts);
544        assert_eq!(parts.parts.len(), 10);
545        assert_eq!(&parts.parts[0].name, "nvme0n1");
546        assert_eq!(parts.parts[0].major, 259);
547        assert_eq!(parts.parts[0].minor, 0);
548        assert_eq!(parts.parts[0].blocks, 250059096);
549        let _ = std::fs::write("./test-filesystems.json", parts.to_json_pretty().unwrap());
550    }
551
552    #[test]
553    fn partition_invalid_str_test() {
554        let s = "256 0 nvme";
555        let part = Partition::try_from(s);
556        assert!(part.is_err());
557    }
558
559    #[test]
560    fn partition_valid_str_test() {
561        let s = "255 4 666 sda";
562        let part = Partition::try_from(s);
563        assert!(part.is_ok());
564    }
565}