use log::{debug, trace};
use std::fs::File;
use std::io::{Error, ErrorKind, Read, Write};
use std::path::{Path, PathBuf};
use std::str::FromStr;
#[test]
fn test_parser() {
use std::io::Cursor;
let expected_results = vec![
FsEntry {
fs_spec: "/dev/mapper/xubuntu--vg--ssd-root".to_string(),
mountpoint: PathBuf::from("/"),
vfs_type: "ext4".to_string(),
mount_options: vec!["noatime".to_string(), "errors=remount-ro".to_string()],
dump: false,
fsck_order: 1,
},
FsEntry {
fs_spec: "UUID=378f3c86-b21a-4172-832d-e2b3d4bc7511".to_string(),
mountpoint: PathBuf::from("/boot"),
vfs_type: "ext2".to_string(),
mount_options: vec!["defaults".to_string()],
dump: false,
fsck_order: 2,
},
FsEntry {
fs_spec: "/dev/mapper/xubuntu--vg--ssd-swap_1".to_string(),
mountpoint: PathBuf::from("none"),
vfs_type: "swap".to_string(),
mount_options: vec!["sw".to_string()],
dump: false,
fsck_order: 0,
},
FsEntry {
fs_spec: "UUID=be8a49b9-91a3-48df-b91b-20a0b409ba0f".to_string(),
mountpoint: PathBuf::from("/mnt/raid"),
vfs_type: "ext4".to_string(),
mount_options: vec!["errors=remount-ro".to_string(), "user".to_string()],
dump: false,
fsck_order: 1,
},
];
let input = r#"
# /etc/fstab: static file system information.
#
# Use 'blkid' to print the universally unique identifier for a
# device; this may be used with UUID= as a more robust way to name devices
# that works even if disks are added and removed. See fstab(5).
#
# <file system> <mount point> <type> <options> <dump> <pass>
/dev/mapper/xubuntu--vg--ssd-root / ext4 noatime,errors=remount-ro 0 1
# /boot was on /dev/sda1 during installation
UUID=378f3c86-b21a-4172-832d-e2b3d4bc7511 /boot ext2 defaults 0 2
/dev/mapper/xubuntu--vg--ssd-swap_1 none swap sw 0 0
UUID=be8a49b9-91a3-48df-b91b-20a0b409ba0f /mnt/raid ext4 errors=remount-ro,user 0 1
# tmpfs /tmp tmpfs rw,nosuid,nodev
"#;
let bytes = input.as_bytes();
let mut buff = Cursor::new(bytes);
let fstab = FsTab::new(&Path::new("/fake"));
let results = fstab.parse_entries(&mut buff).unwrap();
println!("Result: {:?}", results);
assert_eq!(results, expected_results);
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FsEntry {
pub fs_spec: String,
pub mountpoint: PathBuf,
pub vfs_type: String,
pub mount_options: Vec<String>,
pub dump: bool,
pub fsck_order: u16,
}
#[derive(Debug)]
pub struct FsTab {
location: PathBuf,
}
impl Default for FsTab {
fn default() -> Self {
FsTab { location: PathBuf::from("/etc/fstab") }
}
}
impl FsTab {
pub fn new(fstab: &Path) -> Self {
FsTab { location: fstab.to_path_buf() }
}
pub fn get_entries(&self) -> Result<Vec<FsEntry>, Error> {
let mut file = File::open(&self.location)?;
let entries = self.parse_entries(&mut file)?;
Ok(entries)
}
fn parse_entries<T: Read>(&self, file: &mut T) -> Result<Vec<FsEntry>, Error> {
let mut entries: Vec<FsEntry> = Vec::new();
let mut contents = String::new();
file.read_to_string(&mut contents)?;
for line in contents.lines() {
if line.starts_with("#") {
trace!("Skipping commented line: {}", line);
continue;
}
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() != 6 {
debug!("Unknown fstab entry: {}", line);
continue;
}
let fsck_order = u16::from_str(parts[5]).map_err(|e| {
Error::new(ErrorKind::InvalidInput, e)
})?;
entries.push(FsEntry {
fs_spec: parts[0].to_string(),
mountpoint: PathBuf::from(parts[1]),
vfs_type: parts[2].to_string(),
mount_options: parts[3].split(",").map(|s| s.to_string()).collect(),
dump: if parts[4] == "0" { false } else { true },
fsck_order: fsck_order,
})
}
Ok(entries)
}
fn save_fstab(&self, entries: &Vec<FsEntry>) -> Result<usize, Error> {
let mut file = File::create(&self.location)?;
let mut bytes_written: usize = 0;
for entry in entries {
bytes_written += file.write(&format!(
"{spec} {mount} {vfs} {options} {dump} {fsck}\n",
spec = entry.fs_spec,
mount = entry.mountpoint.display(),
vfs = entry.vfs_type,
options = entry.mount_options.join(","),
dump = if entry.dump { "1" } else { "0" },
fsck = entry.fsck_order
).as_bytes())?;
}
file.flush()?;
debug!("Wrote {} bytes to fstab", bytes_written);
Ok(bytes_written)
}
pub fn add_entry(&self, entry: FsEntry) -> Result<bool, Error> {
let mut entries = self.get_entries()?;
let position = entries.iter().position(|e| e == &entry);
if let Some(pos) = position {
debug!("Removing {} from fstab entries", pos);
entries.remove(pos);
}
entries.push(entry);
self.save_fstab(&mut entries)?;
match position {
Some(_) => Ok(false),
None => Ok(true),
}
}
pub fn add_entries(&self, entries: Vec<FsEntry>) -> Result<(), Error> {
let mut existing_entries = self.get_entries()?;
for new_entry in entries {
match existing_entries.contains(&new_entry) {
false => existing_entries.push(new_entry),
true => {
let position = existing_entries
.iter()
.position(|e| e == &new_entry)
.unwrap();
existing_entries.remove(position);
existing_entries.push(new_entry);
}
}
}
self.save_fstab(&mut existing_entries)?;
Ok(())
}
pub fn remove_entry(&self, spec: &str) -> Result<bool, Error> {
let mut entries = self.get_entries()?;
let position = entries.iter().position(|e| e.fs_spec == spec);
match position {
Some(pos) => {
debug!("Removing {} from fstab entries", pos);
entries.remove(pos);
self.save_fstab(&mut entries)?;
Ok(true)
}
None => Ok(false),
}
}
}