1use crate::unit::DataSize;
4use crate::util::{blkdev_sector_size, read_to_string_mut};
5
6use std::convert::TryInto;
7use std::path::Path;
8use std::{fs, io};
9
10use byte_parser::{parse_iter, ParseIterator, StrParser};
11
12#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct Partitions {
15 raw: String,
16}
17
18impl Partitions {
19 fn path() -> &'static Path {
20 Path::new("/proc/partitions")
21 }
22
23 #[cfg(test)]
24 fn from_string(raw: String) -> Self {
25 Self { raw }
26 }
27
28 pub fn read() -> io::Result<Self> {
30 Ok(Self {
31 raw: fs::read_to_string(Self::path())?,
32 })
33 }
34
35 pub fn reload(&mut self) -> io::Result<()> {
37 read_to_string_mut(Self::path(), &mut self.raw)
38 }
39
40 pub fn entries<'a>(&'a self) -> impl Iterator<Item = PartitionEntry<'a>> {
41 self.raw
42 .trim()
43 .split('\n')
44 .skip(2) .map(PartitionEntry::from_str)
46 }
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct PartitionEntry<'a> {
51 raw: &'a str,
52}
53
54impl<'a> PartitionEntry<'a> {
55 fn from_str(raw: &'a str) -> Self {
56 Self { raw }
57 }
58
59 pub fn values(&self) -> impl Iterator<Item = &'a str> {
61 self.raw.split(' ').map(str::trim).filter(|s| !s.is_empty())
62 }
63
64 pub fn major(&self) -> Option<usize> {
66 self.values().nth(0)?.parse().ok()
67 }
68
69 pub fn minor(&self) -> Option<usize> {
71 self.values().nth(1)?.parse().ok()
72 }
73
74 pub fn blocks(&self) -> Option<usize> {
76 self.values().nth(2)?.parse().ok()
77 }
78
79 pub fn name(&self) -> Option<&'a str> {
81 self.values().nth(3)
82 }
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct MountPoints {
88 raw: String,
89}
90
91impl MountPoints {
92 fn path() -> &'static Path {
93 Path::new("/proc/self/mountinfo")
94 }
95
96 #[cfg(test)]
97 fn from_string(raw: String) -> Self {
98 Self { raw }
99 }
100
101 pub fn read() -> io::Result<Self> {
103 Ok(Self {
104 raw: fs::read_to_string(Self::path())?,
105 })
106 }
107
108 pub fn reload(&mut self) -> io::Result<()> {
110 read_to_string_mut(Self::path(), &mut self.raw)
111 }
112
113 pub fn points<'a>(&'a self) -> impl Iterator<Item = MountPoint<'a>> {
115 self.raw.trim().split('\n').map(MountPoint::from_str)
116 }
117}
118
119#[derive(Debug, Clone, PartialEq, Eq)]
120pub struct MountPoint<'a> {
121 raw: &'a str,
122}
123
124impl<'a> MountPoint<'a> {
125 fn from_str(raw: &'a str) -> Self {
126 Self { raw }
127 }
128
129 #[inline]
131 pub fn values(&self) -> impl Iterator<Item = &'a str> {
132 self.raw.split(' ')
133 }
134
135 pub fn mount_id(&self) -> Option<usize> {
137 self.values().nth(0)?.parse().ok()
138 }
139
140 pub fn parent_id(&self) -> Option<usize> {
143 self.values().nth(1)?.parse().ok()
144 }
145
146 #[inline]
148 pub fn major_minor(&self) -> Option<&'a str> {
149 self.values().nth(2)
150 }
151
152 pub fn major(&self) -> Option<usize> {
154 self.major_minor()?.split(':').nth(0)?.parse().ok()
155 }
156
157 pub fn minor(&self) -> Option<usize> {
159 self.major_minor()?.split(':').nth(1)?.parse().ok()
160 }
161
162 pub fn root(&self) -> Option<&'a str> {
165 self.values().nth(3)
166 }
167
168 pub fn mount_point(&self) -> Option<&'a str> {
171 self.values().nth(4)
172 }
173
174 pub fn mount_options(&self) -> Option<&'a str> {
176 self.values().nth(5)
177 }
178
179 pub fn optional_fields(
182 &self,
183 ) -> impl Iterator<Item = (&'a str, Option<&'a str>)> {
184 self.values().skip(6).take_while(|&i| i != "-").map(|opt| {
185 let mut iters = opt.split(':');
186 (
187 iters.next().unwrap(),
188 iters.next(), )
191 })
192 }
193
194 fn after_separator(&self) -> impl Iterator<Item = &'a str> {
195 self.values().skip(5).skip_while(|&i| i != "-").skip(1) }
197
198 pub fn filesystem_type(&self) -> Option<&'a str> {
200 self.after_separator().nth(0)
202 }
203
204 pub fn mount_source(&self) -> Option<&'a str> {
209 self.after_separator().nth(1)
210 }
216
217 pub fn super_options(&self) -> Option<&'a str> {
219 self.after_separator().nth(2)
220 }
221
222 pub fn stats(&self) -> io::Result<FsStat> {
224 FsStat::read(self.mount_point().unwrap_or(""))
225 }
226}
227
228#[derive(Clone)]
230pub struct FsStat {
231 raw: libc::statfs,
232}
233
234impl FsStat {
235 pub fn read(path: impl AsRef<Path>) -> io::Result<Self> {
238 crate::util::statfs(path).map(|raw| Self { raw })
239 }
240
241 pub fn has_blocks(&self) -> bool {
243 self.total_blocks().map(|b| b > 0).unwrap_or(false)
244 }
245
246 pub fn block_size(&self) -> Option<usize> {
248 self.raw.f_bsize.try_into().ok()
249 }
250
251 pub fn total_blocks(&self) -> Option<usize> {
253 self.raw.f_blocks.try_into().ok()
254 }
255
256 pub fn free_blocks(&self) -> Option<usize> {
259 self.raw.f_bfree.try_into().ok()
260 }
261
262 pub fn available_blocks(&self) -> Option<usize> {
265 self.raw.f_bavail.try_into().ok()
266 }
267
268 pub fn used_blocks(&self) -> Option<usize> {
270 Some(self.total_blocks()? - self.free_blocks()?)
271 }
272
273 pub fn total(&self) -> Option<DataSize> {
275 DataSize::from_size_bytes(self.total_blocks()? * self.block_size()?)
276 }
277
278 pub fn free(&self) -> Option<DataSize> {
280 DataSize::from_size_bytes(self.free_blocks()? * self.block_size()?)
281 }
282
283 pub fn available(&self) -> Option<DataSize> {
286 DataSize::from_size_bytes(self.available_blocks()? * self.block_size()?)
287 }
288
289 pub fn used(&self) -> Option<DataSize> {
292 DataSize::from_size_bytes(self.used_blocks()? * self.block_size()?)
293 }
294}
295
296#[derive(Debug, Clone, PartialEq, Eq)]
298pub struct Raids {
299 raw: String,
300}
301
302impl Raids {
303 fn path() -> &'static Path {
304 Path::new("/proc/mdstat")
305 }
306
307 #[cfg(test)]
308 fn from_string(raw: String) -> Self {
309 Self { raw }
310 }
311
312 pub fn read() -> io::Result<Self> {
314 Ok(Self {
315 raw: fs::read_to_string(Self::path())?,
316 })
317 }
318
319 pub fn reload(&mut self) -> io::Result<()> {
321 read_to_string_mut(Self::path(), &mut self.raw)
322 }
323
324 pub fn raids(&self) -> impl Iterator<Item = Raid<'_>> {
326 let mut first_line = false;
327 parse_iter(StrParser::new(self.raw.trim()), move |parser| {
328 if !first_line {
329 parser.consume_while_byte_fn(|&b| b != b'\n');
330 parser.advance();
332 first_line = true;
333 }
334 parser.peek()?;
335 let key = parser
336 .record()
337 .while_byte_fn(|&b| b != b':')
338 .consume_to_str()
339 .trim();
340
341 if key == "unused devices" {
342 return None;
343 }
344 parser.advance();
346
347 let mut parser = parser.record();
348 let mut one = false;
349
350 loop {
351 if one && matches!(parser.peek(), Some(b'\n')) {
352 let s = parser.to_str().trim();
354 parser.advance();
355 return Some(Raid::from_str(key, s));
356 }
357
358 if one {
359 one = false;
360 continue;
361 }
362
363 match parser.next() {
364 Some(b'\n') => one = true,
365 None => {
366 return Some(Raid::from_str(
368 key,
369 parser.to_str().trim(),
370 ));
371 }
372 _ => {}
373 }
374 }
375 })
376 }
377}
378
379#[derive(Debug, Clone, PartialEq, Eq)]
382pub struct Raid<'a> {
383 name: &'a str,
384 raw: &'a str,
385}
386
387impl<'a> Raid<'a> {
388 fn from_str(name: &'a str, raw: &'a str) -> Self {
389 Self { name, raw }
390 }
391
392 #[inline]
394 pub fn values(
395 &self,
396 ) -> impl Iterator<Item = impl Iterator<Item = &'a str>> {
397 self.raw.split('\n').map(str::trim).map(|l| l.split(' '))
398 }
399
400 pub fn name(&self) -> &'a str {
402 self.name
403 }
404
405 pub fn state(&self) -> Option<&'a str> {
407 self.values().nth(0)?.nth(0)
408 }
409
410 fn line(&self, line: usize) -> impl Iterator<Item = &'a str> {
411 let mut iter = self.values().nth(line);
412 std::iter::from_fn(move || iter.as_mut()?.next())
413 }
414
415 pub fn kind(&self) -> Option<&'a str> {
418 self.line(0).nth(1)
419 }
420
421 pub fn devices(&self) -> impl Iterator<Item = (usize, &'a str)> {
423 self.line(0).skip(2).filter_map(|dev| {
424 let mut split = dev.split(&['[', ']'][..]);
425 let name = split.next()?;
426 Some((split.next()?.parse().ok()?, name))
427 })
428 }
429
430 pub fn usable_blocks(&self) -> Option<usize> {
432 self.line(1).nth(0)?.parse().ok()
433 }
434
435 pub fn used_devices(&self) -> Option<usize> {
438 self.line(1)
439 .find(|l| l.starts_with('['))?
440 .split('/')
441 .nth(0)?
442 .strip_prefix('[')?
443 .parse()
444 .ok()
445 }
446
447 pub fn ideal_devices(&self) -> Option<usize> {
450 self.line(1)
451 .find(|l| l.starts_with('['))?
452 .split('/')
453 .nth(1)?
454 .strip_suffix(']')?
455 .parse()
456 .ok()
457 }
458
459 pub fn progress(&self) -> Option<&'a str> {
462 let l = self.raw.split('\n').nth(2)?.trim();
463 l.starts_with('[').then(|| l)
464 }
465
466 pub fn stats(&self) -> io::Result<FsStat> {
468 FsStat::read(format!("/dev/{}", self.name()))
469 }
470}
471
472pub fn sector_size(path: impl AsRef<Path>) -> io::Result<u64> {
476 blkdev_sector_size(fs::File::open(path)?)
477}
478
479#[cfg(test)]
480mod tests {
481 use super::*;
482
483 fn partitions() -> Partitions {
484 Partitions::from_string(
485 "\
486major minor #blocks name
487
488 7 0 142152 loop0
489 7 1 101528 loop1
490 259 0 500107608 nvme0n1
491 259 1 510976 nvme0n1p1\n\
492 "
493 .into(),
494 )
495 }
496
497 fn cmp_entry(
498 major: usize,
499 minor: usize,
500 blocks: usize,
501 name: &str,
502 e: &PartitionEntry<'_>,
503 ) {
504 assert_eq!(e.major().unwrap(), major);
505 assert_eq!(e.minor().unwrap(), minor);
506 assert_eq!(e.blocks().unwrap(), blocks);
507 assert_eq!(e.name().unwrap(), name);
508 }
509
510 #[test]
511 fn all_partitions() {
512 let part = partitions();
513 let mut e = part.entries();
514 println!("e: {:?}", part.entries().collect::<Vec<_>>());
515 cmp_entry(7, 0, 142152, "loop0", &e.next().unwrap());
516 cmp_entry(7, 1, 101528, "loop1", &e.next().unwrap());
517 cmp_entry(259, 0, 500107608, "nvme0n1", &e.next().unwrap());
518 cmp_entry(259, 1, 510976, "nvme0n1p1", &e.next().unwrap());
519 assert!(e.next().is_none());
520 }
521
522 fn mount_points() -> MountPoints {
523 MountPoints::from_string("\
52426 29 0:5 / /dev rw,nosuid,noexec,relatime shared:2 - devtmpfs udev rw,size=8123832k,nr_inodes=2030958,mode=755
52527 26 0:24 / /dev/pts rw,nosuid,noexec,relatime shared:3 - devpts devpts rw,gid=5,mode=620,ptmxmode=000
52635 33 0:30 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime shared:11 other - cgroup cgroup rw,xattr,name=systemd
5272509 28 0:25 /snapd/ns /run/snapd/ns rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,size=1631264k,mode=755
5282893 2509 0:4 mnt:[4026532961] /run/snapd/ns/snap-store.mnt rw - nsfs nsfs rw\n\
529 ".into())
530 }
531
532 fn cmp_point(
533 mount_id: usize,
534 parent_id: usize,
535 major_minor: &str,
536 root: &str,
537 mount_point: &str,
538 mount_options: &str,
539 optional_fields: &[(&str, Option<&str>)],
540 filesystem_type: &str,
541 mount_source: &str,
542 super_options: &str,
543 point: &MountPoint<'_>,
544 ) {
545 assert_eq!(point.mount_id().unwrap(), mount_id);
546 assert_eq!(point.parent_id().unwrap(), parent_id);
547 assert_eq!(point.major_minor().unwrap(), major_minor);
548 assert_eq!(point.root().unwrap(), root);
549 assert_eq!(point.mount_point().unwrap(), mount_point);
550 assert_eq!(point.mount_options().unwrap(), mount_options);
551 assert_eq!(
552 point.optional_fields().collect::<Vec<_>>(),
553 optional_fields
554 );
555 assert_eq!(point.filesystem_type().unwrap(), filesystem_type);
556 assert_eq!(point.mount_source().unwrap(), mount_source);
557 assert_eq!(point.super_options().unwrap(), super_options);
558 }
559
560 #[test]
561 fn all_mount_points() {
562 let mt = mount_points();
563 let mut mt = mt.points();
564 cmp_point(
565 26,
566 29,
567 "0:5",
568 "/",
569 "/dev",
570 "rw,nosuid,noexec,relatime",
571 &[("shared", Some("2"))],
572 "devtmpfs",
573 "udev",
574 "rw,size=8123832k,nr_inodes=2030958,mode=755",
575 &mt.next().unwrap(),
576 );
577 cmp_point(
578 27,
579 26,
580 "0:24",
581 "/",
582 "/dev/pts",
583 "rw,nosuid,noexec,relatime",
584 &[("shared", Some("3"))],
585 "devpts",
586 "devpts",
587 "rw,gid=5,mode=620,ptmxmode=000",
588 &mt.next().unwrap(),
589 );
590 cmp_point(
591 35,
592 33,
593 "0:30",
594 "/",
595 "/sys/fs/cgroup/systemd",
596 "rw,nosuid,nodev,noexec,relatime",
597 &[("shared", Some("11")), ("other", None)],
598 "cgroup",
599 "cgroup",
600 "rw,xattr,name=systemd",
601 &mt.next().unwrap(),
602 );
603 cmp_point(
604 2509,
605 28,
606 "0:25",
607 "/snapd/ns",
608 "/run/snapd/ns",
609 "rw,nosuid,nodev,noexec,relatime",
610 &[],
611 "tmpfs",
612 "tmpfs",
613 "rw,size=1631264k,mode=755",
614 &mt.next().unwrap(),
615 );
616 cmp_point(
617 2893,
618 2509,
619 "0:4",
620 "mnt:[4026532961]",
621 "/run/snapd/ns/snap-store.mnt",
622 "rw",
623 &[],
624 "nsfs",
625 "nsfs",
626 "rw",
627 &mt.next().unwrap(),
628 );
629 }
630
631 #[test]
632 fn raid_case_1() {
633 let raids = Raids::from_string("\
634Personalities : [raid1] [linear] [multipath] [raid0] [raid6] [raid5] [raid4] [raid10]
635md10 : active raid1 sdd[0] sdc[1]
636 3906886464 blocks super 1.2 [2/2] [UU]
637 bitmap: 0/30 pages [0KB], 65536KB chunk
638
639md0 : active raid1 sdb[1] sda[0]
640 499975488 blocks super 1.2 [2/2] [UU]
641 bitmap: 3/4 pages [12KB], 65536KB chunk
642
643unused devices: <none>\n".into());
644 assert_eq!(raids.raids().count(), 2);
645 let first = raids.raids().next().unwrap();
646 assert_eq!(first.name(), "md10");
647 assert_eq!(first.used_devices().unwrap(), 2);
648 assert_eq!(first.ideal_devices().unwrap(), 2);
649 assert!(first.progress().is_none());
650 assert_eq!(first.devices().count(), first.used_devices().unwrap());
651 }
652
653 #[test]
654 fn raid_case_2() {
655 let raids = Raids::from_string("\
656Personalities : [raid1] [raid6] [raid5] [raid4]
657md127 : active raid5 sdh1[6] sdg1[4] sdf1[3] sde1[2] sdd1[1] sdc1[0]
658 1464725760 blocks level 5, 64k chunk, algorithm 2 [6/5] [UUUUU_]
659 [==>..................] recovery = 12.6% (37043392/292945152) finish=127.5min speed=33440K/sec
660
661unused devices: <none>\n".into());
662 assert_eq!(raids.raids().count(), 1);
663 let first = raids.raids().next().unwrap();
664 let comp_dev: Vec<_> = first.devices().collect();
665 assert_eq!(
666 comp_dev,
667 [
668 (6, "sdh1"),
669 (4, "sdg1"),
670 (3, "sdf1"),
671 (2, "sde1"),
672 (1, "sdd1"),
673 (0, "sdc1")
674 ]
675 );
676 assert_eq!(first.kind().unwrap(), "raid5");
677 assert_eq!(first.usable_blocks().unwrap(), 1464725760);
678 assert_eq!(first.used_devices().unwrap(), 6);
679 assert_eq!(first.ideal_devices().unwrap(), 5);
680 assert_eq!(first.progress().unwrap(), "[==>..................] recovery = 12.6% (37043392/292945152) finish=127.5min speed=33440K/sec");
681 assert_eq!(first.devices().count(), first.used_devices().unwrap());
682 }
683
684 #[test]
685 fn raid_case_3() {
686 let raids = Raids::from_string(
687 "\
688Personalities : [linear] [raid0] [raid1] [raid5] [raid4] [raid6]
689md0 : active raid6 sdf1[0] sde1[1] sdd1[2] sdc1[3] sdb1[4] sda1[5] hdb1[6]
690 1225557760 blocks level 6, 256k chunk, algorithm 2 [7/7] [UUUUUUU]
691 bitmap: 0/234 pages [0KB], 512KB chunk
692
693unused devices: <none>\n"
694 .into(),
695 );
696 assert_eq!(raids.raids().count(), 1);
697 let first = raids.raids().next().unwrap();
698 assert_eq!(first.devices().count(), first.used_devices().unwrap());
699 }
700}
701
702