use crate::unit::DataSize;
use crate::util::{blkdev_sector_size, read_to_string_mut};
use std::convert::TryInto;
use std::path::Path;
use std::{fs, io};
use byte_parser::{parse_iter, ParseIterator, StrParser};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Partitions {
raw: String,
}
impl Partitions {
fn path() -> &'static Path {
Path::new("/proc/partitions")
}
#[cfg(test)]
fn from_string(raw: String) -> Self {
Self { raw }
}
pub fn read() -> io::Result<Self> {
Ok(Self {
raw: fs::read_to_string(Self::path())?,
})
}
pub fn reload(&mut self) -> io::Result<()> {
read_to_string_mut(Self::path(), &mut self.raw)
}
pub fn entries<'a>(&'a self) -> impl Iterator<Item = PartitionEntry<'a>> {
self.raw
.trim()
.split('\n')
.skip(2) .map(PartitionEntry::from_str)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PartitionEntry<'a> {
raw: &'a str,
}
impl<'a> PartitionEntry<'a> {
fn from_str(raw: &'a str) -> Self {
Self { raw }
}
pub fn values(&self) -> impl Iterator<Item = &'a str> {
self.raw.split(' ').map(str::trim).filter(|s| !s.is_empty())
}
pub fn major(&self) -> Option<usize> {
self.values().nth(0)?.parse().ok()
}
pub fn minor(&self) -> Option<usize> {
self.values().nth(1)?.parse().ok()
}
pub fn blocks(&self) -> Option<usize> {
self.values().nth(2)?.parse().ok()
}
pub fn name(&self) -> Option<&'a str> {
self.values().nth(3)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MountPoints {
raw: String,
}
impl MountPoints {
fn path() -> &'static Path {
Path::new("/proc/self/mountinfo")
}
#[cfg(test)]
fn from_string(raw: String) -> Self {
Self { raw }
}
pub fn read() -> io::Result<Self> {
Ok(Self {
raw: fs::read_to_string(Self::path())?,
})
}
pub fn reload(&mut self) -> io::Result<()> {
read_to_string_mut(Self::path(), &mut self.raw)
}
pub fn points<'a>(&'a self) -> impl Iterator<Item = MountPoint<'a>> {
self.raw.trim().split('\n').map(MountPoint::from_str)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MountPoint<'a> {
raw: &'a str,
}
impl<'a> MountPoint<'a> {
fn from_str(raw: &'a str) -> Self {
Self { raw }
}
#[inline]
pub fn values(&self) -> impl Iterator<Item = &'a str> {
self.raw.split(' ')
}
pub fn mount_id(&self) -> Option<usize> {
self.values().nth(0)?.parse().ok()
}
pub fn parent_id(&self) -> Option<usize> {
self.values().nth(1)?.parse().ok()
}
#[inline]
pub fn major_minor(&self) -> Option<&'a str> {
self.values().nth(2)
}
pub fn major(&self) -> Option<usize> {
self.major_minor()?.split(':').nth(0)?.parse().ok()
}
pub fn minor(&self) -> Option<usize> {
self.major_minor()?.split(':').nth(1)?.parse().ok()
}
pub fn root(&self) -> Option<&'a str> {
self.values().nth(3)
}
pub fn mount_point(&self) -> Option<&'a str> {
self.values().nth(4)
}
pub fn mount_options(&self) -> Option<&'a str> {
self.values().nth(5)
}
pub fn optional_fields(
&self,
) -> impl Iterator<Item = (&'a str, Option<&'a str>)> {
self.values().skip(6).take_while(|&i| i != "-").map(|opt| {
let mut iters = opt.split(':');
(
iters.next().unwrap(),
iters.next(), )
})
}
fn after_separator(&self) -> impl Iterator<Item = &'a str> {
self.values().skip(5).skip_while(|&i| i != "-").skip(1) }
pub fn filesystem_type(&self) -> Option<&'a str> {
self.after_separator().nth(0)
}
pub fn mount_source(&self) -> Option<&'a str> {
self.after_separator().nth(1)
}
pub fn super_options(&self) -> Option<&'a str> {
self.after_separator().nth(2)
}
pub fn stats(&self) -> io::Result<FsStat> {
FsStat::read(self.mount_point().unwrap_or(""))
}
}
#[derive(Clone)]
pub struct FsStat {
raw: libc::statfs,
}
impl FsStat {
pub fn read(path: impl AsRef<Path>) -> io::Result<Self> {
crate::util::statfs(path).map(|raw| Self { raw })
}
pub fn has_blocks(&self) -> bool {
self.total_blocks().map(|b| b > 0).unwrap_or(false)
}
pub fn block_size(&self) -> Option<usize> {
self.raw.f_bsize.try_into().ok()
}
pub fn total_blocks(&self) -> Option<usize> {
self.raw.f_blocks.try_into().ok()
}
pub fn free_blocks(&self) -> Option<usize> {
self.raw.f_bfree.try_into().ok()
}
pub fn available_blocks(&self) -> Option<usize> {
self.raw.f_bavail.try_into().ok()
}
pub fn used_blocks(&self) -> Option<usize> {
Some(self.total_blocks()? - self.free_blocks()?)
}
pub fn total(&self) -> Option<DataSize> {
DataSize::from_size_bytes(self.total_blocks()? * self.block_size()?)
}
pub fn free(&self) -> Option<DataSize> {
DataSize::from_size_bytes(self.free_blocks()? * self.block_size()?)
}
pub fn available(&self) -> Option<DataSize> {
DataSize::from_size_bytes(self.available_blocks()? * self.block_size()?)
}
pub fn used(&self) -> Option<DataSize> {
DataSize::from_size_bytes(self.used_blocks()? * self.block_size()?)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Raids {
raw: String,
}
impl Raids {
fn path() -> &'static Path {
Path::new("/proc/mdstat")
}
#[cfg(test)]
fn from_string(raw: String) -> Self {
Self { raw }
}
pub fn read() -> io::Result<Self> {
Ok(Self {
raw: fs::read_to_string(Self::path())?,
})
}
pub fn reload(&mut self) -> io::Result<()> {
read_to_string_mut(Self::path(), &mut self.raw)
}
pub fn raids(&self) -> impl Iterator<Item = Raid<'_>> {
let mut first_line = false;
parse_iter(StrParser::new(self.raw.trim()), move |parser| {
if !first_line {
parser.consume_while_byte_fn(|&b| b != b'\n');
parser.advance();
first_line = true;
}
parser.peek()?;
let key = parser
.record()
.while_byte_fn(|&b| b != b':')
.consume_to_str()
.trim();
if key == "unused devices" {
return None;
}
parser.advance();
let mut parser = parser.record();
let mut one = false;
loop {
if one && matches!(parser.peek(), Some(b'\n')) {
let s = parser.to_str().trim();
parser.advance();
return Some(Raid::from_str(key, s));
}
if one {
one = false;
continue;
}
match parser.next() {
Some(b'\n') => one = true,
None => {
return Some(Raid::from_str(
key,
parser.to_str().trim(),
));
}
_ => {}
}
}
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Raid<'a> {
name: &'a str,
raw: &'a str,
}
impl<'a> Raid<'a> {
fn from_str(name: &'a str, raw: &'a str) -> Self {
Self { name, raw }
}
#[inline]
pub fn values(
&self,
) -> impl Iterator<Item = impl Iterator<Item = &'a str>> {
self.raw.split('\n').map(str::trim).map(|l| l.split(' '))
}
pub fn name(&self) -> &'a str {
self.name
}
pub fn state(&self) -> Option<&'a str> {
self.values().nth(0)?.nth(0)
}
fn line(&self, line: usize) -> impl Iterator<Item = &'a str> {
let mut iter = self.values().nth(line);
std::iter::from_fn(move || iter.as_mut()?.next())
}
pub fn kind(&self) -> Option<&'a str> {
self.line(0).nth(1)
}
pub fn devices(&self) -> impl Iterator<Item = (usize, &'a str)> {
self.line(0).skip(2).filter_map(|dev| {
let mut split = dev.split(&['[', ']'][..]);
let name = split.next()?;
Some((split.next()?.parse().ok()?, name))
})
}
pub fn usable_blocks(&self) -> Option<usize> {
self.line(1).nth(0)?.parse().ok()
}
pub fn used_devices(&self) -> Option<usize> {
self.line(1)
.find(|l| l.starts_with('['))?
.split('/')
.nth(0)?
.strip_prefix('[')?
.parse()
.ok()
}
pub fn ideal_devices(&self) -> Option<usize> {
self.line(1)
.find(|l| l.starts_with('['))?
.split('/')
.nth(1)?
.strip_suffix(']')?
.parse()
.ok()
}
pub fn progress(&self) -> Option<&'a str> {
let l = self.raw.split('\n').nth(2)?.trim();
l.starts_with('[').then(|| l)
}
pub fn stats(&self) -> io::Result<FsStat> {
FsStat::read(format!("/dev/{}", self.name()))
}
}
pub fn sector_size(path: impl AsRef<Path>) -> io::Result<u64> {
blkdev_sector_size(fs::File::open(path)?)
}
#[cfg(test)]
mod tests {
use super::*;
fn partitions() -> Partitions {
Partitions::from_string(
"\
major minor #blocks name
7 0 142152 loop0
7 1 101528 loop1
259 0 500107608 nvme0n1
259 1 510976 nvme0n1p1\n\
"
.into(),
)
}
fn cmp_entry(
major: usize,
minor: usize,
blocks: usize,
name: &str,
e: &PartitionEntry<'_>,
) {
assert_eq!(e.major().unwrap(), major);
assert_eq!(e.minor().unwrap(), minor);
assert_eq!(e.blocks().unwrap(), blocks);
assert_eq!(e.name().unwrap(), name);
}
#[test]
fn all_partitions() {
let part = partitions();
let mut e = part.entries();
println!("e: {:?}", part.entries().collect::<Vec<_>>());
cmp_entry(7, 0, 142152, "loop0", &e.next().unwrap());
cmp_entry(7, 1, 101528, "loop1", &e.next().unwrap());
cmp_entry(259, 0, 500107608, "nvme0n1", &e.next().unwrap());
cmp_entry(259, 1, 510976, "nvme0n1p1", &e.next().unwrap());
assert!(e.next().is_none());
}
fn mount_points() -> MountPoints {
MountPoints::from_string("\
26 29 0:5 / /dev rw,nosuid,noexec,relatime shared:2 - devtmpfs udev rw,size=8123832k,nr_inodes=2030958,mode=755
27 26 0:24 / /dev/pts rw,nosuid,noexec,relatime shared:3 - devpts devpts rw,gid=5,mode=620,ptmxmode=000
35 33 0:30 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime shared:11 other - cgroup cgroup rw,xattr,name=systemd
2509 28 0:25 /snapd/ns /run/snapd/ns rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,size=1631264k,mode=755
2893 2509 0:4 mnt:[4026532961] /run/snapd/ns/snap-store.mnt rw - nsfs nsfs rw\n\
".into())
}
fn cmp_point(
mount_id: usize,
parent_id: usize,
major_minor: &str,
root: &str,
mount_point: &str,
mount_options: &str,
optional_fields: &[(&str, Option<&str>)],
filesystem_type: &str,
mount_source: &str,
super_options: &str,
point: &MountPoint<'_>,
) {
assert_eq!(point.mount_id().unwrap(), mount_id);
assert_eq!(point.parent_id().unwrap(), parent_id);
assert_eq!(point.major_minor().unwrap(), major_minor);
assert_eq!(point.root().unwrap(), root);
assert_eq!(point.mount_point().unwrap(), mount_point);
assert_eq!(point.mount_options().unwrap(), mount_options);
assert_eq!(
point.optional_fields().collect::<Vec<_>>(),
optional_fields
);
assert_eq!(point.filesystem_type().unwrap(), filesystem_type);
assert_eq!(point.mount_source().unwrap(), mount_source);
assert_eq!(point.super_options().unwrap(), super_options);
}
#[test]
fn all_mount_points() {
let mt = mount_points();
let mut mt = mt.points();
cmp_point(
26,
29,
"0:5",
"/",
"/dev",
"rw,nosuid,noexec,relatime",
&[("shared", Some("2"))],
"devtmpfs",
"udev",
"rw,size=8123832k,nr_inodes=2030958,mode=755",
&mt.next().unwrap(),
);
cmp_point(
27,
26,
"0:24",
"/",
"/dev/pts",
"rw,nosuid,noexec,relatime",
&[("shared", Some("3"))],
"devpts",
"devpts",
"rw,gid=5,mode=620,ptmxmode=000",
&mt.next().unwrap(),
);
cmp_point(
35,
33,
"0:30",
"/",
"/sys/fs/cgroup/systemd",
"rw,nosuid,nodev,noexec,relatime",
&[("shared", Some("11")), ("other", None)],
"cgroup",
"cgroup",
"rw,xattr,name=systemd",
&mt.next().unwrap(),
);
cmp_point(
2509,
28,
"0:25",
"/snapd/ns",
"/run/snapd/ns",
"rw,nosuid,nodev,noexec,relatime",
&[],
"tmpfs",
"tmpfs",
"rw,size=1631264k,mode=755",
&mt.next().unwrap(),
);
cmp_point(
2893,
2509,
"0:4",
"mnt:[4026532961]",
"/run/snapd/ns/snap-store.mnt",
"rw",
&[],
"nsfs",
"nsfs",
"rw",
&mt.next().unwrap(),
);
}
#[test]
fn raid_case_1() {
let raids = Raids::from_string("\
Personalities : [raid1] [linear] [multipath] [raid0] [raid6] [raid5] [raid4] [raid10]
md10 : active raid1 sdd[0] sdc[1]
3906886464 blocks super 1.2 [2/2] [UU]
bitmap: 0/30 pages [0KB], 65536KB chunk
md0 : active raid1 sdb[1] sda[0]
499975488 blocks super 1.2 [2/2] [UU]
bitmap: 3/4 pages [12KB], 65536KB chunk
unused devices: <none>\n".into());
assert_eq!(raids.raids().count(), 2);
let first = raids.raids().next().unwrap();
assert_eq!(first.name(), "md10");
assert_eq!(first.used_devices().unwrap(), 2);
assert_eq!(first.ideal_devices().unwrap(), 2);
assert!(first.progress().is_none());
assert_eq!(first.devices().count(), first.used_devices().unwrap());
}
#[test]
fn raid_case_2() {
let raids = Raids::from_string("\
Personalities : [raid1] [raid6] [raid5] [raid4]
md127 : active raid5 sdh1[6] sdg1[4] sdf1[3] sde1[2] sdd1[1] sdc1[0]
1464725760 blocks level 5, 64k chunk, algorithm 2 [6/5] [UUUUU_]
[==>..................] recovery = 12.6% (37043392/292945152) finish=127.5min speed=33440K/sec
unused devices: <none>\n".into());
assert_eq!(raids.raids().count(), 1);
let first = raids.raids().next().unwrap();
let comp_dev: Vec<_> = first.devices().collect();
assert_eq!(
comp_dev,
[
(6, "sdh1"),
(4, "sdg1"),
(3, "sdf1"),
(2, "sde1"),
(1, "sdd1"),
(0, "sdc1")
]
);
assert_eq!(first.kind().unwrap(), "raid5");
assert_eq!(first.usable_blocks().unwrap(), 1464725760);
assert_eq!(first.used_devices().unwrap(), 6);
assert_eq!(first.ideal_devices().unwrap(), 5);
assert_eq!(first.progress().unwrap(), "[==>..................] recovery = 12.6% (37043392/292945152) finish=127.5min speed=33440K/sec");
assert_eq!(first.devices().count(), first.used_devices().unwrap());
}
#[test]
fn raid_case_3() {
let raids = Raids::from_string(
"\
Personalities : [linear] [raid0] [raid1] [raid5] [raid4] [raid6]
md0 : active raid6 sdf1[0] sde1[1] sdd1[2] sdc1[3] sdb1[4] sda1[5] hdb1[6]
1225557760 blocks level 6, 256k chunk, algorithm 2 [7/7] [UUUUUUU]
bitmap: 0/234 pages [0KB], 512KB chunk
unused devices: <none>\n"
.into(),
);
assert_eq!(raids.raids().count(), 1);
let first = raids.raids().next().unwrap();
assert_eq!(first.devices().count(), first.used_devices().unwrap());
}
}