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 direct_options =
37                regex_captures_iter!("(?:^|,)([^=,]+)(?:=([^=,]*))?", tokens.next()?,);
38
39            let mut options: Vec<MountOption> = direct_options
40                .map(|c| {
41                    let name = c.get(1).unwrap().as_str().to_string();
42                    let value = c.get(2).map(|v| v.as_str().to_string());
43                    MountOption { name, value }
44                })
45                .collect();
46
47            // skip optional fields in the form name:value where
48            // name can be "shared", "master", "propagate_for", or "unbindable"
49            loop {
50                let token = tokens.next()?;
51                if token == "-" {
52                    break;
53                }
54            }
55
56            let fs_type = tokens.next()?.to_string();
57            let fs = tokens.next()?.to_string();
58
59            if let Some(super_options) = tokens.next() {
60                for c in regex_captures_iter!("(?:^|,)([^=,]+)(?:=([^=,]*))?", super_options) {
61                    let name = c.get(1).unwrap().as_str().to_string();
62                    if name == "rw" {
63                        continue; // rw at super level is not relevant
64                    }
65                    if options.iter().any(|o| o.name == name) {
66                        continue;
67                    }
68                    let value = c.get(2).map(|v| v.as_str().to_string());
69                    options.push(MountOption { name, value });
70                }
71            }
72
73            Some(Self {
74                id,
75                parent,
76                dev,
77                root,
78                mount_point,
79                options,
80                fs,
81                fs_type,
82                bound: false, // determined by post-treatment
83            })
84        })()
85        .with_context(|| ParseMountInfoSnafu { line })
86    }
87}
88
89/// convert a string to a pathbuf, converting ascii-octal encoded
90/// chars.
91/// This is necessary because some chars are encoded. For example
92/// the `/media/dys/USB DISK` is present as `/media/dys/USB\040DISK`
93#[cfg(target_os = "linux")]
94fn str_to_pathbuf(s: &str) -> PathBuf {
95    PathBuf::from(sys::decode_string(s))
96}
97
98/// read all the mount points
99#[cfg(target_os = "linux")]
100pub fn read_all_mountinfos() -> Result<Vec<MountInfo>, Error> {
101    let mut mounts: Vec<MountInfo> = Vec::new();
102    let path = "/proc/self/mountinfo";
103    let file_content = sys::read_file(path).context(CantReadDirSnafu { path })?;
104    for line in file_content.trim().split('\n') {
105        let mut mount: MountInfo = line
106            .parse()
107            .map_err(|source| Error::ParseMountInfo { source })?;
108        mount.bound = mounts.iter().any(|m| m.dev == mount.dev);
109        mounts.push(mount);
110    }
111    Ok(mounts)
112}
113
114#[cfg(target_os = "linux")]
115#[allow(clippy::bool_assert_comparison)]
116#[test]
117fn test_from_str() {
118    use std::str::FromStr;
119
120    let mi = MountInfo::from_str(
121        "47 21 0:41 / /dev/hugepages rw,relatime shared:27 - hugetlbfs hugetlbfs rw,pagesize=2M",
122    )
123    .unwrap();
124    assert_eq!(mi.id, Some(47));
125    assert_eq!(mi.dev, DeviceId::new(0, 41));
126    assert_eq!(mi.root, PathBuf::from("/"));
127    assert_eq!(mi.mount_point, PathBuf::from("/dev/hugepages"));
128    assert_eq!(mi.options_string(), "rw,relatime,pagesize=2M".to_string());
129
130    let mi = MountInfo::from_str(
131        "106 26 8:17 / /home/dys/dev rw,noatime,compress=zstd:3 shared:57 - btrfs /dev/sdb1 rw,attr2,inode64,noquota"
132    ).unwrap();
133    assert_eq!(mi.id, Some(106));
134    assert_eq!(mi.dev, DeviceId::new(8, 17));
135    assert_eq!(&mi.fs, "/dev/sdb1");
136    assert_eq!(&mi.fs_type, "btrfs");
137    let mut options = mi.options.clone().into_iter();
138    assert_eq!(options.next(), Some(MountOption::new("rw", None)),);
139    assert_eq!(options.next(), Some(MountOption::new("noatime", None)));
140    assert_eq!(
141        options.next(),
142        Some(MountOption::new("compress", Some("zstd:3")))
143    );
144    assert_eq!(mi.has_option("noatime"), true);
145    assert_eq!(mi.has_option("relatime"), false);
146    assert_eq!(mi.option_value("thing"), None);
147    assert_eq!(mi.option_value("compress"), Some("zstd:3"));
148    assert_eq!(
149        mi.options_string(),
150        "rw,noatime,compress=zstd:3,attr2,inode64,noquota".to_string()
151    );
152
153    let mi = MountInfo::from_str(
154        "73 2 0:33 /root / rw,relatime shared:1 - btrfs /dev/vda3 rw,seclabel,compress=zstd:1,ssd,space_cache=v2,subvolid=256,subvol=/root"
155    ).unwrap();
156    assert_eq!(mi.option_value("compress"), Some("zstd:1"));
157    assert_eq!(
158        mi.options_string(),
159        "rw,relatime,seclabel,compress=zstd:1,ssd,space_cache=v2,subvolid=256,subvol=/root"
160            .to_string()
161    );
162}