1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
use {
    crate::*,
    lazy_regex::*,
    snafu::prelude::*,
    std::{
        path::PathBuf,
        str::FromStr,
    },
};

static REMOTE_ONLY_FS_TYPES: &[&str] = &["afs", "coda", "auristorfs", "fhgfs", "gpfs", "ibrix", "ocfs2", "vxfs"];

/// An id of a mount
pub type MountId = u32;

/// A mount point as described in /proc/self/mountinfo
#[derive(Debug, Clone)]
pub struct MountInfo {
    pub id: MountId,
    pub parent: MountId,
    pub dev: DeviceId,
    pub root: PathBuf,
    pub mount_point: PathBuf,
    pub fs: String,
    pub fs_type: String,
    /// whether it's a bound mount (usually mirroring part of another device)
    pub bound: bool,
}

impl MountInfo {
    /// return `<name>` when the path is `/dev/mapper/<name>`
    pub fn dm_name(&self) -> Option<&str> {
        regex_captures!(r#"^/dev/mapper/([^/]+)$"#, &self.fs)
            .map(|(_, dm_name)| dm_name)
    }
    /// return the last token of the fs path
    pub fn fs_name(&self) -> Option<&str> {
        regex_find!(r#"[^\\/]+$"#, &self.fs)
    }
    /// tell whether the mount looks remote
    ///
    /// Heuristics copied from https://github.com/coreutils/gnulib/blob/master/lib/mountlist.c
    pub fn is_remote(&self) -> bool {
        self.fs.contains(':')
            || (
                self.fs.starts_with("//")
                && ["cifs", "smb3", "smbfs"].contains(&self.fs_type.as_ref())
            )
            || REMOTE_ONLY_FS_TYPES.contains(&self.fs_type.as_ref())
            || self.fs == "-hosts"
    }
}

#[derive(Debug, Snafu)]
#[snafu(display("Could not parse {line} as mount info"))]
pub struct ParseMountInfoError {
    line: String,
}

impl FromStr for MountInfo {
    type Err = ParseMountInfoError;
    fn from_str(line: &str) -> Result<Self, Self::Err> {
        (|| {
            // this parsing is based on `man 5 proc`
            let mut tokens = line.split_whitespace();
            let id = tokens.next()?.parse().ok()?;
            let parent = tokens.next()?.parse().ok()?;
            let dev = tokens.next()?.parse().ok()?;
            let root = str_to_pathbuf(tokens.next()?);
            let mount_point = str_to_pathbuf(tokens.next()?);
            loop {
                let token = tokens.next()?;
                if token == "-" {
                    break;
                }
            };
            let fs_type = tokens.next()?.to_string();
            let fs = tokens.next()?.to_string();
            Some(Self {
                id,
                parent,
                dev,
                root,
                mount_point,
                fs,
                fs_type,
                bound: false, // determined by post-treatment
            })
        })().with_context(|| ParseMountInfoSnafu { line })
    }
}

/// convert a string to a pathbuf, converting ascii-octal encoded
/// chars.
/// This is necessary because some chars are encoded. For example
/// the `/media/dys/USB DISK` is present as `/media/dys/USB\040DISK`
fn str_to_pathbuf(s: &str) -> PathBuf {
    PathBuf::from(sys::decode_string(s))
}

/// read all the mount points
pub fn read_mountinfo() -> Result<Vec<MountInfo>, Error> {
    let mut mounts: Vec<MountInfo> = Vec::new();
    let path = "/proc/self/mountinfo";
    let file_content = sys::read_file(path)
        .context(CantReadDirSnafu { path })?;
    for line in file_content.trim().split('\n') {
        let mut mount: MountInfo = line.parse()
            .map_err(|source| Error::ParseMountInfo { source })?;
        mount.bound = mounts.iter().any(|m| m.dev == mount.dev);
        mounts.push(mount);
    }
    Ok(mounts)
}

#[test]
fn test_from_str() {
    let mi = MountInfo::from_str(
        "47 21 0:41 / /dev/hugepages rw,relatime shared:27 - hugetlbfs hugetlbfs rw,pagesize=2M"
    ).unwrap();
    assert_eq!(mi.id, 47);
    assert_eq!(mi.dev, DeviceId::new(0, 41));
    assert_eq!(mi.root, PathBuf::from("/"));
    assert_eq!(mi.mount_point, PathBuf::from("/dev/hugepages"));

    let mi = MountInfo::from_str(
        "106 26 8:17 / /home/dys/dev rw,relatime shared:57 - xfs /dev/sdb1 rw,attr2,inode64,noquota"
    ).unwrap();
    assert_eq!(mi.id, 106);
    assert_eq!(mi.dev, DeviceId::new(8, 17));
    assert_eq!(&mi.fs, "/dev/sdb1");
    assert_eq!(&mi.fs_type, "xfs");
}