lfs_core/linux/
read_mountinfos.rs

1use {
2    crate::*,
3    lazy_regex::*,
4    snafu::prelude::*,
5    std::path::PathBuf,
6};
7
8#[derive(Debug, Snafu)]
9#[snafu(display("Could not parse {line} as mount info"))]
10pub struct ParseMountInfoError {
11    line: String,
12}
13
14#[cfg(target_os = "linux")]
15impl std::str::FromStr for MountInfo {
16    type Err = ParseMountInfoError;
17    fn from_str(line: &str) -> Result<Self, Self::Err> {
18        (|| {
19            // this parsing is based on `man 5 proc`
20            // Structure is also visible at
21            //  https://man7.org/linux/man-pages/man5/proc_pid_mountinfo.5.html
22            let mut tokens = line.split_whitespace();
23
24            let id = tokens.next()?.parse().ok()?;
25            let parent = tokens.next()?.parse().ok()?;
26
27            // while linux mountinfo need an id and a parent id, they're optional in
28            // the more global model
29            let id = Some(id);
30            let parent = Some(parent);
31
32            let dev = tokens.next()?.parse().ok()?;
33            let root = str_to_pathbuf(tokens.next()?);
34            let mount_point = str_to_pathbuf(tokens.next()?);
35
36            let options = regex_captures_iter!("(?:^|,)([^=,]+)(?:=([^=,]*))?", tokens.next()?,);
37            let options = options
38                .map(|c| {
39                    let name = c.get(1).unwrap().as_str().to_string();
40                    let value = c.get(2).map(|v| v.as_str().to_string());
41                    MountOption { name, value }
42                })
43                .collect();
44
45            // skip optional fields in the form name:value where
46            // name can be "shared", "master", "propagate_for", or "unbindable"
47            loop {
48                let token = tokens.next()?;
49                if token == "-" {
50                    break;
51                }
52            }
53
54            let fs_type = tokens.next()?.to_string();
55            let fs = tokens.next()?.to_string();
56            Some(Self {
57                id,
58                parent,
59                dev,
60                root,
61                mount_point,
62                options,
63                fs,
64                fs_type,
65                bound: false, // determined by post-treatment
66            })
67        })()
68        .with_context(|| ParseMountInfoSnafu { line })
69    }
70}
71
72/// convert a string to a pathbuf, converting ascii-octal encoded
73/// chars.
74/// This is necessary because some chars are encoded. For example
75/// the `/media/dys/USB DISK` is present as `/media/dys/USB\040DISK`
76#[cfg(target_os = "linux")]
77fn str_to_pathbuf(s: &str) -> PathBuf {
78    PathBuf::from(sys::decode_string(s))
79}
80
81/// read all the mount points
82#[cfg(target_os = "linux")]
83pub fn read_all_mountinfos() -> Result<Vec<MountInfo>, Error> {
84    let mut mounts: Vec<MountInfo> = Vec::new();
85    let path = "/proc/self/mountinfo";
86    let file_content = sys::read_file(path).context(CantReadDirSnafu { path })?;
87    for line in file_content.trim().split('\n') {
88        let mut mount: MountInfo = line
89            .parse()
90            .map_err(|source| Error::ParseMountInfo { source })?;
91        mount.bound = mounts.iter().any(|m| m.dev == mount.dev);
92        mounts.push(mount);
93    }
94    Ok(mounts)
95}
96
97#[cfg(target_os = "linux")]
98#[allow(clippy::bool_assert_comparison)]
99#[test]
100fn test_from_str() {
101    use std::str::FromStr;
102
103    let mi = MountInfo::from_str(
104        "47 21 0:41 / /dev/hugepages rw,relatime shared:27 - hugetlbfs hugetlbfs rw,pagesize=2M",
105    )
106    .unwrap();
107    assert_eq!(mi.id, Some(47));
108    assert_eq!(mi.dev, DeviceId::new(0, 41));
109    assert_eq!(mi.root, PathBuf::from("/"));
110    assert_eq!(mi.mount_point, PathBuf::from("/dev/hugepages"));
111    assert_eq!(mi.options_string(), "rw,relatime".to_string());
112
113    let mi = MountInfo::from_str(
114        "106 26 8:17 / /home/dys/dev rw,noatime,compress=zstd:3 shared:57 - btrfs /dev/sdb1 rw,attr2,inode64,noquota"
115    ).unwrap();
116    assert_eq!(mi.id, Some(106));
117    assert_eq!(mi.dev, DeviceId::new(8, 17));
118    assert_eq!(&mi.fs, "/dev/sdb1");
119    assert_eq!(&mi.fs_type, "btrfs");
120    let mut options = mi.options.clone().into_iter();
121    assert_eq!(options.next(), Some(MountOption::new("rw", None)),);
122    assert_eq!(options.next(), Some(MountOption::new("noatime", None)));
123    assert_eq!(
124        options.next(),
125        Some(MountOption::new("compress", Some("zstd:3")))
126    );
127    assert_eq!(options.next(), None);
128    assert_eq!(mi.has_option("noatime"), true);
129    assert_eq!(mi.has_option("relatime"), false);
130    assert_eq!(mi.option_value("thing"), None);
131    assert_eq!(mi.option_value("compress"), Some("zstd:3"));
132}