mount_watcher/
mount.rs

1//! Parse /proc/mounts.
2
3use std::{
4    fs::File,
5    io::{Read, Seek},
6};
7
8use thiserror::Error;
9
10pub const PROC_MOUNTS_PATH: &str = "/proc/mounts";
11
12/// A mounted filesystem.
13///
14/// See `man fstab` for a detailed description of the fields.
15#[derive(Debug, PartialEq, Eq, Hash, Clone)]
16pub struct LinuxMount {
17    pub spec: String,
18    pub mount_point: String,
19    pub fs_type: String,
20    pub mount_options: Vec<String>,
21    pub dump_fs_freq: u32,
22    pub fsck_fs_passno: u32,
23}
24
25/// Error while parsing `/proc/mounts`.
26#[derive(Debug, Error)]
27#[error("invalid mount line: {input}")]
28pub struct ParseError {
29    pub(crate) input: String,
30}
31
32/// Error while reading/parsing `/proc/mounts`.
33#[derive(Debug, Error)]
34pub enum ReadError {
35    #[error("failed to parse {PROC_MOUNTS_PATH}")]
36    Parse(#[from] ParseError),
37    #[error("failed to read {PROC_MOUNTS_PATH}")]
38    Io(#[from] std::io::Error),
39}
40
41impl LinuxMount {
42    /// Attempts to parse one line of `/proc/mounts`.
43    /// Returns `None` if it fails.
44    pub fn parse(line: &str) -> Option<Self> {
45        let mut fields = line.split_ascii_whitespace().into_iter();
46        let spec = fields.next()?.to_string();
47        let mount_point = fields.next()?.to_string();
48        let fs_type = fields.next()?.to_string();
49        let mount_options = fields.next()?.split(',').map(ToOwned::to_owned).collect();
50        let dump_fs_freq = fields.next()?.parse().ok()?;
51        let fsck_fs_passno = fields.next()?.parse().ok()?;
52        Some(Self {
53            spec,
54            mount_point,
55            fs_type,
56            mount_options,
57            dump_fs_freq,
58            fsck_fs_passno,
59        })
60    }
61}
62
63/// Returns the filesystems that are currently mounted.
64pub fn list_current_mounts() -> Result<Vec<LinuxMount>, ReadError> {
65    let mut file = File::open(PROC_MOUNTS_PATH)?;
66    read_proc_mounts(&mut file)
67}
68
69/// Reads `/proc/mounts` from the beginning and parses its content.
70pub(crate) fn read_proc_mounts(file: &mut File) -> Result<Vec<LinuxMount>, ReadError> {
71    let mut content = String::with_capacity(4096);
72    file.rewind()?;
73    file.read_to_string(&mut content)?;
74    let mut mounts = Vec::with_capacity(64);
75    parse_proc_mounts(&content, &mut mounts)?;
76    Ok(mounts)
77}
78
79/// Parses the content of `/proc/mounts` and stores the result in `buf`.
80pub(crate) fn parse_proc_mounts(
81    content: &str,
82    buf: &mut Vec<LinuxMount>,
83) -> Result<(), ParseError> {
84    for line in content.lines() {
85        let line = line.trim_start_matches(|c: char| c.is_ascii_whitespace());
86        if !line.is_empty() && !line.starts_with('#') {
87            let m = LinuxMount::parse(line).ok_or_else(|| ParseError {
88                input: line.to_owned(),
89            })?;
90            buf.push(m);
91        }
92    }
93    Ok(())
94}
95
96#[cfg(test)]
97mod tests {
98    use pretty_assertions::assert_eq;
99
100    use super::{parse_proc_mounts, LinuxMount};
101
102    fn vec_str(values: &[&str]) -> Vec<String> {
103        values.into_iter().map(|s| s.to_string()).collect()
104    }
105
106    #[test]
107    fn parsing() {
108        let content = "
109sysfs /sys sysfs rw,nosuid,nodev,noexec,relatime 0 0
110tmpfs /run tmpfs rw,nosuid,nodev,noexec,relatime,size=1599352k,mode=755,inode64 1 2
111cgroup2 /sys/fs/cgroup cgroup2 rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot 0 0
112/dev/nvme0n1p1 /boot/efi vfat rw,relatime,errors=remount-ro 0 0";
113        let mut mounts = Vec::new();
114        parse_proc_mounts(&content, &mut mounts).unwrap();
115
116        let expected = vec![
117            LinuxMount {
118                spec: String::from("sysfs"),
119                mount_point: String::from("/sys"),
120                fs_type: String::from("sysfs"),
121                mount_options: vec_str(&["rw", "nosuid", "nodev", "noexec", "relatime"]),
122                dump_fs_freq: 0,
123                fsck_fs_passno: 0,
124            },
125            LinuxMount {
126                spec: String::from("tmpfs"),
127                mount_point: String::from("/run"),
128                fs_type: String::from("tmpfs"),
129                mount_options: vec_str(&[
130                    "rw",
131                    "nosuid",
132                    "nodev",
133                    "noexec",
134                    "relatime",
135                    "size=1599352k",
136                    "mode=755",
137                    "inode64",
138                ]),
139                dump_fs_freq: 1,
140                fsck_fs_passno: 2,
141            },
142            LinuxMount {
143                spec: String::from("cgroup2"),
144                mount_point: String::from("/sys/fs/cgroup"),
145                fs_type: String::from("cgroup2"),
146                mount_options: vec_str(&[
147                    "rw",
148                    "nosuid",
149                    "nodev",
150                    "noexec",
151                    "relatime",
152                    "nsdelegate",
153                    "memory_recursiveprot",
154                ]),
155                dump_fs_freq: 0,
156                fsck_fs_passno: 0,
157            },
158            LinuxMount {
159                spec: String::from("/dev/nvme0n1p1"),
160                mount_point: String::from("/boot/efi"),
161                fs_type: String::from("vfat"),
162                mount_options: vec_str(&["rw", "relatime", "errors=remount-ro"]),
163                dump_fs_freq: 0,
164                fsck_fs_passno: 0,
165            },
166        ];
167        assert_eq!(expected, mounts);
168    }
169
170    #[test]
171    fn parsing_error() {
172        let mut mounts = Vec::new();
173        parse_proc_mounts("badbad", &mut mounts).unwrap_err();
174        parse_proc_mounts("croup2 /sys/fs/cgroup", &mut mounts).unwrap_err();
175    }
176
177    #[test]
178    fn parsing_comments() {
179        let mut mounts = Vec::new();
180        parse_proc_mounts("\n# badbad\n", &mut mounts).unwrap();
181    }
182}