fstab/
lib.rs

1use log::{debug, trace};
2
3use std::fs::File;
4use std::io::{Error, ErrorKind, Read, Write};
5use std::path::{Path, PathBuf};
6use std::str::FromStr;
7
8#[test]
9fn test_parser() {
10    use std::io::Cursor;
11    let expected_results = vec![
12        FsEntry {
13            fs_spec: "/dev/mapper/xubuntu--vg--ssd-root".to_string(),
14            mountpoint: PathBuf::from("/"),
15            vfs_type: "ext4".to_string(),
16            mount_options: vec!["noatime".to_string(), "errors=remount-ro".to_string()],
17            dump: false,
18            fsck_order: 1,
19        },
20        FsEntry {
21            fs_spec: "UUID=378f3c86-b21a-4172-832d-e2b3d4bc7511".to_string(),
22            mountpoint: PathBuf::from("/boot"),
23            vfs_type: "ext2".to_string(),
24            mount_options: vec!["defaults".to_string()],
25            dump: false,
26            fsck_order: 2,
27        },
28        FsEntry {
29            fs_spec: "/dev/mapper/xubuntu--vg--ssd-swap_1".to_string(),
30            mountpoint: PathBuf::from("none"),
31            vfs_type: "swap".to_string(),
32            mount_options: vec!["sw".to_string()],
33            dump: false,
34            fsck_order: 0,
35        },
36        FsEntry {
37            fs_spec: "UUID=be8a49b9-91a3-48df-b91b-20a0b409ba0f".to_string(),
38            mountpoint: PathBuf::from("/mnt/raid"),
39            vfs_type: "ext4".to_string(),
40            mount_options: vec!["errors=remount-ro".to_string(), "user".to_string()],
41            dump: false,
42            fsck_order: 1,
43        },
44    ];
45    let input = r#"
46# /etc/fstab: static file system information.
47#
48# Use 'blkid' to print the universally unique identifier for a
49# device; this may be used with UUID= as a more robust way to name devices
50# that works even if disks are added and removed. See fstab(5).
51#
52# <file system> <mount point>   <type>  <options>       <dump>  <pass>
53/dev/mapper/xubuntu--vg--ssd-root /               ext4    noatime,errors=remount-ro 0       1
54# /boot was on /dev/sda1 during installation
55UUID=378f3c86-b21a-4172-832d-e2b3d4bc7511 /boot           ext2    defaults        0       2
56/dev/mapper/xubuntu--vg--ssd-swap_1 none            swap    sw              0       0
57UUID=be8a49b9-91a3-48df-b91b-20a0b409ba0f /mnt/raid ext4 errors=remount-ro,user 0 1
58# tmpfs /tmp tmpfs rw,nosuid,nodev
59"#;
60    let bytes = input.as_bytes();
61    let mut buff = Cursor::new(bytes);
62    let fstab = FsTab::new(&Path::new("/fake"));
63    let results = fstab.parse_entries(&mut buff).unwrap();
64    println!("Result: {:?}", results);
65    assert_eq!(results, expected_results);
66
67    //Modify an entry and then update it and see what the results are
68
69    //let bytes_written = super::add_entry(expected_results[1].clone(), Path::new("/tmp/fstab"))
70    //    .unwrap();
71    //println!("Wrote: {}", bytes_written);
72}
73
74/// For help with what these fields mean consult: `man fstab` on linux.
75#[derive(Clone, Debug, Eq, PartialEq)]
76pub struct FsEntry {
77    /// The device identifier
78    pub fs_spec: String,
79    /// The mount point
80    pub mountpoint: PathBuf,
81    /// Which filesystem type it is
82    pub vfs_type: String,
83    /// Mount options to use
84    pub mount_options: Vec<String>,
85    /// This field is used by dump(8) to determine which filesystems need to be dumped
86    pub dump: bool,
87    /// This field is used by fsck(8) to determine the order in which filesystem checks
88    /// are done at boot time.
89    pub fsck_order: u16,
90}
91
92#[derive(Debug)]
93pub struct FsTab {
94    location: PathBuf,
95}
96
97impl Default for FsTab {
98    fn default() -> Self {
99        FsTab { location: PathBuf::from("/etc/fstab") }
100    }
101}
102
103impl FsTab {
104    pub fn new(fstab: &Path) -> Self {
105        FsTab { location: fstab.to_path_buf() }
106    }
107
108    /// Takes the location to the fstab and parses it.  On linux variants
109    /// this is usually /etc/fstab.  On SVR4 systems store block devices and
110    /// mount point information in /etc/vfstab file. AIX stores block device
111    /// and mount points information in /etc/filesystems file.
112    pub fn get_entries(&self) -> Result<Vec<FsEntry>, Error> {
113        let mut file = File::open(&self.location)?;
114        let entries = self.parse_entries(&mut file)?;
115        Ok(entries)
116    }
117
118    fn parse_entries<T: Read>(&self, file: &mut T) -> Result<Vec<FsEntry>, Error> {
119        let mut entries: Vec<FsEntry> = Vec::new();
120        let mut contents = String::new();
121        file.read_to_string(&mut contents)?;
122
123        for line in contents.lines() {
124            if line.starts_with("#") {
125                trace!("Skipping commented line: {}", line);
126                continue;
127            }
128            let parts: Vec<&str> = line.split_whitespace().collect();
129            if parts.len() != 6 {
130                debug!("Unknown fstab entry: {}", line);
131                continue;
132            }
133            let fsck_order = u16::from_str(parts[5]).map_err(|e| {
134                Error::new(ErrorKind::InvalidInput, e)
135            })?;
136            entries.push(FsEntry {
137                fs_spec: parts[0].to_string(),
138                mountpoint: PathBuf::from(parts[1]),
139                vfs_type: parts[2].to_string(),
140                mount_options: parts[3].split(",").map(|s| s.to_string()).collect(),
141                dump: if parts[4] == "0" { false } else { true },
142                fsck_order: fsck_order,
143            })
144        }
145        Ok(entries)
146    }
147
148    fn save_fstab(&self, entries: &Vec<FsEntry>) -> Result<usize, Error> {
149        let mut file = File::create(&self.location)?;
150        let mut bytes_written: usize = 0;
151        for entry in entries {
152            bytes_written += file.write(&format!(
153                "{spec} {mount} {vfs} {options} {dump} {fsck}\n",
154                spec = entry.fs_spec,
155                mount = entry.mountpoint.display(),
156                vfs = entry.vfs_type,
157                options = entry.mount_options.join(","),
158                dump = if entry.dump { "1" } else { "0" },
159                fsck = entry.fsck_order
160            ).as_bytes())?;
161        }
162        file.flush()?;
163        debug!("Wrote {} bytes to fstab", bytes_written);
164        Ok(bytes_written)
165    }
166
167    /// Add a new entry to the fstab.  If the fstab previously did not contain this entry
168    /// then true is returned.  Otherwise it will return false indicating it has been updated
169    pub fn add_entry(&self, entry: FsEntry) -> Result<bool, Error> {
170        let mut entries = self.get_entries()?;
171
172        let position = entries.iter().position(|e| e == &entry);
173        if let Some(pos) = position {
174            debug!("Removing {} from fstab entries", pos);
175            entries.remove(pos);
176        }
177        entries.push(entry);
178        self.save_fstab(&mut entries)?;
179
180        match position {
181            Some(_) => Ok(false),
182            None => Ok(true),
183        }
184    }
185
186    /// Bulk add a new entries to the fstab.
187    pub fn add_entries(&self, entries: Vec<FsEntry>) -> Result<(), Error> {
188        let mut existing_entries = self.get_entries()?;
189        for new_entry in entries {
190            match existing_entries.contains(&new_entry) {
191                false => existing_entries.push(new_entry),
192                true => {
193                    // The old entries contain this so lets update it
194                    let position = existing_entries
195                        .iter()
196                        .position(|e| e == &new_entry)
197                        .unwrap();
198                    existing_entries.remove(position);
199                    existing_entries.push(new_entry);
200                }
201            }
202        }
203        self.save_fstab(&mut existing_entries)?;
204        Ok(())
205    }
206
207    /// Remove the fstab entry that corresponds to the spec given.  IE: first fields match
208    /// Returns true if the value was present in the fstab.
209    pub fn remove_entry(&self, spec: &str) -> Result<bool, Error> {
210        let mut entries = self.get_entries()?;
211        let position = entries.iter().position(|e| e.fs_spec == spec);
212
213        match position {
214            Some(pos) => {
215                debug!("Removing {} from fstab entries", pos);
216                entries.remove(pos);
217                self.save_fstab(&mut entries)?;
218                Ok(true)
219            }
220            None => Ok(false),
221        }
222    }
223}