1use std::{
4 fs::File,
5 io::{Read, Seek},
6};
7
8use thiserror::Error;
9
10pub const PROC_MOUNTS_PATH: &str = "/proc/mounts";
11
12#[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#[derive(Debug, Error)]
27#[error("invalid mount line: {input}")]
28pub struct ParseError {
29 pub(crate) input: String,
30}
31
32#[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 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
63pub 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
69pub(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
79pub(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}