use crate::error::{EntryBuilderError, MountPointError};
use crate::fstype::FsType;
use crate::options::Options;
use crate::spec::Spec;
use std::fmt;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MountPoint(PathBuf);
impl MountPoint {
pub fn new(path: impl Into<PathBuf>) -> Result<Self, MountPointError> {
let path = path.into();
let s = path.to_string_lossy();
if s.is_empty() {
return Err(MountPointError::Empty);
}
if s == "none" || s.starts_with('/') {
Ok(MountPoint(path))
} else {
Err(MountPointError::NotAbsolute)
}
}
#[must_use]
pub fn swap() -> Self {
MountPoint(PathBuf::from("none"))
}
#[must_use]
pub fn is_swap(&self) -> bool {
self.0.to_string_lossy() == "none"
}
#[must_use]
pub fn is_root(&self) -> bool {
let normalized: PathBuf = self.0.components().collect();
normalized == Path::new("/")
}
#[must_use]
pub fn as_path(&self) -> &Path {
&self.0
}
}
impl std::ops::Deref for MountPoint {
type Target = Path;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl fmt::Display for MountPoint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0.display())
}
}
impl AsRef<Path> for MountPoint {
fn as_ref(&self) -> &Path {
&self.0
}
}
impl TryFrom<&str> for MountPoint {
type Error = MountPointError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
MountPoint::new(s)
}
}
impl TryFrom<String> for MountPoint {
type Error = MountPointError;
fn try_from(s: String) -> Result<Self, Self::Error> {
MountPoint::new(s)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Entry {
pub spec: Spec,
pub file: MountPoint,
pub vfstype: FsType,
pub options: Options,
pub freq: u32,
pub passno: u32,
pub comment: Option<String>,
}
impl Entry {
#[must_use]
pub fn new(spec: Spec, file: MountPoint, vfstype: FsType, options: Options) -> Self {
Entry {
spec,
file,
vfstype,
options,
freq: 0,
passno: 0,
comment: None,
}
}
#[must_use]
pub fn builder() -> EntryBuilder {
EntryBuilder::default()
}
#[must_use]
pub fn is_swap(&self) -> bool {
self.vfstype.is_swap()
}
#[must_use]
pub fn is_bind_mount(&self) -> bool {
self.vfstype.is_bind()
}
#[must_use]
pub fn is_root(&self) -> bool {
self.file.is_root() && self.passno == 1
}
#[must_use]
pub fn is_network(&self) -> bool {
self.vfstype.is_network() || self.options.is_netdev()
}
#[must_use]
pub fn comment(&self) -> Option<&str> {
self.comment.as_deref()
}
}
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct EntryBuilder {
spec: Option<Spec>,
file: Option<MountPoint>,
vfstype: Option<FsType>,
options: Options,
freq: u32,
passno: u32,
comment: Option<String>,
}
impl EntryBuilder {
pub fn spec(mut self, spec: Spec) -> Self {
self.spec = Some(spec);
self
}
pub fn file(mut self, file: MountPoint) -> Self {
self.file = Some(file);
self
}
pub fn vfstype(mut self, vfstype: FsType) -> Self {
self.vfstype = Some(vfstype);
self
}
pub fn options(mut self, options: Options) -> Self {
self.options = options;
self
}
pub fn freq(mut self, freq: u32) -> Self {
self.freq = freq;
self
}
pub fn passno(mut self, passno: u32) -> Self {
self.passno = passno;
self
}
pub fn comment(mut self, comment: impl Into<String>) -> Self {
self.comment = Some(comment.into());
self
}
pub fn build(self) -> Result<Entry, EntryBuilderError> {
Ok(Entry {
spec: self.spec.ok_or(EntryBuilderError::MissingSpec)?,
file: self.file.ok_or(EntryBuilderError::MissingFile)?,
vfstype: self.vfstype.ok_or(EntryBuilderError::MissingFsType)?,
options: self.options,
freq: self.freq,
passno: self.passno,
comment: self.comment,
})
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Fstab {
pub intro_comment: Option<String>,
pub entries: Vec<Entry>,
pub trailing_comment: Option<String>,
}
impl Fstab {
#[must_use]
pub fn new() -> Self {
Fstab::default()
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
#[must_use]
pub fn entries(&self) -> &[Entry] {
&self.entries
}
#[must_use]
pub fn from_entries(entries: Vec<Entry>) -> Self {
Fstab {
entries,
intro_comment: None,
trailing_comment: None,
}
}
#[must_use]
pub fn into_entries(self) -> Vec<Entry> {
self.entries
}
pub fn add(&mut self, entry: Entry) -> &mut Self {
self.entries.push(entry);
self
}
pub fn insert(
&mut self,
index: usize,
entry: Entry,
) -> Result<&mut Self, crate::error::FstabError> {
if index > self.entries.len() {
return Err(crate::error::FstabError::IndexOutOfBounds(
index,
self.entries.len(),
));
}
self.entries.insert(index, entry);
Ok(self)
}
pub fn remove(&mut self, index: usize) -> Option<Entry> {
if index < self.entries.len() {
Some(self.entries.remove(index))
} else {
None
}
}
pub fn replace(&mut self, index: usize, entry: Entry) -> Option<Entry> {
if index < self.entries.len() {
Some(std::mem::replace(&mut self.entries[index], entry))
} else {
None
}
}
pub fn clear(&mut self) {
self.entries.clear();
self.intro_comment = None;
self.trailing_comment = None;
}
#[must_use]
pub fn find_by_source(&self, source: &str) -> Vec<&Entry> {
self.entries
.iter()
.filter(|e| e.spec.to_string().contains(source))
.collect()
}
#[must_use]
pub fn find_by_mountpoint(&self, mp: &Path) -> Option<&Entry> {
self.entries.iter().find(|e| e.file.as_path() == mp)
}
#[must_use]
pub fn root(&self) -> Option<&Entry> {
self.entries
.iter()
.find(|e| e.passno == 1 && e.file.is_root())
}
}
impl IntoIterator for Fstab {
type Item = Entry;
type IntoIter = std::vec::IntoIter<Entry>;
fn into_iter(self) -> Self::IntoIter {
self.entries.into_iter()
}
}
impl<'a> IntoIterator for &'a Fstab {
type Item = &'a Entry;
type IntoIter = std::slice::Iter<'a, Entry>;
fn into_iter(self) -> Self::IntoIter {
self.entries.iter()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::EntryBuilderError;
use crate::fstype::FsType;
use crate::options::Options;
use crate::spec::Spec;
use std::path::Path;
#[test]
fn mount_point_new_absolute_path() {
let mp = MountPoint::new("/mnt/data").unwrap();
assert_eq!(mp.as_path(), Path::new("/mnt/data"));
assert!(!mp.is_swap());
assert!(!mp.is_root());
}
#[test]
fn mount_point_new_root() {
let mp = MountPoint::new("/").unwrap();
assert!(mp.is_root());
assert!(!mp.is_swap());
}
#[test]
fn mount_point_new_none() {
let mp = MountPoint::new("none").unwrap();
assert!(mp.is_swap());
assert!(!mp.is_root());
}
#[test]
fn mount_point_new_empty() {
let err = MountPoint::new("").unwrap_err();
assert_eq!(err, MountPointError::Empty);
}
#[test]
fn mount_point_new_not_absolute() {
let err = MountPoint::new("relative/path").unwrap_err();
assert_eq!(err, MountPointError::NotAbsolute);
}
#[test]
fn mount_point_swap_constructor() {
let mp = MountPoint::swap();
assert!(mp.is_swap());
assert_eq!(mp.as_path(), Path::new("none"));
}
#[test]
fn mount_point_deref() {
let mp = MountPoint::new("/etc").unwrap();
let path: &Path = &*mp;
assert_eq!(path, Path::new("/etc"));
assert!(mp.is_absolute());
assert!(mp.parent() == Some(Path::new("/")));
}
#[test]
fn entry_new_defaults() {
let spec = Spec::Device("/dev/sda1".into());
let file = MountPoint::new("/").unwrap();
let fstype = FsType::new("ext4").unwrap();
let opts = Options::defaults();
let entry = Entry::new(spec.clone(), file.clone(), fstype.clone(), opts.clone());
assert_eq!(entry.spec, spec);
assert_eq!(entry.file, file);
assert_eq!(entry.vfstype, fstype);
assert_eq!(entry.options, opts);
assert_eq!(entry.freq, 0);
assert_eq!(entry.passno, 0);
assert_eq!(entry.comment, None);
}
#[test]
fn entry_is_swap() {
let entry = Entry {
spec: Spec::Keyword("none".into()),
file: MountPoint::new("none").unwrap(),
vfstype: FsType::swap(),
options: Options::defaults(),
freq: 0,
passno: 0,
comment: None,
};
assert!(entry.is_swap());
}
#[test]
fn entry_is_bind_mount() {
let entry = Entry {
spec: Spec::Device("/dev/sda1".into()),
file: MountPoint::new("/mnt/bind").unwrap(),
vfstype: FsType::bind(),
options: Options::defaults(),
freq: 0,
passno: 0,
comment: None,
};
assert!(entry.is_bind_mount());
}
#[test]
fn entry_is_root() {
let entry = Entry {
spec: Spec::Device("/dev/sda1".into()),
file: MountPoint::new("/").unwrap(),
vfstype: FsType::new("ext4").unwrap(),
options: Options::defaults(),
freq: 0,
passno: 1,
comment: None,
};
assert!(entry.is_root());
}
#[test]
fn entry_is_root_requires_passno() {
let entry = Entry {
spec: Spec::Device("/dev/sda1".into()),
file: MountPoint::new("/").unwrap(),
vfstype: FsType::new("ext4").unwrap(),
options: Options::defaults(),
freq: 0,
passno: 0,
comment: None,
};
assert!(!entry.is_root());
}
#[test]
fn entry_is_network_by_fstype() {
let entry = Entry {
spec: Spec::NetworkMount {
host: "server".into(),
path: "/export".into(),
},
file: MountPoint::new("/mnt/nfs").unwrap(),
vfstype: FsType::new("nfs").unwrap(),
options: Options::new(),
freq: 0,
passno: 0,
comment: None,
};
assert!(entry.is_network());
}
#[test]
fn entry_is_network_by_netdev_option() {
let entry = Entry {
spec: Spec::Device("/dev/sda1".into()),
file: MountPoint::new("/mnt/data").unwrap(),
vfstype: FsType::new("ext4").unwrap(),
options: Options::parse("_netdev").unwrap(),
freq: 0,
passno: 0,
comment: None,
};
assert!(entry.is_network());
}
#[test]
fn entry_builder_minimal() {
let entry = Entry::builder()
.spec(Spec::parse("/dev/sda1").unwrap())
.file(MountPoint::new("/").unwrap())
.vfstype(FsType::parse("ext4").unwrap())
.build()
.unwrap();
assert_eq!(entry.spec, Spec::Device("/dev/sda1".into()));
assert!(entry.file.is_root());
assert_eq!(entry.vfstype.as_str(), "ext4");
assert!(entry.options.is_empty());
assert_eq!(entry.freq, 0);
assert_eq!(entry.passno, 0);
}
#[test]
fn entry_builder_all_fields() {
let entry = Entry::builder()
.spec(Spec::parse("UUID=root").unwrap())
.file(MountPoint::new("/").unwrap())
.vfstype(FsType::parse("ext4").unwrap())
.options(Options::parse("defaults,noatime").unwrap())
.freq(0)
.passno(1)
.comment("# Root filesystem")
.build()
.unwrap();
assert_eq!(entry.spec, Spec::Uuid("root".into()));
assert!(entry.file.is_root());
assert_eq!(entry.passno, 1);
assert_eq!(entry.comment, Some("# Root filesystem".into()));
}
#[test]
fn entry_builder_missing_spec() {
let err = Entry::builder()
.file(MountPoint::new("/").unwrap())
.vfstype(FsType::parse("ext4").unwrap())
.build()
.unwrap_err();
assert_eq!(err, EntryBuilderError::MissingSpec);
}
#[test]
fn entry_builder_missing_file() {
let err = Entry::builder()
.spec(Spec::parse("/dev/sda1").unwrap())
.vfstype(FsType::parse("ext4").unwrap())
.build()
.unwrap_err();
assert_eq!(err, EntryBuilderError::MissingFile);
}
#[test]
fn entry_builder_missing_vfstype() {
let err = Entry::builder()
.spec(Spec::parse("/dev/sda1").unwrap())
.file(MountPoint::new("/").unwrap())
.build()
.unwrap_err();
assert_eq!(err, EntryBuilderError::MissingFsType);
}
#[test]
fn entry_comment_accessor() {
let entry = Entry {
comment: Some("# Test".into()),
..Entry::new(
Spec::Device("/dev/sda1".into()),
MountPoint::new("/").unwrap(),
FsType::new("ext4").unwrap(),
Options::defaults(),
)
};
assert_eq!(entry.comment(), Some("# Test"));
}
#[test]
fn fstab_new_is_empty() {
let fstab = Fstab::new();
assert!(fstab.is_empty());
assert_eq!(fstab.len(), 0);
}
#[test]
fn fstab_add_entry() {
let mut fstab = Fstab::new();
let entry = Entry::new(
Spec::Device("/dev/sda1".into()),
MountPoint::new("/").unwrap(),
FsType::new("ext4").unwrap(),
Options::defaults(),
);
fstab.add(entry);
assert_eq!(fstab.len(), 1);
assert!(!fstab.is_empty());
}
#[test]
fn fstab_add_chaining() {
let mut fstab = Fstab::new();
let e1 = Entry::new(
Spec::Device("/dev/sda1".into()),
MountPoint::new("/").unwrap(),
FsType::new("ext4").unwrap(),
Options::defaults(),
);
let e2 = Entry::new(
Spec::Device("/dev/sda2".into()),
MountPoint::new("/home").unwrap(),
FsType::new("ext4").unwrap(),
Options::defaults(),
);
fstab.add(e1).add(e2);
assert_eq!(fstab.len(), 2);
}
#[test]
fn fstab_insert_valid() {
let mut fstab = Fstab::new();
let e1 = Entry::new(
Spec::Device("/dev/sda1".into()),
MountPoint::new("/").unwrap(),
FsType::new("ext4").unwrap(),
Options::defaults(),
);
let e2 = Entry::new(
Spec::Device("/dev/sda2".into()),
MountPoint::new("/home").unwrap(),
FsType::new("ext4").unwrap(),
Options::defaults(),
);
fstab.add(e1);
let e3 = Entry::new(
Spec::Device("/dev/sdb1".into()),
MountPoint::new("/mnt/data").unwrap(),
FsType::new("xfs").unwrap(),
Options::defaults(),
);
assert!(fstab.insert(1, e3).is_ok());
assert_eq!(fstab.len(), 2);
assert!(fstab.insert(0, e2).is_ok());
assert_eq!(fstab.len(), 3);
}
#[test]
fn fstab_insert_out_of_bounds() {
let mut fstab = Fstab::new();
let entry = Entry::new(
Spec::Device("/dev/sda1".into()),
MountPoint::new("/").unwrap(),
FsType::new("ext4").unwrap(),
Options::defaults(),
);
let result = fstab.insert(1, entry);
assert!(result.is_err());
}
#[test]
fn fstab_remove() {
let mut fstab = Fstab::new();
let entry = Entry::new(
Spec::Device("/dev/sda1".into()),
MountPoint::new("/").unwrap(),
FsType::new("ext4").unwrap(),
Options::defaults(),
);
fstab.add(entry);
let removed = fstab.remove(0);
assert!(removed.is_some());
assert!(fstab.is_empty());
let none = fstab.remove(0);
assert!(none.is_none());
}
#[test]
fn fstab_replace() {
let mut fstab = Fstab::new();
let e1 = Entry::new(
Spec::Device("/dev/sda1".into()),
MountPoint::new("/").unwrap(),
FsType::new("ext4").unwrap(),
Options::defaults(),
);
let e2 = Entry::new(
Spec::Device("/dev/sdb1".into()),
MountPoint::new("/mnt/data").unwrap(),
FsType::new("xfs").unwrap(),
Options::defaults(),
);
fstab.add(e1);
let old = fstab.replace(0, e2);
assert!(old.is_some());
assert_eq!(fstab.len(), 1);
let none = fstab.replace(5, old.unwrap());
assert!(none.is_none());
}
#[test]
fn fstab_clear() {
let mut fstab = Fstab::new();
fstab.intro_comment = Some("# intro".into());
fstab.add(Entry::new(
Spec::Device("/dev/sda1".into()),
MountPoint::new("/").unwrap(),
FsType::new("ext4").unwrap(),
Options::defaults(),
));
fstab.trailing_comment = Some("# trailing".into());
fstab.clear();
assert!(fstab.is_empty());
assert!(fstab.intro_comment.is_none());
assert!(fstab.trailing_comment.is_none());
}
#[test]
fn fstab_find_by_source() {
let mut fstab = Fstab::new();
fstab.add(Entry::new(
Spec::Device("/dev/sda1".into()),
MountPoint::new("/").unwrap(),
FsType::new("ext4").unwrap(),
Options::defaults(),
));
fstab.add(Entry::new(
Spec::Uuid("abc-123".into()),
MountPoint::new("/home").unwrap(),
FsType::new("ext4").unwrap(),
Options::defaults(),
));
let results = fstab.find_by_source("sda1");
assert_eq!(results.len(), 1);
let results = fstab.find_by_source("ext4");
assert_eq!(results.len(), 0);
}
#[test]
fn fstab_find_by_source_label() {
let mut fstab = Fstab::new();
fstab.add(Entry::new(
Spec::Label("ROOT".into()),
MountPoint::new("/").unwrap(),
FsType::new("ext4").unwrap(),
Options::defaults(),
));
let results = fstab.find_by_source("LABEL=ROOT");
assert_eq!(results.len(), 1);
}
#[test]
fn fstab_find_by_mountpoint() {
let mut fstab = Fstab::new();
fstab.add(Entry::new(
Spec::Device("/dev/sda1".into()),
MountPoint::new("/").unwrap(),
FsType::new("ext4").unwrap(),
Options::defaults(),
));
fstab.add(Entry::new(
Spec::Device("/dev/sda2".into()),
MountPoint::new("/home").unwrap(),
FsType::new("ext4").unwrap(),
Options::defaults(),
));
let found = fstab.find_by_mountpoint(Path::new("/home"));
assert!(found.is_some());
assert!(
fstab
.find_by_mountpoint(Path::new("/nonexistent"))
.is_none()
);
}
#[test]
fn fstab_root() {
let mut fstab = Fstab::new();
let root_entry = Entry {
spec: Spec::Device("/dev/sda1".into()),
file: MountPoint::new("/").unwrap(),
vfstype: FsType::new("ext4").unwrap(),
options: Options::defaults(),
freq: 0,
passno: 1,
comment: None,
};
fstab.add(Entry::new(
Spec::Device("/dev/sda2".into()),
MountPoint::new("/home").unwrap(),
FsType::new("ext4").unwrap(),
Options::defaults(),
));
fstab.add(root_entry);
let root = fstab.root();
assert!(root.is_some());
assert!(root.unwrap().is_root());
}
#[test]
fn fstab_root_no_root_entry() {
let mut fstab = Fstab::new();
fstab.add(Entry::new(
Spec::Device("/dev/sda1".into()),
MountPoint::new("/home").unwrap(),
FsType::new("ext4").unwrap(),
Options::defaults(),
));
assert!(fstab.root().is_none());
}
#[test]
fn fstab_entries_slice() {
let mut fstab = Fstab::new();
fstab.add(Entry::new(
Spec::Device("/dev/sda1".into()),
MountPoint::new("/").unwrap(),
FsType::new("ext4").unwrap(),
Options::defaults(),
));
assert_eq!(fstab.entries().len(), 1);
}
#[test]
fn fstab_into_iter() {
let mut fstab = Fstab::new();
fstab.add(Entry::new(
Spec::Device("/dev/sda1".into()),
MountPoint::new("/").unwrap(),
FsType::new("ext4").unwrap(),
Options::defaults(),
));
fstab.add(Entry::new(
Spec::Device("/dev/sda2".into()),
MountPoint::new("/home").unwrap(),
FsType::new("ext4").unwrap(),
Options::defaults(),
));
let count = fstab.into_iter().count();
assert_eq!(count, 2);
}
#[test]
fn fstab_ref_into_iter() {
let mut fstab = Fstab::new();
fstab.add(Entry::new(
Spec::Device("/dev/sda1".into()),
MountPoint::new("/").unwrap(),
FsType::new("ext4").unwrap(),
Options::defaults(),
));
let entries: Vec<&Entry> = (&fstab).into_iter().collect();
assert_eq!(entries.len(), 1);
}
#[test]
fn spec_display_device() {
let spec = Spec::Device("/dev/sda1".into());
assert_eq!(spec.to_string(), "/dev/sda1");
}
#[test]
fn spec_display_label() {
let spec = Spec::Label("ROOT".into());
assert_eq!(spec.to_string(), "LABEL=ROOT");
}
#[test]
fn spec_display_uuid() {
let spec = Spec::Uuid("abc-123".into());
assert_eq!(spec.to_string(), "UUID=abc-123");
}
#[test]
fn spec_display_partlabel() {
let spec = Spec::PartLabel("System".into());
assert_eq!(spec.to_string(), "PARTLABEL=System");
}
#[test]
fn spec_display_partuuid() {
let spec = Spec::PartUuid("abc-def".into());
assert_eq!(spec.to_string(), "PARTUUID=abc-def");
}
#[test]
fn spec_display_id() {
#[allow(deprecated)]
let spec = Spec::Id("wwn-0x50014ee2".into());
assert_eq!(spec.to_string(), "ID=wwn-0x50014ee2");
}
#[test]
fn spec_display_network() {
let spec = Spec::NetworkMount {
host: "server".into(),
path: "/export".into(),
};
assert_eq!(spec.to_string(), "server:/export");
}
#[test]
fn spec_display_keyword() {
let spec = Spec::Keyword("proc".into());
assert_eq!(spec.to_string(), "proc");
}
}