From 947ff86015cb5493464750b9ac61aa9d085861a2 Mon Sep 17 00:00:00 2001 From: rowan Date: Tue, 4 Mar 2025 22:45:56 -0600 Subject: [PATCH] wip overlay refactor --- src/fs/mod.rs | 6 +- src/fs/mount.rs | 323 ++++++++++++++----------------- src/fs/stackable/fuse_overlay.rs | 226 ++++----------------- src/fs/stackable/mod.rs | 110 ++++++++++- src/fs/stackable/overlay.rs | 287 +++++++++++++++++++++------ src/lib.rs | 19 +- src/macros.rs | 58 ++++++ src/main.rs | 8 + src/utils.rs | 119 ++++++++++++ 9 files changed, 708 insertions(+), 448 deletions(-) create mode 100644 src/main.rs diff --git a/src/fs/mod.rs b/src/fs/mod.rs index 68d5dba..6ced050 100644 --- a/src/fs/mod.rs +++ b/src/fs/mod.rs @@ -1,6 +1,6 @@ -mod id_mapping; -mod mount; -mod permission; +pub(crate) mod id_mapping; +pub(crate) mod mount; +pub mod permission; pub mod stackable; use std::{fmt::Display, slice::Iter}; diff --git a/src/fs/mount.rs b/src/fs/mount.rs index 9bae8dc..b4f2a46 100644 --- a/src/fs/mount.rs +++ b/src/fs/mount.rs @@ -2,6 +2,7 @@ use itertools::Itertools; use std::{ convert::Infallible, error::Error, + ffi::{OsStr, OsString}, fmt::Display, ops::{Deref, DerefMut}, path::PathBuf, @@ -9,9 +10,12 @@ use std::{ str::FromStr, }; +use crate::utils::DisplayString; + use super::{ id_mapping::{ParseUntypedIdRangeError, UntypedIdRange}, permission::Permissions, + FileSystem, }; #[derive(Debug, Clone, PartialEq, Eq)] @@ -27,24 +31,24 @@ impl Error for ParseKeyValuePairError {} #[derive(Clone, Debug, PartialEq, Eq)] pub struct KeyValuePair { - name: String, - value: Option, + name: DisplayString, + value: Option, } impl KeyValuePair { - pub fn new(name: impl Into, value: Option>) -> Self { + pub fn new(name: impl Into, value: Option>) -> Self { Self { name: name.into(), value: value.map(Into::into), } } - pub fn into_tuple(self) -> (String, Option) { + pub fn into_tuple(self) -> (DisplayString, Option) { (self.name, self.value) } } -impl From for (String, Option) { +impl From for (DisplayString, Option) { fn from(kvp: KeyValuePair) -> Self { kvp.into_tuple() } @@ -70,58 +74,6 @@ impl Display for KeyValuePair { } } -#[derive(Debug)] -pub struct MountOptions(pub Vec); - -impl Clone for MountOptions { - fn clone(&self) -> Self { - Self(self.0.clone()) - } -} - -impl Default for MountOptions { - fn default() -> Self { - Self(Vec::new()) - } -} - -impl Deref for MountOptions { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for MountOptions { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl FromIterator for MountOptions { - fn from_iter>(iter: I) -> Self { - Self(iter.into_iter().collect()) - } -} - -impl FromStr for MountOptions { - type Err = T::Err; - - fn from_str(s: &str) -> Result { - s.split(',') - .map(T::from_str) - .process_results(|it| MountOptions(it.collect())) - } -} - -impl Display for MountOptions { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let options = self.0.iter().map(|x| x.to_string()).join(" "); - write!(f, "{options}") - } -} - #[derive(Debug, PartialEq, Eq)] pub struct ParseAccessError; @@ -178,12 +130,12 @@ impl Error for ParseDeviceIdTypeError {} #[derive(Clone, Debug, PartialEq, Eq)] pub enum DeviceId { - Label(String), - Uuid(String), - PartLabel(String), - PartUuid(String), - Id(String), - Other(String), + Label(DisplayString), + Uuid(DisplayString), + PartLabel(DisplayString), + PartUuid(DisplayString), + Id(DisplayString), + Other(DisplayString), } impl FromStr for DeviceId { @@ -192,14 +144,14 @@ impl FromStr for DeviceId { fn from_str(s: &str) -> Result { match s.split_once('=') { Some((key, value)) => match key { - "LABEL" => Ok(Self::Label(value.to_string())), - "UUID" => Ok(Self::Uuid(value.to_string())), - "PARTLABEL" => Ok(Self::PartLabel(value.to_string())), - "PARTUUID" => Ok(Self::PartUuid(value.to_string())), - "ID" => Ok(Self::Id(value.to_string())), + "LABEL" => Ok(Self::Label(value.into())), + "UUID" => Ok(Self::Uuid(value.into())), + "PARTLABEL" => Ok(Self::PartLabel(value.into())), + "PARTUUID" => Ok(Self::PartUuid(value.into())), + "ID" => Ok(Self::Id(value.into())), key => Err(ParseDeviceIdTypeError(key.to_string())), }, - None => Ok(Self::Other(s.to_string())), + None => Ok(Self::Other(s.into())), } } } @@ -223,13 +175,13 @@ impl Display for DeviceId { impl From for DeviceId { fn from(value: String) -> Self { - Self::from_str(&value).unwrap_or_else(|_v| Self::Other(value)) + Self::from_str(&value).unwrap_or_else(|_v| Self::Other(value.into())) } } impl From<&str> for DeviceId { fn from(value: &str) -> Self { - Self::from_str(value).unwrap_or_else(|_v| Self::Other(value.to_string())) + Self::from_str(value).unwrap_or_else(|_v| Self::Other(value.into())) } } @@ -425,6 +377,32 @@ impl From for GidRange { #[derive(Debug)] pub struct Options(Vec); +impl Default for Options { + fn default() -> Self { + Self(Vec::new()) + } +} + +impl AsRef> for Options { + fn as_ref(&self) -> &Vec { + &self.0 + } +} + +impl Deref for Options { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Options { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + impl Clone for Options { fn clone(&self) -> Self { Self(self.0.clone()) @@ -464,12 +442,24 @@ impl Display for Options { } } +impl FromIterator for Options { + fn from_iter>(iter: I) -> Self { + Self(iter.into_iter().collect()) + } +} + impl From> for Options { fn from(value: Vec) -> Self { Self(value) } } +impl From for Options { + fn from(value: T) -> Self { + Self(vec![value]) + } +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct Sources(Options); @@ -499,13 +489,13 @@ impl From for ParseUncheckedOptionsError { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct UncheckedOptions(Options); +pub struct UncheckedOptions(Options); impl FromStr for UncheckedOptions { type Err = ParseUncheckedOptionsError; fn from_str(s: &str) -> Result { - Ok(Self(Options::::from_str(s)?)) + Ok(Self(Options::::from_str(s)?)) } } @@ -521,36 +511,26 @@ impl From for UncheckedOptions { } } -impl From<&str> for UncheckedOptions { - fn from(value: &str) -> Self { - Self::from_str(value).unwrap() +impl> FromIterator for UncheckedOptions { + fn from_iter>(iter: I) -> Self { + Self(Options::from_iter(iter.into_iter().map(Into::into))) } } -impl From> for UncheckedOptions { - fn from(value: Vec) -> Self { - Self(Options::from(value)) - } -} - -impl From> for UncheckedOptions { - fn from(value: Vec<&str>) -> Self { - value - .into_iter() - .map(String::from) - .collect::>() - .into() - } -} - -impl From for Options { +impl From for Options { fn from(value: UncheckedOptions) -> Self { value.0 } } -impl From> for UncheckedOptions { - fn from(value: Options) -> Self { +impl AsRef> for UncheckedOptions { + fn as_ref(&self) -> &Options { + &self.0 + } +} + +impl From> for UncheckedOptions { + fn from(value: Options) -> Self { Self(value) } } @@ -564,7 +544,7 @@ pub enum ParseMountOptionError { OptionsSource(ParseOptionsSourceError), Operation(ParseMountOperationError), Subtree(ParseSubtreeError), - UnknownOption(String), + UnknownOption(DisplayString), } impl Display for ParseMountOptionError { @@ -640,7 +620,7 @@ pub enum MountOption { NoCanonicalize, Fake, Fork, - Fstab(String), + Fstab(OsString), InternalOnly, ShowLabels, MapGroups(GidRange), @@ -658,7 +638,7 @@ pub enum MountOption { TargetPrefix(PathBuf), Verbose, Access(Access), - Namespace(String), + Namespace(OsString), Operation(MountOperation), Subtree(Subtree), } @@ -713,7 +693,7 @@ impl FromStr for MountOption { (op, None) if op.starts_with("make-") => Ok(Self::Subtree(Subtree::from_str( op.strip_prefix("make-").unwrap(), )?)), - (opt, _) => Err(ParseMountOptionError::UnknownOption(opt.to_string())), + (opt, _) => Err(ParseMountOptionError::UnknownOption(opt.into())), } } } @@ -730,7 +710,7 @@ impl Display for MountOption { Self::NoCanonicalize => "--no-canonicalize".to_string(), Self::Fake => "--fake".to_string(), Self::Fork => "--fork".to_string(), - Self::Fstab(path) => format!("--fstab {path}"), + Self::Fstab(path) => format!("--fstab {}", path.to_string_lossy()), Self::InternalOnly => "--internal-only".to_string(), Self::ShowLabels => "--show-labels".to_string(), Self::MapGroups(range) => format!("--map-groups {range}"), @@ -753,7 +733,7 @@ impl Display for MountOption { Self::TargetPrefix(path) => format!("--target-prefix {}", path.to_string_lossy()), Self::Verbose => "--verbose".to_string(), Self::Access(access) => format!("--{access}"), - Self::Namespace(ns) => format!("--namespace {ns}"), + Self::Namespace(ns) => format!("--namespace {}", ns.to_string_lossy()), Self::Operation(op) => format!("--{op}"), Self::Subtree(op) => format!("--make-{op}"), } @@ -762,7 +742,7 @@ impl Display for MountOption { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct ParseSubtreeError(String); +pub struct ParseSubtreeError(DisplayString); impl Display for ParseSubtreeError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -802,29 +782,25 @@ impl FromStr for Subtree { "rprivate" => Ok(Self::recursive(Self::Private)), "runbindable" => Ok(Self::recursive(Self::Unbindable)), - _ => Err(ParseSubtreeError(s.to_string())), + _ => Err(ParseSubtreeError(s.into())), } } } impl Display for Subtree { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - match self { - Subtree::Shared => "shared".to_string(), - Subtree::Replica => "slave".to_string(), - Subtree::Private => "private".to_string(), - Subtree::Unbindable => "unbindable".to_string(), - Subtree::Recursive(op) => format!("r{op}"), - }, - ) + match self { + Subtree::Shared => write!(f, "shared"), + Subtree::Replica => write!(f, "slave"), + Subtree::Private => write!(f, "private"), + Subtree::Unbindable => write!(f, "unbindable"), + Subtree::Recursive(op) => write!(f, "r{op}"), + } } } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct ParseMountOperationError(String); +pub struct ParseMountOperationError(DisplayString); impl Display for ParseMountOperationError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -849,7 +825,7 @@ impl FromStr for MountOperation { "bind" => Ok(Self::Bind), "rbind" => Ok(Self::RecursiveBind), "move" => Ok(Self::Move), - v => Err(ParseMountOperationError(v.to_string())), + v => Err(ParseMountOperationError(v.into())), } } } @@ -868,45 +844,57 @@ impl Display for MountOperation { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Mount { - source: DeviceId, - target: PathBuf, - options: MountOptions, + device_id: DeviceId, + mountpoint: PathBuf, + options: Options, } impl Mount { - pub fn new( - src: impl Into, - target: impl Into, - options: impl Into>, - ) -> Self { + pub fn new(source: impl Into, target: impl Into, options: I) -> Self + where + T: Into, + I: IntoIterator, + { Self { - source: src.into(), - target: target.into(), - options: options.into(), + device_id: source.into(), + mountpoint: target.into(), + options: Options::from_iter(options.into_iter().map(Into::into)), } } - - pub fn mount(&self) -> std::io::Result { - Command::new("mount") - .arg(self.options.to_string()) - .arg(self.source.to_string()) - .arg(&self.target) - .output() - } } -impl Display for Mount { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "mount {} {} {}", - self.options, - self.source, - self.target.to_string_lossy() - ) +impl FileSystem for Mount { + fn mount(&mut self) -> std::io::Result<()> { + mount( + self.device_id, + &self.mountpoint, + self.options.map(Into::into), + )?; + Ok(()) } + + fn unmount(&mut self) -> std::io::Result<()> { + unmount(&self.mountpoint)?; + Ok(()) + } +} + +pub fn mount, I: IntoIterator>( + source: impl Into, + target: impl Into, + options: I, +) -> std::io::Result { + Command::new("mount") + .args(options.into_iter().map(|s| s.as_ref().to_string())) + .arg(source.into().to_string()) + .arg(target.into()) + .output() +} + +pub fn unmount(target: impl Into) -> std::io::Result { + Command::new("umount").arg(target.into()).output() } #[cfg(test)] @@ -914,7 +902,6 @@ mod tests { use std::{borrow::Cow, path::PathBuf, str::FromStr}; use enumflags2::BitFlags; - use itertools::Itertools; use crate::fs::{ id_mapping::UntypedIdRange, @@ -923,8 +910,8 @@ mod tests { }; use super::{ - Access, DeviceId, KeyValuePair, MapUsers, Mount, MountOperation, MountOption, MountOptions, - Options, OptionsMode, OptionsSource, ParseMountOptionError, Subtree, UncheckedOptions, + Access, DeviceId, KeyValuePair, MapUsers, MountOperation, MountOption, Options, + OptionsMode, OptionsSource, ParseMountOptionError, Subtree, UncheckedOptions, }; #[test] @@ -962,12 +949,12 @@ mod tests { let other = DeviceId::from_str("awawa").unwrap(); let should_err = DeviceId::from_str("GARBAGE=trash"); - assert_eq!(label, DeviceId::Label("test".to_string())); - assert_eq!(uuid, DeviceId::Uuid("1234".to_string())); - assert_eq!(pl, DeviceId::PartLabel("test".to_string())); - assert_eq!(pu, DeviceId::PartUuid("98776543".to_string())); - assert_eq!(id, DeviceId::Id("awawa".to_string())); - assert_eq!(other, DeviceId::Other("awawa".to_string())); + assert_eq!(label, DeviceId::Label("test".into())); + assert_eq!(uuid, DeviceId::Uuid("1234".into())); + assert_eq!(pl, DeviceId::PartLabel("test".into())); + assert_eq!(pu, DeviceId::PartUuid("98776543".into())); + assert_eq!(id, DeviceId::Id("awawa".into())); + assert_eq!(other, DeviceId::Other("awawa".into())); assert_eq!( should_err, Err(ParseDeviceIdTypeError("GARBAGE".to_string())) @@ -1110,7 +1097,7 @@ mod tests { }; cmds.iter().map(|s| to_args(s)).fold( - Err(ParseMountOptionError::UnknownOption("".to_string())), + Err(ParseMountOptionError::UnknownOption("".into())), |_, c| MountOption::from_str(&c), ) } @@ -1168,8 +1155,9 @@ mod tests { Opts::from((["-L", "--label"].as_slice(), "LABEL=label")) .assert(MountOption::Label(DeviceId::Label("label".to_string()))); - Opts::from((["-t", "--types"].as_slice(), "nomsdos,smbs")) - .assert(MountOption::FsType(vec!["nomsdos", "smbs"].into())); + Opts::from((["-t", "--types"].as_slice(), "nomsdos,smbs")).assert(MountOption::FsType( + UncheckedOptions::from_iter(vec!["nomsdos", "smbs"]), + )); Opts::from("-a").assert(MountOption::All); Opts::from(["-c", "--no-canonicalize"].as_slice()).assert(MountOption::NoCanonicalize); @@ -1211,11 +1199,7 @@ mod tests { Opts::from("--options-source-force").assert(MountOption::OptionsSourceForce); Opts::from("--onlyonce").assert(MountOption::OnlyOnce); - let opts: UncheckedOptions = vec!["kbity", "dogy", "aaaaaa=awawa"] - .iter() - .map(ToString::to_string) - .collect_vec() - .into(); + let opts = UncheckedOptions::from_iter(vec!["kbity", "dogy", "aaaaaa=awawa"]); Opts::from((["-o", "--options"].as_slice(), "kbity,dogy,aaaaaa=awawa")) .assert(MountOption::Options(opts.clone())); @@ -1264,19 +1248,4 @@ mod tests { Subtree::Unbindable, ))); } - - #[test] - fn mount() { - let mut options = MountOptions::default(); - options.push(MountOption::FsType(vec!["overlay"].into())); - options.push(MountOption::MakeDir(Some(Permissions::Octal( - BitFlags::from_bits(0x7777).unwrap().into(), - )))); - - let mount = Mount::new("/test", "/target", options); - assert_eq!( - mount.to_string(), - "mount --types overlay --mkdir 7777 /test /target" - ); - } } diff --git a/src/fs/stackable/fuse_overlay.rs b/src/fs/stackable/fuse_overlay.rs index 4bfa876..a04ffcd 100644 --- a/src/fs/stackable/fuse_overlay.rs +++ b/src/fs/stackable/fuse_overlay.rs @@ -1,57 +1,15 @@ -use std::{ - convert::Infallible, - error::Error, - fmt::Display, - num::ParseIntError, - ops::Deref, - path::{Path, PathBuf}, - process::Command, - slice::Iter, - str::FromStr, -}; +use std::{error::Error, fmt::Display, num::ParseIntError, path::PathBuf, str::FromStr}; -use crate::fs::{ - id_mapping::{IdMapping as OriginalIdMapping, IdRange, ParseIdRangeError}, - mount::MountOptions, - AsIter, FileSystem, Mountpoint, -}; use crate::macros::*; +use crate::{ + fs::{ + id_mapping::{IdMapping as OriginalIdMapping, IdRange, ParseIdRangeError}, + FileSystem, + }, + utils::Command, +}; -use super::Stack; - -macro_rules! impl_deref { - ($name:ident($inner:ty)) => { - impl Deref for $name { - type Target = $inner; - - fn deref(&self) -> &Self::Target { - &self.0 - } - } - }; -} - -macro_rules! impl_fromstr { - ($name:ident($inner:ty), $err:ty) => { - impl FromStr for $name { - type Err = $err; - - fn from_str(s: &str) -> Result { - Ok(Self(<$inner>::from_str(s)?)) - } - } - }; -} - -macro_rules! impl_display { - ($name:ident) => { - impl Display for $name { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } - } - }; -} +use super::{DirIter, LowerDirs, Stack, StackFs}; macro_rules! impl_wrapper_struct { ($name:ident($inner:ty), $err:ty) => { @@ -94,99 +52,6 @@ impl_wrapper_err_for!(ParseSquashToGidError(ParseSquashToIdError), "invalid gid" impl_wrapper_err_for!(ParseUidMappingError(ParseIdRangeError), "invalid uid"); impl_wrapper_err_for!(ParseGidMappingError(ParseIdRangeError), "invalid gid"); -#[derive(Clone, Debug, Default)] -pub struct DirIter<'a> { - inner: Iter<'a, PathBuf>, -} - -impl<'a> Iterator for DirIter<'a> { - type Item = &'a Path; - - fn next(&mut self) -> Option { - self.inner.next().map(PathBuf::as_path) - } -} - -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct LowerDirs(Vec); - -impl> FromIterator for LowerDirs { - fn from_iter>(iter: T) -> Self { - Self(iter.into_iter().map(Into::into).collect()) - } -} - -impl<'a> IntoIterator for &'a LowerDirs { - type Item = &'a Path; - type IntoIter = DirIter<'a>; - - fn into_iter(self) -> Self::IntoIter { - DirIter { - inner: self.as_slice().iter(), - } - } -} - -impl Deref for LowerDirs { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Display for LowerDirs { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut iter = self.0.iter(); - if let Some(head) = iter.next() { - write!(f, "{}", head.to_string_lossy())?; - - for item in iter { - write!(f, ",{}", item.to_string_lossy())?; - } - } - - Ok(()) - } -} - -impl FromStr for LowerDirs { - type Err = Infallible; - - fn from_str(s: &str) -> Result { - Ok(Self( - s.split(',') - .filter(|s| !s.is_empty()) - .map(Into::into) - .collect(), - )) - } -} - -impl From> for LowerDirs { - fn from(value: Vec<&str>) -> Self { - Self(value.into_iter().map(PathBuf::from).collect()) - } -} - -impl From> for LowerDirs { - fn from(value: Vec) -> Self { - Self(value.into_iter().map(PathBuf::from).collect()) - } -} - -impl From> for LowerDirs { - fn from(value: Vec<&Path>) -> Self { - Self(value.into_iter().map(Path::to_path_buf).collect()) - } -} - -impl From> for LowerDirs { - fn from(value: Vec) -> Self { - Self(value) - } -} - #[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] pub struct MaxIdleThreads(Option); @@ -225,7 +90,7 @@ impl FromStr for MaxThreads { type Err = ParseMaxThreadsError; fn from_str(s: &str) -> Result { - Ok(usize::from_str_radix(s, 10).map(Self)?) + Ok(s.parse::().map(Self)?) } } @@ -384,7 +249,7 @@ impl FromStr for FuseOverlayOption { impl Display for FuseOverlayOption { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "-o ")?; + write!(f, "-o")?; match self { FuseOverlayOption::LowerDir(lower) => write!(f, "lowerdir={lower}"), @@ -407,41 +272,25 @@ impl Display for FuseOverlayOption { } #[derive(Debug)] -pub struct FuseOverlay { - mount_target: PathBuf, - options: MountOptions, -} +pub struct FuseOverlay(StackFs); + +impl_deref!(FuseOverlay(StackFs)); +impl_deref_mut!(FuseOverlay(StackFs)); impl FuseOverlay { - pub fn new(target: impl Into) -> Self { - Self { - mount_target: target.into(), - options: Default::default(), - } - } - - pub fn push_option(&mut self, option: FuseOverlayOption) { - self.options.push(option); - } - - pub fn push_options(&mut self, options: impl IntoIterator) { - self.options.extend(options) - } - - fn find_option<'a, T, F>(&'a self, f: F) -> Option - where - F: Fn(&'a FuseOverlayOption) -> Option, - { - self.options.iter().filter_map(f).take(1).next() + pub fn new( + target: impl Into, + lowerdir: impl IntoIterator>, + ) -> Self { + let mut fs = StackFs::new(target); + fs.push_option(FuseOverlayOption::LowerDir(LowerDirs::from_iter(lowerdir))); + Self(fs) } } impl Stack for FuseOverlay { fn lower_dirs(&self) -> impl Iterator { - let result = self.find_option(|opt| match opt { - FuseOverlayOption::LowerDir(dirs) => Some(dirs), - _ => None, - }); + let result = self.find_option(|v| unwrap_enum!(v, FuseOverlayOption::LowerDir)); match result { Some(dirs) => dirs.into_iter(), @@ -450,23 +299,13 @@ impl Stack for FuseOverlay { } fn upper_dir(&self) -> Option<&std::path::Path> { - self.find_option(|opt| match opt { - FuseOverlayOption::UpperDir(dir) => Some(dir.as_path()), - _ => None, - }) + self.find_option(|v| unwrap_enum!(v, FuseOverlayOption::UpperDir)) + .map(PathBuf::as_path) } fn work_dir(&self) -> Option<&std::path::Path> { - self.find_option(|opt| match opt { - FuseOverlayOption::WorkDir(dir) => Some(dir.as_path()), - _ => None, - }) - } -} - -impl Mountpoint for FuseOverlay { - fn options(&self) -> impl Iterator { - self.options.as_iter() + self.find_option(|v| unwrap_enum!(v, FuseOverlayOption::WorkDir)) + .map(PathBuf::as_path) } } @@ -474,6 +313,7 @@ impl FileSystem for FuseOverlay { fn mount(&mut self) -> std::io::Result<()> { Command::new("fuse-overlayfs") .arg(self.options.to_string()) + .arg(&self.mountpoint) .output()?; Ok(()) @@ -482,7 +322,7 @@ impl FileSystem for FuseOverlay { fn unmount(&mut self) -> std::io::Result<()> { Command::new("fusermount") .arg("-u") - .arg(&self.mount_target) + .arg(&self.mountpoint) .output()?; Ok(()) @@ -495,11 +335,11 @@ impl Display for FuseOverlay { // INFO: this is to get rid of the extraneous space if // self.options is empty - if self.options.len() > 0 { + if !self.options.is_empty() { write!(f, " {}", self.options)?; } - write!(f, " {}", self.mount_target.to_string_lossy()) + write!(f, " {}", self.mountpoint.to_string_lossy()) } } @@ -602,7 +442,7 @@ mod tests { assert_eq!(basic.to_string(), "fuse-overlayfs /mnt"); let mut with_options = FuseOverlay::new("~/.config"); - with_options.push_options([ + with_options.extend_options([ FuseOverlayOption::NoAcl, LowerDirs::from_iter(["~/.themes", "~/.colors"]).into(), ]); @@ -617,7 +457,7 @@ mod tests { fn mountpoint_trait_impl() { let mut all = FuseOverlay::new("/mnt"); - all.push_options([ + all.extend_options([ LowerDirs::from_iter(["/doesnt", "/matter"]).into(), FuseOverlayOption::UpperDir("/upper".into()), FuseOverlayOption::WorkDir("/work".into()), diff --git a/src/fs/stackable/mod.rs b/src/fs/stackable/mod.rs index 42e6417..8e435d6 100644 --- a/src/fs/stackable/mod.rs +++ b/src/fs/stackable/mod.rs @@ -1,7 +1,115 @@ pub mod fuse_overlay; pub mod overlay; -use std::path::Path; +use std::convert::Infallible; +use std::fmt::Display; +use std::path::{Path, PathBuf}; +use std::slice::Iter; +use std::str::FromStr; + +use crate::macros::impl_deref; + +use super::mount::Options; +use super::Mountpoint; + +#[derive(Clone, Debug, Default)] +pub struct DirIter<'a> { + inner: Iter<'a, PathBuf>, +} + +impl<'a> Iterator for DirIter<'a> { + type Item = &'a Path; + + fn next(&mut self) -> Option { + self.inner.next().map(PathBuf::as_path) + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct LowerDirs(Vec); + +impl_deref!(LowerDirs(Vec)); + +impl> FromIterator for LowerDirs { + fn from_iter>(iter: T) -> Self { + Self(iter.into_iter().map(Into::into).collect()) + } +} + +impl<'a> IntoIterator for &'a LowerDirs { + type Item = &'a Path; + type IntoIter = DirIter<'a>; + + fn into_iter(self) -> Self::IntoIter { + DirIter { + inner: self.as_slice().iter(), + } + } +} + +impl Display for LowerDirs { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut iter = self.0.iter(); + if let Some(head) = iter.next() { + write!(f, "{}", head.to_string_lossy())?; + + for item in iter { + write!(f, ":{}", item.to_string_lossy())?; + } + } + + Ok(()) + } +} + +impl FromStr for LowerDirs { + type Err = Infallible; + + fn from_str(s: &str) -> Result { + Ok(Self( + s.split(',') + .filter(|s| !s.is_empty()) + .map(Into::into) + .collect(), + )) + } +} + +#[derive(Debug)] +pub struct StackFs { + mountpoint: PathBuf, + options: Options, +} + +impl StackFs { + pub fn new(target: impl Into) -> Self { + Self { + mountpoint: target.into(), + options: Default::default(), + } + } + + pub fn push_option(&mut self, value: T) { + self.options.push(value); + } + + pub fn extend_options(&mut self, iter: impl IntoIterator) { + self.options.extend(iter); + } + + pub fn find_option<'a, R, F>(&'a self, f: F) -> Option + where + F: Fn(&'a T) -> Option, + { + self.options.iter().filter_map(f).take(1).next() + } +} + +impl Mountpoint for StackFs { + fn options(&self) -> impl Iterator { + self.options.iter() + } +} pub trait Stack { fn lower_dirs(&self) -> impl Iterator; diff --git a/src/fs/stackable/overlay.rs b/src/fs/stackable/overlay.rs index e980091..b0ab85b 100644 --- a/src/fs/stackable/overlay.rs +++ b/src/fs/stackable/overlay.rs @@ -1,10 +1,46 @@ +use std::ffi::OsString; use std::fmt::Display; -use std::iter; use std::path::PathBuf; +use std::str::FromStr; -use crate::fs::{AsIter, Mountpoint}; +use char_enum::FromStrError; -use super::Stack; +use crate::fs::mount::{mount, unmount, MountOption, UncheckedOptions}; +use crate::fs::FileSystem; +use crate::macros::{impl_deref, impl_deref_mut, unwrap_enum}; + +use super::{LowerDirs, Stack, StackFs}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum Switch { + On, + Off, +} + +impl FromStr for Switch { + type Err = FromStrError; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "on" => Ok(Self::On), + "off" => Ok(Self::Off), + s => Err(FromStrError(s.to_string())), + } + } +} + +impl Display for Switch { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Switch::On => "on", + Switch::Off => "off", + } + ) + } +} #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum RedirectDir { @@ -14,6 +50,35 @@ pub enum RedirectDir { Off, } +impl FromStr for RedirectDir { + type Err = FromStrError; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "on" => Ok(Self::On), + "follow" => Ok(Self::Follow), + "nofollow" => Ok(Self::NoFollow), + "off" => Ok(Self::Off), + s => Err(FromStrError(s.to_string())), + } + } +} + +impl Display for RedirectDir { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + RedirectDir::On => "on", + RedirectDir::Follow => "follow", + RedirectDir::NoFollow => "nofollow", + RedirectDir::Off => "off", + } + ) + } +} + #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum Uuid { On, @@ -22,6 +87,35 @@ pub enum Uuid { Off, } +impl FromStr for Uuid { + type Err = FromStrError; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "on" => Ok(Self::On), + "auto" => Ok(Self::Auto), + "null" => Ok(Self::Null), + "off" => Ok(Self::Off), + s => Err(FromStrError(s.to_string())), + } + } +} + +impl Display for Uuid { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::On => "on", + Self::Auto => "auto", + Self::Null => "null", + Self::Off => "off", + } + ) + } +} + #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum Verity { On, @@ -29,6 +123,33 @@ pub enum Verity { Off, } +impl FromStr for Verity { + type Err = FromStrError; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "on" => Ok(Self::On), + "require" => Ok(Self::Require), + "off" => Ok(Self::Off), + s => Err(FromStrError(s.to_string())), + } + } +} + +impl Display for Verity { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::On => "on", + Self::Require => "require", + Self::Off => "off", + } + ) + } +} + #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum Xino { On, @@ -36,89 +157,133 @@ pub enum Xino { Off, } -#[derive(Debug)] -pub enum OverlayOptions { - Index(bool), - Metacopy(bool), - NfsExport(bool), - RedirectDir(RedirectDir), - UserXAttr, - Uuid(Uuid), - Verity(Verity), - Volatile, - Xino(Xino), +impl FromStr for Xino { + type Err = FromStrError; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "on" => Ok(Self::On), + "auto" => Ok(Self::Auto), + "off" => Ok(Self::Off), + s => Err(FromStrError(s.to_string())), + } + } } -impl Display for OverlayOptions { +impl Display for Xino { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "{}", match self { - OverlayOptions::Index(enabled) => match enabled { - true => "index=on", - false => "index=off", - }, - OverlayOptions::Metacopy(enabled) => match enabled { - true => "metacopy=on", - false => "metacopy=off", - }, - OverlayOptions::NfsExport(enabled) => match enabled { - true => "nfs_export=on", - false => "nfs_export=off", - }, - OverlayOptions::RedirectDir(redirect_dir) => match redirect_dir { - RedirectDir::On => "redirect_dir=on", - RedirectDir::Follow => "redirect_dir=follow", - RedirectDir::NoFollow => "redirect_dir=nofollow", - RedirectDir::Off => "redirect_dir=off", - }, - OverlayOptions::UserXAttr => "userxattr", - OverlayOptions::Uuid(uuid) => match uuid { - Uuid::On => "uuid=on", - Uuid::Auto => "uuid=auto", - Uuid::Null => "uuid=null", - Uuid::Off => "uuid=off", - }, - OverlayOptions::Verity(verity) => match verity { - Verity::On => "verity=on", - Verity::Require => "verity=require", - Verity::Off => "verity=off", - }, - OverlayOptions::Volatile => "volatile", - OverlayOptions::Xino(xino) => match xino { - Xino::On => "xino=on", - Xino::Auto => "xino=auto", - Xino::Off => "xino=off", - }, + Self::On => "on", + Self::Auto => "Auto", + Self::Off => "off", } ) } } #[derive(Debug)] -pub struct Overlay { - mount_target: PathBuf, - options: Vec, +pub enum OverlayOption { + Index(Switch), + LowerDir(LowerDirs), + Metacopy(Switch), + NfsExport(Switch), + RedirectDir(RedirectDir), + UpperDir(PathBuf), + UserXAttr, + Uuid(Uuid), + Verity(Verity), + Volatile, + WorkDir(PathBuf), + Xino(Xino), } +impl FromStr for OverlayOption { + type Err = FromStrError; + + fn from_str(s: &str) -> Result { + let s = s.trim(); + let arg = match s.split_once('=') { + Some((k, v)) => (k, Some(v)), + None => (s, None), + }; + + match arg { + ("index", Some(enabled)) => Ok(Self::Index(Switch::from_str(enabled)?)), + ("lowerdir", Some(dirs)) => Ok(Self::LowerDir(LowerDirs::from_str(dirs).unwrap())), + ("metacopy", Some(enabled)) => Ok(Self::Metacopy(Switch::from_str(enabled)?)), + ("nfs_export", Some(enabled)) => Ok(Self::NfsExport(Switch::from_str(enabled)?)), + ("redirect_dir", Some(dir)) => Ok(Self::RedirectDir(RedirectDir::from_str(dir)?)), + ("upperdir", Some(dir)) => Ok(Self::UpperDir(PathBuf::from_str(dir).unwrap())), + ("userxattr", None) => Ok(Self::UserXAttr), + ("uuid", Some(uuid)) => Ok(Self::Uuid(Uuid::from_str(uuid)?)), + ("verity", Some(verity)) => Ok(Self::Verity(Verity::from_str(verity)?)), + ("volatile", None) => Ok(Self::Volatile), + ("workdir", Some(dir)) => Ok(Self::WorkDir(PathBuf::from_str(dir).unwrap())), + ("xino", Some(xino)) => Ok(Self::Xino(Xino::from_str(xino)?)), + (arg, _) => Err(FromStrError(arg.to_string())), + } + } +} + +impl Display for OverlayOption { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Index(enabled) => write!(f, "index={enabled}"), + Self::LowerDir(dirs) => write!(f, "lowerdir={dirs}"), + Self::Metacopy(enabled) => write!(f, "metacopy={enabled}"), + Self::NfsExport(enabled) => write!(f, "nfs_export={enabled}"), + Self::RedirectDir(option) => write!(f, "redirect_dir={option}"), + Self::UpperDir(dir) => write!(f, "upperdir={}", dir.to_string_lossy()), + Self::UserXAttr => write!(f, "userxattr"), + Self::Uuid(uuid) => write!(f, "uid={uuid}"), + Self::Verity(verity) => write!(f, "verity={verity}"), + Self::Volatile => write!(f, "volatile"), + Self::WorkDir(dir) => write!(f, "workdir={}", dir.to_string_lossy()), + Self::Xino(xino) => write!(f, "xino={xino}"), + } + } +} + +#[derive(Debug)] +pub struct Overlay(StackFs); + +impl_deref!(Overlay(StackFs)); +impl_deref_mut!(Overlay(StackFs)); + impl Stack for Overlay { fn lower_dirs(&self) -> impl Iterator { - todo!(); - iter::empty() + let result = self.find_option(|v| unwrap_enum!(v, OverlayOption::LowerDir)); + + match result { + Some(dirs) => dirs.into_iter(), + None => Default::default(), + } } fn upper_dir(&self) -> Option<&std::path::Path> { - todo!() + self.find_option(|v| unwrap_enum!(v, OverlayOption::UpperDir)) + .map(PathBuf::as_path) } fn work_dir(&self) -> Option<&std::path::Path> { - todo!() + self.find_option(|v| unwrap_enum!(v, OverlayOption::WorkDir)) + .map(PathBuf::as_path) } } -impl Mountpoint for Overlay { - fn options(&self) -> impl Iterator { - self.options.as_iter() +impl FileSystem for Overlay { + fn mount(&mut self) -> std::io::Result<()> { + let opts = self.options.iter().map(ToString::to_string); + let options = MountOption::Options(UncheckedOptions::from_iter(opts)); + mount("overlay", &self.mountpoint, options)?; + Ok(()) + } + + fn unmount(&mut self) -> std::io::Result<()> { + unmount(&self.mountpoint)?; + Ok(()) } } diff --git a/src/lib.rs b/src/lib.rs index 3d6f76e..4d84356 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,17 +2,10 @@ pub mod fs; mod macros; mod utils; -pub fn add(left: u64, right: u64) -> u64 { - left + right -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } +pub mod prelude { + pub use crate::fs::mount::*; + pub use crate::fs::stackable::fuse_overlay::*; + pub use crate::fs::stackable::overlay::*; + pub use crate::fs::stackable::*; + pub use crate::fs::*; } diff --git a/src/macros.rs b/src/macros.rs index 2caec69..3200246 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -1,3 +1,47 @@ +macro_rules! impl_fromstr { + ($name:ident($inner:ty), $err:ty) => { + impl std::str::FromStr for $name { + type Err = $err; + + fn from_str(s: &str) -> Result { + Ok(Self(<$inner>::from_str(s)?)) + } + } + }; +} + +macro_rules! impl_display { + ($name:ident) => { + impl Display for $name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } + } + }; +} + +macro_rules! impl_deref { + ($name:ident($inner:ty)) => { + impl std::ops::Deref for $name { + type Target = $inner; + + fn deref(&self) -> &Self::Target { + &self.0 + } + } + }; +} + +macro_rules! impl_deref_mut { + ($name:ident($inner:ty)) => { + impl std::ops::DerefMut for $name { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } + } + }; +} + macro_rules! impl_from_variants { ($enum_type:ident, $($variant:ident($inner_type:ty)),*) => { $( @@ -39,7 +83,21 @@ macro_rules! impl_wrapper_err { }; } +macro_rules! unwrap_enum { + ($value:ident, $enum:ident::$variant:ident) => { + match $value { + $enum::$variant(v) => Some(v), + _ => None, + } + }; +} + +pub(crate) use impl_deref; +pub(crate) use impl_deref_mut; +pub(crate) use impl_display; pub(crate) use impl_from_ty; pub(crate) use impl_from_variants; +pub(crate) use impl_fromstr; pub(crate) use impl_wrapper_err; pub(crate) use impl_wrapper_err_for; +pub(crate) use unwrap_enum; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..0423a7a --- /dev/null +++ b/src/main.rs @@ -0,0 +1,8 @@ +use vahanafs::fs::FileSystem; +use vahanafs::prelude::*; + +fn main() { + let mut fs = FuseOverlay::new("/home/rowan/vfs/mnt", ["/home/rowan/vfs/lower1"]); + println!("{fs}"); + fs.mount().expect("could not mount overlayfs"); +} diff --git a/src/utils.rs b/src/utils.rs index d2dee23..48cf104 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,11 @@ use core::fmt::Debug; +use std::convert::Infallible; +use std::ffi::{OsStr, OsString}; +use std::fmt::Display; +use std::path::Path; +use std::process::{Command as StdCommand, Output}; + +use crate::macros::{impl_deref, impl_deref_mut, impl_fromstr}; pub trait MapWhileRefExt<'a, I: 'a> { fn map_while_ref(&'a mut self, f: F) -> MapWhileRef<'a, I, F>; @@ -41,3 +48,115 @@ where (0, self.iter.size_hint().1) } } + +#[derive(Debug)] +pub enum CommandError { + Fail(Output), + Error(std::io::Error), +} + +impl Display for CommandError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CommandError::Fail(output) => match output.status.code() { + Some(code) => { + write!( + f, + "exit {code}: {}", + String::from_utf8_lossy(&output.stderr) + ) + } + None => write!(f, "process terminated by signal"), + }, + CommandError::Error(error) => write!(f, "{error:?}"), + } + } +} + +impl From for CommandError { + fn from(value: std::io::Error) -> Self { + Self::Error(value) + } +} + +impl From for std::io::Error { + fn from(value: CommandError) -> Self { + match value { + CommandError::Fail(output) => { + std::io::Error::other(String::from_utf8_lossy(&output.stderr)) + } + + CommandError::Error(error) => error, + } + } +} + +#[derive(Debug)] +pub struct Command(StdCommand); + +impl_deref!(Command(StdCommand)); +impl_deref_mut!(Command(StdCommand)); + +impl Command { + pub fn new(cmd: impl AsRef) -> Self { + Self(StdCommand::new(cmd)) + } + + pub fn arg(&mut self, arg: impl AsRef) -> &mut Self { + self.0.arg(arg); + self + } + + pub fn args(&mut self, args: I) -> &mut Self + where + I: IntoIterator, + S: AsRef, + { + self.0.args(args); + self + } + + pub fn env(&mut self, key: impl AsRef, value: impl AsRef) -> &mut Self { + self.0.env(key, value); + self + } + + pub fn env_remove(&mut self, key: impl AsRef) -> &mut Self { + self.0.env_remove(key); + self + } + + pub fn current_dir(&mut self, path: impl AsRef) -> &mut Self { + self.0.current_dir(path); + self + } + + pub fn output(&mut self) -> Result { + let output = self.0.output()?; + + if output.status.success() { + Ok(output) + } else { + Err(CommandError::Fail(output)) + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct DisplayString(OsString); + +impl_deref!(DisplayString(OsString)); +impl_deref_mut!(DisplayString(OsString)); +impl_fromstr!(DisplayString(OsString), Infallible); + +impl> From for DisplayString { + fn from(value: T) -> Self { + Self(value.as_ref().into()) + } +} + +impl Display for DisplayString { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0.display()) + } +}