]>
Commit | Line | Data |
---|---|---|
40da8098 WB |
1 | //! `/proc/PID/mountinfo` handling. |
2 | ||
bd4a6c5b | 3 | use std::collections::BTreeMap; |
40da8098 | 4 | use std::ffi::{OsStr, OsString}; |
bd4a6c5b | 5 | use std::iter::FromIterator; |
40da8098 WB |
6 | use std::os::unix::ffi::OsStrExt; |
7 | use std::path::PathBuf; | |
8 | use std::str::FromStr; | |
9 | ||
10 | use failure::{bail, format_err, Error}; | |
11 | use nix::sys::stat; | |
12 | use 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)] |
17 | pub struct MountId(usize); | |
18 | ||
19 | impl 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)] | |
29 | pub struct Device { | |
30 | major: u32, | |
31 | minor: u32, | |
32 | } | |
33 | ||
34 | impl 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 | ||
47 | impl 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))] | |
64 | pub struct Tag { | |
65 | pub tag: OsString, | |
66 | pub value: Option<OsString>, | |
67 | } | |
68 | ||
69 | impl 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 |
88 | pub 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 | ||
120 | impl 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 | 168 | pub struct MountInfo { |
bd4a6c5b | 169 | entries: BTreeMap<MountId, Entry>, |
40da8098 WB |
170 | } |
171 | ||
775c96f9 | 172 | /// An iterator over entries in a `MountInfo`. |
bd4a6c5b | 173 | pub type Iter<'a> = std::collections::btree_map::Iter<'a, MountId, Entry>; |
40da8098 | 174 | |
775c96f9 | 175 | /// An iterator over mutable entries in a `MountInfo`. |
bd4a6c5b | 176 | pub type IterMut<'a> = std::collections::btree_map::IterMut<'a, MountId, Entry>; |
775c96f9 | 177 | |
40da8098 WB |
178 | impl 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 | ||
242 | impl 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 | ||
251 | impl<'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 | ||
260 | impl<'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 |
269 | impl std::ops::Deref for MountInfo { |
270 | type Target = BTreeMap<MountId, Entry>; | |
271 | ||
272 | fn deref(&self) -> &Self::Target { | |
273 | &self.entries | |
274 | } | |
275 | } | |
276 | ||
277 | impl 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] |
284 | fn 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 | } |