]> git.proxmox.com Git - proxmox.git/blame - proxmox-sys/src/linux/procfs/mountinfo.rs
sys: double-parse fixup
[proxmox.git] / proxmox-sys / src / linux / procfs / mountinfo.rs
CommitLineData
40da8098
WB
1//! `/proc/PID/mountinfo` handling.
2
bd4a6c5b 3use std::collections::BTreeMap;
40da8098 4use std::ffi::{OsStr, OsString};
bd4a6c5b 5use std::iter::FromIterator;
40da8098
WB
6use std::os::unix::ffi::OsStrExt;
7use std::path::PathBuf;
8use std::str::FromStr;
9
10use failure::{bail, format_err, Error};
11use nix::sys::stat;
12use nix::unistd::Pid;
13
14/// A mount ID as found within `/proc/PID/mountinfo`.
bd4a6c5b 15#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
40da8098
WB
16#[repr(transparent)]
17pub struct MountId(usize);
18
19impl FromStr for MountId {
20 type Err = <usize as FromStr>::Err;
21
22 fn from_str(s: &str) -> Result<Self, Self::Err> {
23 s.parse().map(Self)
24 }
25}
26
27/// A device node entry (major:minor). This is a more strongly typed version of dev_t.
28#[derive(Copy, Clone, Debug, Eq, PartialEq)]
29pub struct Device {
30 major: u32,
31 minor: u32,
32}
33
34impl Device {
35 pub fn from_dev_t(dev: stat::dev_t) -> Self {
36 Self {
37 major: stat::major(dev) as u32,
38 minor: stat::minor(dev) as u32,
39 }
40 }
41
42 pub fn into_dev_t(self) -> stat::dev_t {
43 stat::makedev(u64::from(self.major), u64::from(self.minor))
44 }
45}
46
47impl FromStr for Device {
48 type Err = Error;
49
50 fn from_str(s: &str) -> Result<Self, Error> {
51 let (major, minor) = s.split_at(
52 s.find(':')
53 .ok_or_else(|| format_err!("expected 'major:minor' format"))?,
54 );
55 Ok(Self {
56 major: major.parse()?,
57 minor: minor[1..].parse()?,
58 })
59 }
60}
61
62#[derive(Clone, Debug)]
63#[cfg_attr(test, derive(Eq, PartialEq))]
64pub struct Tag {
65 pub tag: OsString,
66 pub value: Option<OsString>,
67}
68
69impl Tag {
70 fn parse(tag: &[u8]) -> Result<Self, Error> {
71 Ok(match tag.iter().position(|b| *b == b':') {
72 Some(pos) => {
73 let (tag, value) = tag.split_at(pos);
74 Self {
75 tag: OsStr::from_bytes(tag).to_owned(),
76 value: Some(OsStr::from_bytes(&value[1..]).to_owned()),
77 }
78 }
79 None => Self {
80 tag: OsStr::from_bytes(tag).to_owned(),
81 value: None,
82 },
83 })
84 }
85}
86
20fe5e17 87#[derive(Clone, Debug)]
40da8098
WB
88pub struct Entry {
89 /// unique identifier of the mount (may be reused after being unmounted)
90 pub id: MountId,
91
92 /// id of the parent (or of self for the top of the mount tree)
93 pub parent: MountId,
94
95 /// value of st_dev for files on this file system
96 pub device: Device,
97
98 /// root of the mount within the file system
99 pub root: PathBuf,
100
101 /// mount point relative to the process' root
102 pub mount_point: PathBuf,
103
104 /// per-mount options
105 pub mount_options: OsString,
106
107 /// tags
108 pub tags: Vec<Tag>,
109
110 /// Name of the file system in the form "type[.subtype]".
111 pub fs_type: String,
112
113 /// File system specific mount source information.
114 pub mount_source: Option<OsString>,
115
116 /// superblock options
117 pub super_options: OsString,
118}
119
120impl Entry {
121 /// Parse a line from a `mountinfo` file.
122 pub fn parse(line: &[u8]) -> Result<Self, Error> {
123 let mut parts = line.split(u8::is_ascii_whitespace);
124
125 let mut next = || {
126 parts
127 .next()
128 .ok_or_else(|| format_err!("incomplete mountinfo line"))
129 };
130
131 let this = Self {
132 id: std::str::from_utf8(next()?)?.parse()?,
133 parent: std::str::from_utf8(next()?)?.parse()?,
134 device: std::str::from_utf8(next()?)?.parse()?,
135 root: OsStr::from_bytes(next()?).to_owned().into(),
136 mount_point: OsStr::from_bytes(next()?).to_owned().into(),
137 mount_options: OsStr::from_bytes(next()?).to_owned(),
138 tags: next()?.split(|b| *b == b',').try_fold(
139 Vec::new(),
140 |mut acc, tag| -> Result<_, Error> {
141 acc.push(Tag::parse(tag)?);
142 Ok(acc)
143 },
144 )?,
145 fs_type: std::str::from_utf8({
146 next()?;
147 next()?
148 })?
149 .to_string(),
150 mount_source: next().map(|src| match src {
151 b"none" => None,
152 other => Some(OsStr::from_bytes(other).to_owned()),
153 })?,
154 super_options: OsStr::from_bytes(next()?).to_owned(),
155 };
156
157 if parts.next().is_some() {
158 bail!("excess data in mountinfo line");
159 }
160
161 Ok(this)
162 }
163}
164
165// TODO: Add some structure to this? Eg. sort by parent/child relation? Make a tree?
166/// Mount info found in `/proc/PID/mountinfo`.
20fe5e17 167#[derive(Clone, Debug)]
40da8098 168pub struct MountInfo {
bd4a6c5b 169 entries: BTreeMap<MountId, Entry>,
40da8098
WB
170}
171
775c96f9 172/// An iterator over entries in a `MountInfo`.
bd4a6c5b 173pub type Iter<'a> = std::collections::btree_map::Iter<'a, MountId, Entry>;
40da8098 174
775c96f9 175/// An iterator over mutable entries in a `MountInfo`.
bd4a6c5b 176pub type IterMut<'a> = std::collections::btree_map::IterMut<'a, MountId, Entry>;
775c96f9 177
40da8098
WB
178impl MountInfo {
179 /// Read the current mount point information.
180 pub fn read() -> Result<Self, Error> {
181 Self::parse(&std::fs::read("/proc/self/mountinfo")?)
182 }
183
184 /// Read the mount point information of a specific pid.
185 pub fn read_for_pid(pid: Pid) -> Result<Self, Error> {
186 Self::parse(&std::fs::read(format!("/proc/{}/mountinfo", pid))?)
187 }
188
189 /// Parse a `mountinfo` file.
190 pub fn parse(statstr: &[u8]) -> Result<Self, Error> {
df5eb296
WB
191 let entries = statstr
192 .split(|b| *b == b'\n')
193 .filter(|line| !line.is_empty())
194 .try_fold(Vec::new(), |mut acc, line| -> Result<_, Error> {
195 let entry = match Entry::parse(line) {
196 Ok(entry) => entry,
197 Err(err) => {
198 bail!(
199 "failed to parse mount info line: {:?}\n error: {}",
200 line,
201 err,
202 );
203 }
204 };
2ef0e795 205 acc.push(entry);
40da8098 206 Ok(acc)
df5eb296 207 })?;
40da8098 208
bd4a6c5b
WB
209 let entries = BTreeMap::from_iter(entries.into_iter().map(|entry| (entry.id, entry)));
210
40da8098
WB
211 Ok(Self { entries })
212 }
213
214 /// Iterate over mount entries.
215 pub fn iter(&self) -> Iter {
216 self.entries.iter()
217 }
775c96f9
WB
218
219 /// Check if there exists a mount point for a specific path.
220 ///
221 /// FIXME: Do we need to verify that mount points don't get "hidden" by other higher level
222 /// mount points? For this we'd need to implement mountpoint-tree iteration first, the info for
223 /// which we have available in the `Entry` struct!
224 pub fn path_is_mounted<P>(&self, path: &P) -> bool
225 where
226 PathBuf: PartialEq<P>,
227 {
bd4a6c5b 228 self.iter().any(|(_id, entry)| entry.mount_point == *path)
775c96f9
WB
229 }
230
231 /// Check whether there exists a mount point for a specified source.
232 pub fn source_is_mounted<T>(&self, source: &T) -> bool
233 where
234 OsString: PartialEq<T>,
235 {
236 self.iter()
bd4a6c5b 237 .filter_map(|(_id, entry)| entry.mount_source.as_ref())
775c96f9
WB
238 .any(|s| *s == *source)
239 }
240}
241
242impl IntoIterator for MountInfo {
bd4a6c5b
WB
243 type Item = (MountId, Entry);
244 type IntoIter = std::collections::btree_map::IntoIter<MountId, Entry>;
775c96f9
WB
245
246 fn into_iter(self) -> Self::IntoIter {
247 self.entries.into_iter()
248 }
249}
250
251impl<'a> IntoIterator for &'a MountInfo {
bd4a6c5b 252 type Item = (&'a MountId, &'a Entry);
775c96f9
WB
253 type IntoIter = Iter<'a>;
254
255 fn into_iter(self) -> Self::IntoIter {
256 (&self.entries).into_iter()
257 }
258}
259
260impl<'a> IntoIterator for &'a mut MountInfo {
bd4a6c5b 261 type Item = (&'a MountId, &'a mut Entry);
775c96f9
WB
262 type IntoIter = IterMut<'a>;
263
264 fn into_iter(self) -> Self::IntoIter {
265 (&mut self.entries).into_iter()
266 }
40da8098
WB
267}
268
bd4a6c5b
WB
269impl std::ops::Deref for MountInfo {
270 type Target = BTreeMap<MountId, Entry>;
271
272 fn deref(&self) -> &Self::Target {
273 &self.entries
274 }
275}
276
277impl std::ops::DerefMut for MountInfo {
278 fn deref_mut(&mut self) -> &mut Self::Target {
279 &mut self.entries
280 }
281}
282
40da8098
WB
283#[test]
284fn test_entry() {
285 use std::path::Path;
286
287 let l1: &[u8] =
288 b"48 32 0:43 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime shared:26 - cgroup \
289 cgroup rw,blkio";
290 let entry = Entry::parse(l1).expect("failed to parse first mountinfo test entry");
291
292 assert_eq!(entry.id, MountId(48));
293 assert_eq!(entry.parent, MountId(32));
294 assert_eq!(
295 entry.device,
296 Device {
297 major: 0,
298 minor: 43,
299 }
300 );
301 assert_eq!(entry.root, Path::new("/"));
302 assert_eq!(entry.mount_point, Path::new("/sys/fs/cgroup/blkio"));
303 assert_eq!(
304 entry.mount_options,
305 OsStr::new("rw,nosuid,nodev,noexec,relatime")
306 );
307 assert_eq!(
308 entry.tags,
309 &[Tag {
310 tag: OsString::from("shared"),
311 value: Some(OsString::from("26")),
312 }]
313 );
314 assert_eq!(entry.fs_type, "cgroup");
315 assert_eq!(
316 entry.mount_source.as_ref().map(|s| s.as_os_str()),
317 Some(OsStr::new("cgroup"))
318 );
319 assert_eq!(entry.super_options, "rw,blkio");
320
321 let l2 = b"49 28 0:44 / /proxmox/debian rw,relatime shared:27 - autofs systemd-1 \
322 rw,fd=26,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=27726";
323 let entry = Entry::parse(l2).expect("failed to parse second mountinfo test entry");
324 assert_eq!(entry.id, MountId(49));
325 assert_eq!(entry.parent, MountId(28));
326 assert_eq!(
327 entry.device,
328 Device {
329 major: 0,
330 minor: 44,
331 }
332 );
333 assert_eq!(entry.root, Path::new("/"));
334 assert_eq!(entry.mount_point, Path::new("/proxmox/debian"));
335 assert_eq!(entry.mount_options, OsStr::new("rw,relatime"));
336 assert_eq!(
337 entry.tags,
338 &[Tag {
339 tag: OsString::from("shared"),
340 value: Some(OsString::from("27")),
341 }]
342 );
343 assert_eq!(entry.fs_type, "autofs");
344 assert_eq!(
345 entry.mount_source.as_ref().map(|s| s.as_os_str()),
346 Some(OsStr::new("systemd-1"))
347 );
348 assert_eq!(
349 entry.super_options,
350 "rw,fd=26,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=27726"
351 );
352
353 let mount_info = [l1, l2].join(&b"\n"[..]);
354 MountInfo::parse(&mount_info).expect("failed to parse mount info file");
355}