rsmount 0.2.2

Safe Rust wrapper around the `util-linux/libmount` C library
Documentation
// Copyright (c) 2023 Nick Piaddo
// SPDX-License-Identifier: Apache-2.0 OR MIT

// From dependency library

// From standard library
use std::fmt;
use std::str::FromStr;

// From this library
use crate::core::device::BlockDevice;
use crate::core::device::MountPoint;
use crate::core::device::Pseudo;
use crate::core::device::SmbFs;
use crate::core::device::SshFs;
use crate::core::device::Tag;
use crate::core::device::NFS;
use crate::core::errors::ParserError;

/// Source of a device to mount.
///
/// A source can take any of the following forms:
/// - a block device path (e.g. `/dev/sda1`),
/// - a network ID:
///     - Samba: `smb://ip-address-or-hostname/shared-dir`,
///     - NFS: `hostname:/shared-dir`  (e.g. knuth.cwi.nl:/dir)
///     - SSHFS: `[user@]ip-address-or-hostname:[/shared-dir]` elements in brackets are optional (e.g.
///       tux@192.168.0.1:/share)
/// - a tag:
///     - `UUID=uuid` (file system UUID),
///     - `LABEL=label` (human readable file system identifier),
///     - `PARTLABEL=label` (human readable partition identifier),
///     - `PARTUUID=uuid` (partition UUID),
///     - `ID=id` (hardware block device ID as generated by `udevd`).
/// - `none` for pseudo-filesystems.
///
/// (For more information, see the subsection titled [Indicating the device and
/// filesystem](https://www.man7.org/linux/man-pages/man8/mount.8.html) of the `mount`
/// syscall)
///
/// # Examples
///
/// ```
/// # use pretty_assertions::assert_eq;
/// use rsmount::device::BlockDevice;
/// use rsmount::device::MountPoint;
/// use rsmount::device::NFS;
/// use rsmount::device::Pseudo;
/// use rsmount::device::SmbFs;
/// use rsmount::device::SshFs;
/// use rsmount::device::Tag;
/// use rsmount::device::Source;
///
/// fn main() -> rsmount::Result<()> {
///    let samba_share: SmbFs = "smb://samba.server.internal/shared".parse()?;
///     let source = Source::from(samba_share);
///    assert!(source.is_samba_share());
///
///    let sshfs_share: SshFs = "tux@sshfs.server.internal:/shared".parse()?;
///     let source = Source::from(sshfs_share);
///    assert!(source.is_sshfs_share());
///
///    let block_device: BlockDevice = "/dev/vda".parse()?;
///     let source = Source::from(block_device);
///    assert!(source.is_block_device());
///
///    let mount_point: MountPoint = "/boot".parse()?;
///     let source = Source::from(mount_point);
///    assert!(source.is_mount_point());
///
///    let tag: Tag = "UUID=dd476616-1ce4-415e-9dbd-8c2fa8f42f0f".parse()?;
///     let source = Source::from(tag);
///    assert!(source.is_tag());
///    assert!(source.is_tag_uuid());
///
///    let none: Pseudo = "none".parse()?;
///     let source = Source::from(none);
///    assert!(source.is_pseudo_fs());
///
///    Ok(())
/// }
/// ```
#[derive(Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum Source {
    BlockDevice(BlockDevice),
    MountPoint(MountPoint),
    NFS(NFS),
    SmbFs(SmbFs),
    SshFs(SshFs),
    Tag(Tag),
    PseudoFs(Pseudo),
}

impl Source {
    /// Returns `true` if this `Source` is a block device.
    pub fn is_block_device(&self) -> bool {
        matches!(self, Self::BlockDevice(_))
    }

    /// Returns `true` if this `Source` is a mount point.
    pub fn is_mount_point(&self) -> bool {
        matches!(self, Self::MountPoint(_))
    }

    /// Returns `true` if this `Source` is an NFS share address.
    pub fn is_nfs_share(&self) -> bool {
        matches!(self, Self::NFS(_))
    }

    /// Returns `true` if this `Source` is a SmbFs share address.
    pub fn is_samba_share(&self) -> bool {
        matches!(self, Self::SmbFs(_))
    }

    /// Returns `true` if this `Source` is an SSHFS address.
    pub fn is_sshfs_share(&self) -> bool {
        matches!(self, Self::SshFs(_))
    }

    /// Returns `true` if this `Source` is a tag (e.g `UUID=uuid`, `LABEL=label`,
    /// `PARTUUID=uuid`, etc.).
    pub fn is_tag(&self) -> bool {
        matches!(self, Self::Tag(_))
    }

    /// Returns `true` if this `Source` is a pseudo-filesystem.
    pub fn is_pseudo_fs(&self) -> bool {
        matches!(self, Self::PseudoFs(_))
    }

    /// Returns `true` if this `Source` is a `LABEL=label` tag.
    pub fn is_tag_label(&self) -> bool {
        matches!(self, Self::Tag(t) if t.is_label())
    }

    /// Returns `true` if this `Source` is a `PARTLABEL=label` tag.
    pub fn is_tag_partition_label(&self) -> bool {
        matches!(self, Self::Tag(t) if t.is_partition_label())
    }

    /// Returns `true` if this `Source` is a `UUID=uuid` tag.
    pub fn is_tag_uuid(&self) -> bool {
        matches!(self, Self::Tag(t) if t.is_uuid())
    }

    /// Returns `true` if this `Source` is a `PARTUUID=uuid` tag.
    pub fn is_tag_partition_uuid(&self) -> bool {
        matches!(self, Self::Tag(t) if t.is_partition_uuid())
    }

    /// Returns `true` if this `Source` is an `ID=id` tag.
    pub fn is_tag_id(&self) -> bool {
        matches!(self, Self::Tag(t) if t.is_id())
    }
}

impl AsRef<Source> for Source {
    #[inline]
    fn as_ref(&self) -> &Source {
        self
    }
}

impl TryFrom<&str> for Source {
    type Error = ParserError;

    fn try_from(s: &str) -> Result<Self, Self::Error> {
        // Parse string into matching type...
        Tag::from_str(s)
            .map(Self::from)
            .or_else(|_| Pseudo::from_str(s).map(Self::from))
            .or_else(|_| SmbFs::from_str(s).map(Self::from))
            .or_else(|_| SshFs::from_str(s).map(Self::from))
            .or_else(|_| NFS::from_str(s).map(Self::from))
            .or_else(|_| MountPoint::from_str(s).map(Self::from))
            // ...if all else fails, assume `s` is a block device.
            .or_else(|_| BlockDevice::from_str(s).map(Self::from))
    }
}

impl TryFrom<String> for Source {
    type Error = ParserError;

    #[inline]
    fn try_from(s: String) -> Result<Self, Self::Error> {
        Self::try_from(s.as_str())
    }
}

impl TryFrom<&String> for Source {
    type Error = ParserError;

    #[inline]
    fn try_from(s: &String) -> Result<Self, Self::Error> {
        Self::try_from(s.as_str())
    }
}

impl FromStr for Source {
    type Err = ParserError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Self::try_from(s)
    }
}

impl fmt::Display for Source {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let output = match self {
            Self::BlockDevice(device) => device.to_string(),
            Self::MountPoint(mount_point) => mount_point.to_string(),
            Self::NFS(share) => share.to_string(),
            Self::SmbFs(share) => share.to_string(),
            Self::SshFs(share) => share.to_string(),
            Self::Tag(tag) => tag.to_string(),
            Self::PseudoFs(fs) => fs.to_string(),
        };

        write!(f, "{}", output)
    }
}

impl From<BlockDevice> for Source {
    #[inline]
    fn from(device: BlockDevice) -> Source {
        Source::BlockDevice(device)
    }
}

impl From<MountPoint> for Source {
    #[inline]
    fn from(mount_point: MountPoint) -> Source {
        Source::MountPoint(mount_point)
    }
}

impl From<NFS> for Source {
    #[inline]
    fn from(share: NFS) -> Source {
        Source::NFS(share)
    }
}

impl From<SmbFs> for Source {
    #[inline]
    fn from(share: SmbFs) -> Source {
        Source::SmbFs(share)
    }
}

impl From<SshFs> for Source {
    #[inline]
    fn from(share: SshFs) -> Source {
        Source::SshFs(share)
    }
}

impl From<Tag> for Source {
    #[inline]
    fn from(share: Tag) -> Source {
        Source::Tag(share)
    }
}

impl From<Pseudo> for Source {
    #[inline]
    fn from(fs: Pseudo) -> Source {
        Source::PseudoFs(fs)
    }
}

#[cfg(test)]
#[allow(unused_imports)]
mod tests {
    use super::*;
    use pretty_assertions::{assert_eq, assert_ne};

    #[test]
    #[should_panic(expected = "expected a device path instead of")]
    fn source_does_not_parse_an_empty_string_as_a_block_device() {
        let source = "";
        let _ = Source::try_from(source).unwrap();
    }

    #[test]
    fn source_parses_a_block_device() -> crate::Result<()> {
        let source = "/dev/vda";
        let actual: Source = source.parse()?;

        assert!(actual.is_block_device());

        Ok(())
    }

    #[test]
    fn source_parses_a_mount_point() -> crate::Result<()> {
        let source = "/boot";
        let actual: Source = source.parse()?;

        assert!(actual.is_mount_point());

        Ok(())
    }

    #[test]
    fn source_parses_a_nfs_share_address_as_an_sshfs_share() -> crate::Result<()> {
        let source = "localhost:/share";
        let actual: Source = source.parse()?;

        assert!(actual.is_sshfs_share());

        Ok(())
    }

    #[test]
    fn source_parses_a_samba_share_address() -> crate::Result<()> {
        let source = "smb://localhost/share";
        let actual: Source = source.parse()?;

        assert!(actual.is_samba_share());

        Ok(())
    }

    #[test]
    fn source_parses_a_sshfs_share_address() -> crate::Result<()> {
        let source = "user@localhost:/share";
        let actual: Source = source.parse()?;

        assert!(actual.is_sshfs_share());

        Ok(())
    }

    #[test]
    fn source_parses_a_uuid_tag() -> crate::Result<()> {
        let source = "UUID=dd476616-1ce4-415e-9dbd-8c2fa8f42f0f";
        let actual: Source = source.parse()?;

        assert!(actual.is_tag_uuid());

        Ok(())
    }

    #[test]
    fn source_parses_a_pseudo_fs() -> crate::Result<()> {
        let source = "none";
        let actual: Source = source.parse()?;

        assert!(actual.is_pseudo_fs());

        Ok(())
    }
}