lfs_core/
mountinfo.rs

1use {
2    crate::*,
3    lazy_regex::*,
4    snafu::prelude::*,
5    std::{
6        path::PathBuf,
7        str::FromStr,
8    },
9};
10
11static REMOTE_ONLY_FS_TYPES: &[&str] = &[
12    "afs",
13    "coda",
14    "auristorfs",
15    "fhgfs",
16    "gpfs",
17    "ibrix",
18    "ocfs2",
19    "vxfs",
20];
21
22/// An id of a mount
23pub type MountId = u32;
24
25/// A mount point as described in /proc/self/mountinfo
26#[derive(Debug, Clone)]
27pub struct MountInfo {
28    pub id: Option<MountId>,
29    pub parent: Option<MountId>,
30    pub dev: DeviceId,
31    pub root: PathBuf,
32    pub mount_point: PathBuf,
33    pub fs: String, // rename into "node" ?
34    pub fs_type: String,
35    /// whether it's a bound mount (usually mirroring part of another device)
36    pub bound: bool,
37}
38
39impl MountInfo {
40    /// return `<name>` when the path is `/dev/mapper/<name>`
41    pub fn dm_name(&self) -> Option<&str> {
42        regex_captures!(r#"^/dev/mapper/([^/]+)$"#, &self.fs).map(|(_, dm_name)| dm_name)
43    }
44    /// return the last token of the fs path
45    pub fn fs_name(&self) -> Option<&str> {
46        regex_find!(r#"[^\\/]+$"#, &self.fs)
47    }
48    /// tell whether the mount looks remote
49    ///
50    /// Heuristics copied from https://github.com/coreutils/gnulib/blob/master/lib/mountlist.c
51    pub fn is_remote(&self) -> bool {
52        self.fs.contains(':')
53            || (self.fs.starts_with("//")
54                && ["cifs", "smb3", "smbfs"].contains(&self.fs_type.as_ref()))
55            || REMOTE_ONLY_FS_TYPES.contains(&self.fs_type.as_ref())
56            || self.fs == "-hosts"
57    }
58}
59
60#[derive(Debug, Snafu)]
61#[snafu(display("Could not parse {line} as mount info"))]
62pub struct ParseMountInfoError {
63    line: String,
64}
65
66impl FromStr for MountInfo {
67    type Err = ParseMountInfoError;
68    fn from_str(line: &str) -> Result<Self, Self::Err> {
69        (|| {
70            // this parsing is based on `man 5 proc`
71            let mut tokens = line.split_whitespace();
72
73            let id = tokens.next()?.parse().ok()?;
74            let parent = tokens.next()?.parse().ok()?;
75
76            // while linux mountinfo need an id and a parent id, they're optional in
77            // the more global model
78            let id = Some(id);
79            let parent = Some(parent);
80
81            let dev = tokens.next()?.parse().ok()?;
82            let root = str_to_pathbuf(tokens.next()?);
83            let mount_point = str_to_pathbuf(tokens.next()?);
84            loop {
85                let token = tokens.next()?;
86                if token == "-" {
87                    break;
88                }
89            }
90            let fs_type = tokens.next()?.to_string();
91            let fs = tokens.next()?.to_string();
92            Some(Self {
93                id,
94                parent,
95                dev,
96                root,
97                mount_point,
98                fs,
99                fs_type,
100                bound: false, // determined by post-treatment
101            })
102        })()
103        .with_context(|| ParseMountInfoSnafu { line })
104    }
105}
106
107/// convert a string to a pathbuf, converting ascii-octal encoded
108/// chars.
109/// This is necessary because some chars are encoded. For example
110/// the `/media/dys/USB DISK` is present as `/media/dys/USB\040DISK`
111fn str_to_pathbuf(s: &str) -> PathBuf {
112    PathBuf::from(sys::decode_string(s))
113}
114
115/// read all the mount points
116pub fn read_mountinfo() -> Result<Vec<MountInfo>, Error> {
117    let mut mounts: Vec<MountInfo> = Vec::new();
118    let path = "/proc/self/mountinfo";
119    let file_content = sys::read_file(path).context(CantReadDirSnafu { path })?;
120    for line in file_content.trim().split('\n') {
121        let mut mount: MountInfo = line
122            .parse()
123            .map_err(|source| Error::ParseMountInfo { source })?;
124        mount.bound = mounts.iter().any(|m| m.dev == mount.dev);
125        mounts.push(mount);
126    }
127    Ok(mounts)
128}
129
130#[cfg(target_os = "linux")]
131#[test]
132fn test_from_str() {
133    let mi = MountInfo::from_str(
134        "47 21 0:41 / /dev/hugepages rw,relatime shared:27 - hugetlbfs hugetlbfs rw,pagesize=2M",
135    )
136    .unwrap();
137    assert_eq!(mi.id, Some(47));
138    assert_eq!(mi.dev, DeviceId::new(0, 41));
139    assert_eq!(mi.root, PathBuf::from("/"));
140    assert_eq!(mi.mount_point, PathBuf::from("/dev/hugepages"));
141
142    let mi = MountInfo::from_str(
143        "106 26 8:17 / /home/dys/dev rw,relatime shared:57 - xfs /dev/sdb1 rw,attr2,inode64,noquota"
144    ).unwrap();
145    assert_eq!(mi.id, Some(106));
146    assert_eq!(mi.dev, DeviceId::new(8, 17));
147    assert_eq!(&mi.fs, "/dev/sdb1");
148    assert_eq!(&mi.fs_type, "xfs");
149}