mnt/
parse.rs

1// Copyright (C) 2014-2015 Mickaël Salaün
2//
3// This program is free software: you can redistribute it and/or modify
4// it under the terms of the GNU Lesser General Public License as published by
5// the Free Software Foundation, version 3 of the License.
6//
7// This program is distributed in the hope that it will be useful,
8// but WITHOUT ANY WARRANTY; without even the implied warranty of
9// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
10// GNU Lesser General Public License for more details.
11//
12// You should have received a copy of the GNU Lesser General Public License
13// along with this program. If not, see <http://www.gnu.org/licenses/>.
14
15extern crate libc;
16
17use error::*;
18use self::libc::c_int;
19use std::cmp::Ordering;
20use std::convert::{AsRef, From};
21use std::fmt;
22use std::fs::File;
23use std::io::{BufReader, BufRead, Lines};
24use std::iter::Enumerate;
25use std::path::{Path, PathBuf};
26use std::str::FromStr;
27
28const PROC_MOUNTS: &'static str = "/proc/mounts";
29
30#[derive(Clone, Copy, PartialEq, Eq, Debug)]
31pub enum DumpField {
32    Ignore = 0,
33    Backup = 1,
34}
35
36pub type PassField = Option<c_int>;
37
38#[derive(Clone, PartialEq, Eq, Debug)]
39pub enum MntOps {
40    Atime(bool),
41    DirAtime(bool),
42    RelAtime(bool),
43    Dev(bool),
44    Exec(bool),
45    Suid(bool),
46    Write(bool),
47    Extra(String),
48}
49
50impl FromStr for MntOps {
51    type Err = LineError;
52
53    fn from_str(token: &str) -> Result<MntOps, LineError> {
54        Ok(match token {
55            "atime" => MntOps::Atime(true),
56            "noatime" => MntOps::Atime(false),
57            "diratime" => MntOps::DirAtime(true),
58            "nodiratime" => MntOps::DirAtime(false),
59            "relatime" => MntOps::RelAtime(true),
60            "norelatime" => MntOps::RelAtime(false),
61            "dev" => MntOps::Dev(true),
62            "nodev" => MntOps::Dev(false),
63            "exec" => MntOps::Exec(true),
64            "noexec" => MntOps::Exec(false),
65            "suid" => MntOps::Suid(true),
66            "nosuid" => MntOps::Suid(false),
67            "rw" => MntOps::Write(true),
68            "ro" => MntOps::Write(false),
69            // TODO: Replace with &str
70            extra => MntOps::Extra(extra.to_string()),
71        })
72    }
73}
74
75#[derive(Clone, Debug)]
76pub enum MountParam<'a> {
77    Spec(&'a str),
78    File(&'a Path),
79    VfsType(&'a str),
80    MntOps(&'a MntOps),
81    Freq(&'a DumpField),
82    PassNo(&'a PassField),
83}
84
85#[derive(Clone, PartialEq, Eq)]
86pub struct MountEntry {
87    pub spec: String,
88    pub file: PathBuf,
89    pub vfstype: String,
90    pub mntops: Vec<MntOps>,
91    pub freq: DumpField,
92    pub passno: PassField,
93}
94
95impl MountEntry {
96    pub fn contains(&self, search: &MountParam) -> bool {
97        match search {
98            &MountParam::Spec(spec) => spec == &self.spec,
99            &MountParam::File(file) => file == &self.file,
100            &MountParam::VfsType(vfstype) => vfstype == &self.vfstype,
101            &MountParam::MntOps(mntops) => self.mntops.contains(mntops),
102            &MountParam::Freq(dumpfield) => dumpfield == &self.freq,
103            &MountParam::PassNo(passno) => passno == &self.passno,
104        }
105    }
106}
107
108impl FromStr for MountEntry {
109    type Err = LineError;
110
111    fn from_str(line: &str) -> Result<MountEntry, LineError> {
112        let line = line.trim();
113        let mut tokens = line.split_terminator(|s: char| { s == ' ' || s == '\t' })
114            .filter(|s| { s != &""  } );
115        Ok(MountEntry {
116            spec: try!(tokens.next().ok_or(LineError::MissingSpec)).to_string(),
117            file: {
118                let file = try!(tokens.next().ok_or(LineError::MissingFile));
119                let path = PathBuf::from(file);
120                if path.is_relative() {
121                    return Err(LineError::InvalidFilePath(file.into()));
122                }
123                path
124            },
125            vfstype: try!(tokens.next().ok_or(LineError::MissingVfstype)).to_string(),
126            mntops: try!(tokens.next().ok_or(LineError::MissingMntops))
127                // FIXME: Handle MntOps errors
128                .split_terminator(',').map(|x| { FromStr::from_str(x).unwrap() }).collect(),
129            freq: {
130                let freq = try!(tokens.next().ok_or(LineError::MissingFreq));
131                match FromStr::from_str(freq) {
132                    Ok(0) => DumpField::Ignore,
133                    Ok(1) => DumpField::Backup,
134                    _ => return Err(LineError::InvalidFreq(freq.into())),
135                }
136            },
137            passno: {
138                let passno = try!(tokens.next().ok_or(LineError::MissingPassno));
139                match FromStr::from_str(passno) {
140                    Ok(0) => None,
141                    Ok(f) if f > 0 => Some(f),
142                    _ => return Err(LineError::InvalidPassno(passno.into())),
143                }
144            },
145        })
146    }
147}
148
149
150/// Get a list of all mount points from `root` and beneath using a custom `BufRead`
151pub fn get_submounts_from<T, U>(root: T, iter: MountIter<U>)
152        -> Result<Vec<MountEntry>, ParseError> where T: AsRef<Path>, U: BufRead {
153    let mut ret = vec!();
154    for mount in iter {
155        match mount {
156            Ok(m) => if m.file.starts_with(&root) {
157                ret.push(m);
158            },
159            Err(e) => return Err(e),
160        }
161    }
162    Ok(ret)
163}
164
165/// Get a list of all mount points from `root` and beneath using */proc/mounts*
166pub fn get_submounts<T>(root: T) -> Result<Vec<MountEntry>, ParseError> where T: AsRef<Path> {
167    get_submounts_from(root, try!(MountIter::new_from_proc()))
168}
169
170/// Get the mount point for the `target` using a custom `BufRead`
171pub fn get_mount_from<T, U>(target: T, iter: MountIter<U>)
172        -> Result<Option<MountEntry>, ParseError> where T: AsRef<Path>, U: BufRead {
173    let mut ret = None;
174    for mount in iter {
175        match mount {
176            Ok(m) => if target.as_ref().starts_with(&m.file) {
177                // Get the last entry
178                ret = Some(m);
179            },
180            Err(e) => return Err(e),
181        }
182    }
183    Ok(ret)
184}
185
186/// Get the mount point for the `target` using */proc/mounts*
187pub fn get_mount<T>(target: T) -> Result<Option<MountEntry>, ParseError> where T: AsRef<Path> {
188    get_mount_from(target, try!(MountIter::new_from_proc()))
189}
190
191/// Find the potential mount point providing readable or writable access to a path
192///
193/// Do not check the path existence but its potentially parent mount point.
194pub fn get_mount_writable<T>(target: T, writable: bool) -> Option<MountEntry> where T: AsRef<Path> {
195    match get_mount(target) {
196        Ok(Some(m)) => {
197            if !writable || m.mntops.contains(&MntOps::Write(writable)) {
198                Some(m)
199            } else {
200                None
201            }
202        }
203        _ => None,
204    }
205}
206
207pub trait VecMountEntry {
208    fn remove_overlaps<T>(self, exclude_files: &Vec<T>) -> Self where T: AsRef<Path>;
209}
210
211impl VecMountEntry for Vec<MountEntry> {
212    // FIXME: Doesn't work for moved mounts: they don't change order
213    fn remove_overlaps<T>(self, exclude_files: &Vec<T>) -> Vec<MountEntry> where T: AsRef<Path> {
214        let mut sorted: Vec<MountEntry> = vec!();
215        let root = Path::new("/");
216        'list: for mount in self.into_iter().rev() {
217            // Strip fake root mounts (created from bind mounts)
218            if AsRef::<Path>::as_ref(&mount.file) == root {
219                continue 'list;
220            }
221            let mut has_overlaps = false;
222            'filter: for mount_sorted in sorted.iter() {
223                if exclude_files.iter().skip_while(|x|
224                       AsRef::<Path>::as_ref(&mount_sorted.file) != x.as_ref()).next().is_some() {
225                    continue 'filter;
226                }
227                // Check for mount overlaps
228                if mount.file.starts_with(&mount_sorted.file) {
229                    has_overlaps = true;
230                    break 'filter;
231                }
232            }
233            if !has_overlaps {
234                sorted.push(mount);
235            }
236        }
237        sorted.reverse();
238        sorted
239    }
240}
241
242
243impl fmt::Debug for MountEntry {
244    fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result {
245        write!(out, "MountEntry {{ spec: {:?}, file: {:?}, vfstype: {:?}, mntops: {:?}, freq: {:?}, passno: {:?} }}",
246               self.spec, self.file.display(), self.vfstype, self.mntops, self.freq, self.passno)
247    }
248}
249
250impl PartialOrd for MountEntry {
251    fn partial_cmp(&self, other: &MountEntry) -> Option<Ordering> {
252        self.file.partial_cmp(&other.file)
253    }
254}
255
256impl Ord for MountEntry {
257    fn cmp(&self, other: &MountEntry) -> Ordering {
258        self.file.cmp(&other.file)
259    }
260}
261
262
263pub struct MountIter<T> {
264    lines: Enumerate<Lines<T>>,
265}
266
267impl<T> MountIter<T> where T: BufRead {
268    pub fn new(mtab: T) -> MountIter<T> {
269        MountIter {
270            lines: mtab.lines().enumerate(),
271        }
272    }
273}
274
275impl MountIter<BufReader<File>> {
276    pub fn new_from_proc() -> Result<MountIter<BufReader<File>>, ParseError> {
277        let file = try!(File::open(PROC_MOUNTS));
278        Ok(MountIter::new(BufReader::new(file)))
279    }
280}
281
282impl<T> Iterator for MountIter<T> where T: BufRead {
283    type Item = Result<MountEntry, ParseError>;
284
285    fn next(&mut self) -> Option<<Self as Iterator>::Item> {
286        match self.lines.next() {
287            Some((nb, line)) => Some(match line {
288                Ok(line) => match <MountEntry as FromStr>::from_str(line.as_ref()) {
289                    Ok(m) => Ok(m),
290                    Err(e) => Err(ParseError::new(format!("Failed at line {}: {}", nb, e))),
291                },
292                Err(e) => Err(From::from(e)),
293            }),
294            None => None,
295        }
296    }
297}
298
299
300#[cfg(test)]
301mod test {
302    use std::fs::File;
303    use std::io::{BufReader, BufRead, Cursor};
304    use std::path::{Path, PathBuf};
305    use std::str::FromStr;
306    use super::{DumpField, MntOps, MountEntry, MountIter, MountParam, get_mount_from, get_submounts_from};
307
308    #[test]
309    fn test_line_root() {
310        let root_ref = MountEntry {
311            spec: "rootfs".to_string(),
312            file: PathBuf::from("/"),
313            vfstype: "rootfs".to_string(),
314            mntops: vec!(MntOps::Write(true)),
315            freq: DumpField::Ignore,
316            passno: None,
317        };
318        let from_str = <MountEntry as FromStr>::from_str;
319        assert_eq!(from_str("rootfs / rootfs rw 0 0"), Ok(root_ref.clone()));
320        assert_eq!(from_str("rootfs   / rootfs rw 0 0"), Ok(root_ref.clone()));
321        assert_eq!(from_str("rootfs	/ rootfs rw 0 0"), Ok(root_ref.clone()));
322        assert_eq!(from_str("rootfs / rootfs rw, 0 0"), Ok(root_ref.clone()));
323    }
324
325    #[test]
326    fn test_line_mntops() {
327        let root_ref = MountEntry {
328            spec: "rootfs".to_string(),
329            file: PathBuf::from("/"),
330            vfstype: "rootfs".to_string(),
331            mntops: vec!(MntOps::Exec(false), MntOps::Write(true)),
332            freq: DumpField::Ignore,
333            passno: None,
334        };
335        let from_str = <MountEntry as FromStr>::from_str;
336        assert_eq!(from_str("rootfs / rootfs noexec,rw 0 0"), Ok(root_ref.clone()));
337    }
338
339    fn test_file<T>(path: T) -> Result<(), String> where T: AsRef<Path> {
340        let file = match File::open(&path) {
341            Ok(f) => f,
342            Err(e) => return Err(format!("Failed to open {}: {}", path.as_ref().display(), e)),
343        };
344        let mount = BufReader::new(file);
345        for line in mount.lines() {
346            let line = match line {
347                Ok(l) => l,
348                Err(e) => return Err(format!("Failed to read line: {}", e)),
349            };
350            match <MountEntry as FromStr>::from_str(line.as_ref()) {
351                Ok(_) => {},
352                Err(e) => return Err(format!("Error for `{}`: {}", line.trim(), e)),
353            }
354        }
355        Ok(())
356    }
357
358    #[test]
359    fn test_proc_mounts() {
360        assert!(test_file("/proc/mounts").is_ok());
361    }
362
363    #[test]
364    fn test_path() {
365        let from_str = <MountEntry as FromStr>::from_str;
366        assert!(from_str("rootfs ./ rootfs rw 0 0").is_err());
367        assert!(from_str("rootfs foo rootfs rw 0 0").is_err());
368        // Should fail for a swap pseudo-mount
369        assert!(from_str("/dev/mapper/swap none swap sw 0 0").is_err());
370    }
371
372    #[test]
373    fn test_proc_mounts_from() {
374        use super::MntOps::*;
375        use super::DumpField::*;
376
377        let buf = Cursor::new(b"\
378            rootfs / rootfs rw 0 0\n\
379            sysfs /sys sysfs rw,nosuid,nodev,noexec,relatime 0 0\n\
380            tmpfs /sys/fs/cgroup tmpfs ro,nosuid,nodev,noexec,mode=755 0 0\n\
381            udev /dev devtmpfs rw,relatime,size=10240k,nr_inodes=505357,mode=755 0 0\n\
382            tmpfs /run tmpfs rw,nosuid,relatime,size=809928k,mode=755 0 0\n\
383            /dev/mapper/foo-tmp /var/tmp ext4 rw,relatime,data=ordered 0 0\n\
384        ".as_ref());
385        // FIXME: Append /dev/dm-0 / ext4 rw,relatime,errors=remount-ro,data=ordered 0 0\n\
386        let mount_vartmp = MountEntry {
387            spec: "/dev/mapper/foo-tmp".to_string(),
388            file: PathBuf::from("/var/tmp"),
389            vfstype: "ext4".to_string(),
390            mntops: vec![Write(true), RelAtime(true), Extra("data=ordered".to_string())],
391            freq: Ignore,
392            passno: None
393        };
394        let mount_root = MountEntry {
395            spec: "rootfs".to_string(),
396            file: PathBuf::from("/"),
397            vfstype: "rootfs".to_string(),
398            mntops: vec![Write(true)],
399            freq: Ignore,
400            passno: None
401        };
402        let mount_sysfs = MountEntry {
403            spec: "sysfs".to_string(),
404            file: PathBuf::from("/sys"),
405            vfstype: "sysfs".to_string(),
406            mntops: vec![Write(true), Suid(false), Dev(false), Exec(false), RelAtime(true)],
407            freq: Ignore,
408            passno: None
409        };
410        let mount_tmp = MountEntry {
411            spec: "tmpfs".to_string(),
412            file: PathBuf::from("/sys/fs/cgroup"),
413            vfstype: "tmpfs".to_string(),
414            mntops: vec![Write(false), Suid(false), Dev(false), Exec(false), Extra("mode=755".to_string())],
415            freq: Ignore,
416            passno: None
417        };
418        let mounts_all = vec!(
419            mount_root.clone(),
420            mount_sysfs.clone(),
421            mount_tmp.clone(),
422            MountEntry {
423                spec: "udev".to_string(),
424                file: PathBuf::from("/dev"),
425                vfstype: "devtmpfs".to_string(),
426                mntops: vec![Write(true), RelAtime(true), Extra("size=10240k".to_string()), Extra("nr_inodes=505357".to_string()), Extra("mode=755".to_string())],
427                freq: Ignore,
428                passno: None
429            },
430            MountEntry {
431                spec: "tmpfs".to_string(),
432                file: PathBuf::from("/run"),
433                vfstype: "tmpfs".to_string(),
434                mntops: vec![Write(true), Suid(false), RelAtime(true), Extra("size=809928k".to_string()), Extra("mode=755".to_string())],
435                freq: Ignore,
436                passno: None
437            },
438            mount_vartmp.clone()
439        );
440        let mounts = MountIter::new(buf.clone());
441        assert_eq!(mounts.map(|x| x.unwrap() ).collect::<Vec<_>>(), mounts_all.clone());
442        let mounts = MountIter::new(buf.clone());
443        assert_eq!(get_submounts_from("/", mounts).ok(), Some(mounts_all.clone()));
444        let mounts = MountIter::new(buf.clone());
445        assert_eq!(get_submounts_from("/var/tmp", mounts).ok(), Some(vec!(mount_vartmp.clone())));
446        let mounts = MountIter::new(buf.clone());
447        assert_eq!(get_mount_from("/var/tmp/bar", mounts).ok(), Some(Some(mount_vartmp.clone())));
448        let mounts = MountIter::new(buf.clone());
449        assert_eq!(get_mount_from("/var/", mounts).ok(), Some(Some(mount_root.clone())));
450
451        // search
452        let mut mounts = MountIter::new(buf.clone()).map(|m| m.ok().unwrap());;
453        assert_eq!(mounts.find(|m|
454               m.contains(&MountParam::Spec("rootfs"))
455            ).unwrap(), mount_root.clone());
456        let mut mounts = MountIter::new(buf.clone()).map(|m| m.ok().unwrap());;
457        assert_eq!(mounts.find(|m|
458                m.contains(&MountParam::File(Path::new("/")))
459            ).unwrap(), mount_root.clone());
460        let mut mounts = MountIter::new(buf.clone()).map(|m| m.ok().unwrap());;
461        assert_eq!(mounts.find(|m|
462                m.contains(&MountParam::VfsType("tmpfs"))
463            ).unwrap(), mount_tmp.clone());
464        let mut mounts = MountIter::new(buf.clone()).map(|m| m.ok().unwrap());;
465        let mnt_ops = [MntOps::Write(true), MntOps::Suid(false), MntOps::Dev(false), MntOps::Exec(false)];
466        assert_eq!(mounts.find(|m| {
467                mnt_ops.iter().all( |o| m.contains(&MountParam::MntOps(o)) )
468            }).unwrap(), mount_sysfs.clone());
469
470        let mounts = MountIter::new(buf.clone()).map(|m| m.ok().unwrap());
471        assert_eq!(mounts.filter(|m|
472                 m.contains(&MountParam::Freq(&DumpField::Ignore))
473            ).collect::<Vec<_>>(), mounts_all.clone());
474        let mounts = MountIter::new(buf.clone()).map(|m| m.ok().unwrap());
475        assert_eq!(mounts.filter(|m|
476                m.contains(&MountParam::PassNo(&None))
477            ).collect::<Vec<_>>(), mounts_all.clone());
478    }
479}