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 }
73
74#[derive(Clone, Debug, Eq, PartialEq)]
76pub struct FsEntry {
77 pub fs_spec: String,
79 pub mountpoint: PathBuf,
81 pub vfs_type: String,
83 pub mount_options: Vec<String>,
85 pub dump: bool,
87 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 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 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 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 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 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}