Skip to main content

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