mount-fstab 0.1.1

Type-safe /etc/fstab parsing, editing, and validation library
Documentation
//! Filesystem type — fstab(5) field 3 (`fs_vfstype`).
//!
//! Supports simple types (`ext4`), subtypes (`fuse.sshfs`),
//! and comma-separated type lists (`nfs,ext4`).

use crate::error::FsTypeError;
use std::fmt;

/// Filesystem type — fstab(5) field 3.
///
/// Supports simple types (`ext4`), subtypes (`fuse.sshfs`),
/// and type lists (`nfs,ext4`).
///
/// # Examples
///
/// ```
/// # use mount_fstab::fstype::FsType;
/// let ft = FsType::new("ext4").unwrap();
/// assert_eq!(ft.as_str(), "ext4");
/// assert!(!ft.is_swap());
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct FsType(String);

impl FsType {
    /// Create a new `FsType`, validating it is non-empty.
    ///
    /// # Errors
    ///
    /// Returns [`FsTypeError::Empty`] if the input is empty.
    pub fn new(s: impl Into<String>) -> Result<Self, FsTypeError> {
        let s = s.into();
        if s.is_empty() {
            Err(FsTypeError::Empty)
        } else {
            Ok(FsType(s))
        }
    }

    /// Create an `FsType` for swap entries (`swap`).
    #[must_use]
    pub fn swap() -> Self {
        FsType("swap".to_owned())
    }

    /// Create an `FsType` for bind mounts (`none`).
    #[must_use]
    pub fn bind() -> Self {
        FsType("none".to_owned())
    }

    /// Parse a raw fstab field value into an `FsType`.
    ///
    /// This is an alias for [`new`](Self::new) since no special parsing is needed.
    ///
    /// # Examples
    ///
    /// ```
    /// # use mount_fstab::FsType;
    /// // Simple type
    /// let ft = FsType::parse("ext4").unwrap();
    /// assert_eq!(ft.as_str(), "ext4");
    ///
    /// // Subtype
    /// let ft = FsType::parse("fuse.sshfs").unwrap();
    /// assert_eq!(ft.primary(), "fuse");
    ///
    /// // Type list
    /// let ft = FsType::parse("nfs,ext4").unwrap();
    /// let types: Vec<&str> = ft.iter().collect();
    /// assert_eq!(types, vec!["nfs", "ext4"]);
    /// ```
    pub fn parse(raw: &str) -> Result<Self, FsTypeError> {
        Self::new(raw)
    }

    /// The full type string.
    #[must_use]
    pub fn as_str(&self) -> &str {
        &self.0
    }

    /// Whether this represents a swap entry.
    #[must_use]
    pub fn is_swap(&self) -> bool {
        self.0 == "swap"
    }

    /// Whether this represents a bind mount (`none`).
    #[must_use]
    pub fn is_bind(&self) -> bool {
        self.0 == "none"
    }

    /// Whether this is a network filesystem type.
    ///
    /// Matches common network filesystem type names including NFS, CIFS, SMB,
    /// GlusterFS, Ceph, Lustre, and AFS.
    #[must_use]
    pub fn is_network(&self) -> bool {
        let primary = self.primary();
        matches!(
            primary,
            "nfs"
                | "nfs4"
                | "cifs"
                | "smb"
                | "smbfs"
                | "ncpfs"
                | "glusterfs"
                | "ceph"
                | "lustre"
                | "afs"
        )
    }

    /// Whether this is a pseudo filesystem (no backing block device).
    ///
    /// Matches pseudo filesystems such as `proc`, `sysfs`, `tmpfs`, `devpts`,
    /// `cgroup`, and others.
    #[must_use]
    pub fn is_pseudo(&self) -> bool {
        let primary = self.primary();
        matches!(
            primary,
            "proc"
                | "sysfs"
                | "tmpfs"
                | "devpts"
                | "devtmpfs"
                | "cgroup"
                | "cgroup2"
                | "configfs"
                | "debugfs"
                | "tracefs"
                | "securityfs"
                | "pstore"
                | "efivarfs"
                | "bpf"
                | "fusectl"
                | "mqueue"
                | "hugetlbfs"
                | "autofs"
                | "binfmt_misc"
                | "ramfs"
                | "pipefs"
                | "sockfs"
                | "bdev"
        )
    }

    /// The primary (first) type in a type list or subtype.
    ///
    /// For `fuse.sshfs` returns `"fuse"`; for `nfs,ext4` returns `"nfs"`.
    #[must_use]
    pub fn primary(&self) -> &str {
        self.0
            .split(',')
            .next()
            .and_then(|s| s.split('.').next())
            .unwrap_or(&self.0)
    }

    /// Iterate over all types in a comma-separated list.
    pub fn iter(&self) -> impl Iterator<Item = &str> {
        self.0.split(',')
    }
}

impl fmt::Display for FsType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.0)
    }
}

impl std::str::FromStr for FsType {
    type Err = FsTypeError;

    /// Parse a string into an `FsType`.
    ///
    /// # Errors
    ///
    /// Returns [`FsTypeError::Empty`] if the input is empty.
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Self::parse(s)
    }
}

impl std::ops::Deref for FsType {
    type Target = str;

    /// Dereference to the underlying string slice.
    ///
    /// This allows `&FsType` to be used wherever `&str` is expected.
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl From<String> for FsType {
    fn from(s: String) -> Self {
        FsType(s)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_simple() {
        let ft = FsType::parse("ext4").unwrap();
        assert_eq!(ft.as_str(), "ext4");
        assert!(!ft.is_swap());
        assert!(!ft.is_bind());
    }

    #[test]
    fn parse_swap() {
        let ft = FsType::parse("swap").unwrap();
        assert!(ft.is_swap());
    }

    #[test]
    fn parse_none_for_bind() {
        let ft = FsType::parse("none").unwrap();
        assert!(ft.is_bind());
    }

    #[test]
    fn parse_with_subtype() {
        let ft = FsType::parse("fuse.sshfs").unwrap();
        assert_eq!(ft.primary(), "fuse");
        assert_eq!(ft.as_str(), "fuse.sshfs");
    }

    #[test]
    fn parse_type_list() {
        let ft = FsType::parse("nfs,ext4").unwrap();
        assert_eq!(ft.primary(), "nfs");
        let types: Vec<&str> = ft.iter().collect();
        assert_eq!(types, vec!["nfs", "ext4"]);
    }

    #[test]
    fn is_network() {
        assert!(FsType::parse("nfs").unwrap().is_network());
        assert!(FsType::parse("nfs4").unwrap().is_network());
        assert!(FsType::parse("cifs").unwrap().is_network());
        assert!(FsType::parse("smb").unwrap().is_network());
        assert!(!FsType::parse("ext4").unwrap().is_network());
        assert!(!FsType::parse("xfs").unwrap().is_network());
    }

    #[test]
    fn is_pseudo() {
        assert!(FsType::parse("proc").unwrap().is_pseudo());
        assert!(FsType::parse("sysfs").unwrap().is_pseudo());
        assert!(FsType::parse("tmpfs").unwrap().is_pseudo());
        assert!(FsType::parse("devpts").unwrap().is_pseudo());
        assert!(FsType::parse("cgroup2").unwrap().is_pseudo());
        assert!(!FsType::parse("ext4").unwrap().is_pseudo());
    }

    #[test]
    fn parse_empty_is_error() {
        assert!(FsType::parse("").is_err());
    }

    #[test]
    fn swap_constructor() {
        assert_eq!(FsType::swap().as_str(), "swap");
    }

    #[test]
    fn bind_constructor() {
        assert_eq!(FsType::bind().as_str(), "none");
    }

    #[test]
    fn parse_auto_type() {
        let ft = FsType::parse("auto").unwrap();
        assert_eq!(ft.as_str(), "auto");
    }

    #[test]
    fn parse_ignore_is_not_swap() {
        let ft = FsType::parse("ignore").unwrap();
        assert!(!ft.is_swap());
        assert!(!ft.is_bind());
    }

    #[test]
    fn from_str_works() {
        use std::str::FromStr;
        let ft = FsType::from_str("ext4").unwrap();
        assert_eq!(ft.as_str(), "ext4");
    }

    #[test]
    fn deref_works() {
        let ft = FsType::new("ext4").unwrap();
        assert_eq!(&*ft, "ext4");
        assert_eq!(ft.len(), 4); // str::len via Deref
    }

    #[test]
    fn display() {
        assert_eq!(FsType::parse("ext4").unwrap().to_string(), "ext4");
        assert_eq!(
            FsType::parse("fuse.sshfs").unwrap().to_string(),
            "fuse.sshfs"
        );
    }
}