diff --git a/crates/char_enum/src/lib.rs b/crates/char_enum/src/lib.rs index a380683..69556c0 100644 --- a/crates/char_enum/src/lib.rs +++ b/crates/char_enum/src/lib.rs @@ -3,7 +3,7 @@ pub use char_enum_derive; use std::{error::Error, fmt::Display}; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct FromCharError { message: String, } @@ -22,7 +22,7 @@ impl Display for FromCharError { impl Error for FromCharError {} -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct FromStrError { message: String, } diff --git a/src/fs/id_mapping.rs b/src/fs/id_mapping.rs index 0f18ef5..3bade1f 100644 --- a/src/fs/id_mapping.rs +++ b/src/fs/id_mapping.rs @@ -1,43 +1,61 @@ use char_enum::{char_enum_derive::FromChar, FromChar, FromStrError, ToChar}; -use std::{error::Error, fmt::Display, num::ParseIntError, str::FromStr}; +use std::{error::Error, fmt::Display, num::ParseIntError, ops::ControlFlow, str::FromStr}; -use itertools::Itertools; +use itertools::{peek_nth, Itertools}; -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, FromChar)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, FromChar)] pub enum IdKind { #[value = "u"] Uid, #[value = "g"] Gid, + #[default] #[value = "b"] Both, } -#[derive(Debug, Clone)] -pub struct TypedIdRangeError(String); +#[derive(Debug, Clone, PartialEq)] +pub enum ParseTypedIdRangeError { + IdRange(ParseUntypedIdRangeError), + MissingType(FromStrError), +} -impl From for TypedIdRangeError { +impl Display for ParseTypedIdRangeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ParseTypedIdRangeError::IdRange(err) => write!(f, "{err}"), + ParseTypedIdRangeError::MissingType(err) => write!(f, "{err}"), + } + } +} + +impl Error for ParseTypedIdRangeError {} + +impl From for ParseTypedIdRangeError { fn from(value: FromStrError) -> Self { - Self(value.to_string()) + Self::MissingType(value) } } -impl From for TypedIdRangeError { - fn from(value: ParseIdRangeError) -> Self { - Self(value.to_string()) +impl From for ParseTypedIdRangeError { + fn from(value: ParseUntypedIdRangeError) -> Self { + Self::IdRange(value) } } -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct TypedIdRange(IdKind, IdRange); +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct TypedIdRange(pub IdKind, pub UntypedIdRange); impl FromStr for TypedIdRange { - type Err = TypedIdRangeError; + type Err = ParseTypedIdRangeError; fn from_str(s: &str) -> Result { match s.split_once(":") { - Some((kind, rest)) => Ok(Self(IdKind::from_str(kind)?, IdRange::from_str(rest)?)), - None => Err(FromStrError::new(s.to_string()).into()), + Some((kind, rest)) => Ok(Self( + IdKind::from_str(kind)?, + UntypedIdRange::from_str(rest)?, + )), + None => Ok(Self(IdKind::default(), UntypedIdRange::from_str(s)?)), } } } @@ -48,46 +66,52 @@ impl Display for TypedIdRange { } } -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ParseIdRangeError { +impl From for TypedIdRange { + fn from(value: UntypedIdRange) -> Self { + Self(IdKind::Both, value) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum ParseUntypedIdRangeError { InvalidArguments(usize), NotANumber(ParseIntError), } -impl Display for ParseIdRangeError { +impl Display for ParseUntypedIdRangeError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - ParseIdRangeError::InvalidArguments(args) => write!( + ParseUntypedIdRangeError::InvalidArguments(args) => write!( f, "invalid number of arguments given (got {args}, expected 3)" ), - ParseIdRangeError::NotANumber(err) => { + ParseUntypedIdRangeError::NotANumber(err) => { write!(f, "could not parse argument: {err}") } } } } -impl Error for ParseIdRangeError {} +impl Error for ParseUntypedIdRangeError {} -impl From for ParseIdRangeError { +impl From for ParseUntypedIdRangeError { fn from(value: ParseIntError) -> Self { Self::NotANumber(value) } } #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct IdRange(usize, usize, usize); +pub struct UntypedIdRange(pub usize, pub usize, pub usize); -impl FromStr for IdRange { - type Err = ParseIdRangeError; +impl FromStr for UntypedIdRange { + type Err = ParseUntypedIdRangeError; fn from_str(s: &str) -> Result { let parts: Vec<&str> = s.split(':').collect(); let len = parts.len(); - if len > 3 { - return Err(ParseIdRangeError::InvalidArguments(len)); + if len != 3 { + return Err(ParseUntypedIdRangeError::InvalidArguments(len)); } let range: Vec = parts @@ -99,37 +123,302 @@ impl FromStr for IdRange { } } -impl Display for IdRange { +impl Display for UntypedIdRange { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}:{}:{}", self.0, self.1, self.2) } } -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct IdMapping(Vec); +impl From for UntypedIdRange { + fn from(value: TypedIdRange) -> Self { + value.1 + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ParseIdRangeError(String); + +impl Display for ParseIdRangeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl Error for ParseIdRangeError {} + +impl From for ParseIdRangeError { + fn from(value: ParseUntypedIdRangeError) -> Self { + Self(value.to_string()) + } +} + +impl From for ParseIdRangeError { + fn from(value: ParseTypedIdRangeError) -> Self { + Self(value.to_string()) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum IdRange { + Typed(TypedIdRange), + Untyped(UntypedIdRange), +} + +impl FromStr for IdRange { + type Err = ParseIdRangeError; + + fn from_str(s: &str) -> Result { + TypedIdRange::from_str(s) + .map(IdRange::Typed) + .or_else(|_| Ok(UntypedIdRange::from_str(s)?.into())) + } +} + +impl Display for IdRange { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + IdRange::Typed(range) => write!(f, "{range}"), + IdRange::Untyped(range) => write!(f, "{range}"), + } + } +} + +impl From for IdRange { + fn from(value: TypedIdRange) -> Self { + Self::Typed(value) + } +} + +impl From for IdRange { + fn from(value: UntypedIdRange) -> Self { + Self::Untyped(value) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Delimiter(char); + +impl Default for Delimiter { + fn default() -> Self { + Self(',') + } +} + +impl Display for Delimiter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for Delimiter { + fn from(value: char) -> Self { + Self(value) + } +} + +fn try_split(s: &str, delim: char) -> Option> { + let mut iter = peek_nth(s.split(delim)); + match iter.peek_nth(1) { + Some(_) => Some(iter), + None => None, + } +} + +fn split<'a, 'b>(s: &'a str, delim: &'b [char]) -> Option<(char, impl Iterator)> { + let mut found = None; + + for d in delim { + if let Some(iter) = try_split(s, *d) { + found = Some((*d, iter)); + break; + } + } + + found +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct IdMapping { + ranges: Vec, + delimiter: Delimiter, +} + +impl IdMapping { + pub fn new( + ranges: impl Iterator>, + delimiter: impl Into, + ) -> Self { + Self { + ranges: ranges.map_into().collect(), + delimiter: delimiter.into(), + } + } + + pub fn from_iter<'a>(s: impl IntoIterator) -> Result { + Ok(IdMapping { + ranges: s + .into_iter() + .map_while(|v| IdRange::from_str(v).ok()) + .collect(), + ..Default::default() + }) + } + + pub fn with_delimiter(self, delimiter: impl Into) -> Self { + Self { + delimiter: delimiter.into(), + ..self + } + } +} impl FromStr for IdMapping { type Err = ParseIdRangeError; fn from_str(s: &str) -> Result { - s.split(',') - .map(|s| IdRange::from_str(s)) - .process_results(|iter| Self(iter.collect())) + match split(s, &[',', ' ']) { + Some((d, iter)) => Ok(Self::from_iter(iter)?.with_delimiter(d)), + None => Err(ParseIdRangeError( + "none of the provided delimiters matched string".to_string(), + )), + } } } impl Display for IdMapping { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut iter = self.0.iter(); + let mut iter = self.ranges.iter(); + let delim = self.delimiter; if let Some(head) = iter.next() { write!(f, "{head}")?; for item in iter { - write!(f, ",{item}")?; + write!(f, "{delim}{item}")?; } } Ok(()) } } + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use crate::fs::id_mapping::{ + IdKind, IdRange, ParseTypedIdRangeError, ParseUntypedIdRangeError, + }; + + use super::{IdMapping, TypedIdRange, UntypedIdRange}; + + #[test] + fn untyped_id_range_from_str() { + let three_args = UntypedIdRange::from_str("1:100:3"); + assert_eq!(three_args, Ok(UntypedIdRange(1, 100, 3))); + + let too_many = UntypedIdRange::from_str("1:100:3:4"); + assert_eq!(too_many, Err(ParseUntypedIdRangeError::InvalidArguments(4))); + + let too_few = UntypedIdRange::from_str("1:100"); + assert_eq!(too_few, Err(ParseUntypedIdRangeError::InvalidArguments(2))); + } + + #[test] + fn typed_id_range_from_str() { + let uid_range = TypedIdRange::from_str("u:1:100:3"); + assert_eq!( + uid_range, + Ok(TypedIdRange(IdKind::Uid, UntypedIdRange(1, 100, 3))) + ); + + let gid_range = TypedIdRange::from_str("g:1:100:3"); + assert_eq!( + gid_range, + Ok(TypedIdRange(IdKind::Gid, UntypedIdRange(1, 100, 3))) + ); + + let both_range = TypedIdRange::from_str("b:1:100:3"); + assert_eq!( + both_range, + Ok(TypedIdRange(IdKind::Both, UntypedIdRange(1, 100, 3))) + ); + + let too_many = TypedIdRange::from_str("u:1:100:3:4"); + assert_eq!( + too_many, + Err(ParseTypedIdRangeError::IdRange( + ParseUntypedIdRangeError::InvalidArguments(4) + )) + ); + } + + #[test] + fn id_mapping_from_str() { + let map = IdMapping::from_str("1:100:3,2:101:5"); + + assert_eq!( + map, + Ok(IdMapping::new( + vec![UntypedIdRange(1, 100, 3), UntypedIdRange(2, 101, 5)].into_iter(), + ',' + )) + ); + + let map = IdMapping::from_str("u:1000:0:1 g:1001:1:2 5000:1000:2"); + + assert_eq!( + map, + Ok(IdMapping::new( + vec![ + IdRange::Typed(TypedIdRange(IdKind::Uid, UntypedIdRange(1000, 0, 1))), + IdRange::Typed(TypedIdRange(IdKind::Gid, UntypedIdRange(1001, 1, 2))), + IdRange::Untyped(UntypedIdRange(5000, 1000, 2)), + ] + .into_iter(), + ' ' + )) + ); + } + + #[test] + fn from_str_and_display_are_the_same() { + assert_eq!(IdKind::from_str("u").unwrap().to_string(), "u"); + assert_eq!(IdKind::from_str("g").unwrap().to_string(), "g"); + assert_eq!(IdKind::from_str("b").unwrap().to_string(), "b"); + + assert_eq!( + UntypedIdRange::from_str("1000:0:1").unwrap().to_string(), + "1000:0:1" + ); + + assert_eq!( + TypedIdRange::from_str("u:1000:0:1").unwrap().to_string(), + "u:1000:0:1" + ); + + assert_eq!( + IdRange::from_str("u:1000:0:1").unwrap().to_string(), + "u:1000:0:1" + ); + + assert_eq!( + IdRange::from_str("1000:0:1").unwrap().to_string(), + "1000:0:1" + ); + + assert_eq!( + IdMapping::from_str("1000:0:1,2:1001:5") + .unwrap() + .to_string(), + "1000:0:1,2:1001:5" + ); + + assert_eq!( + IdMapping::from_str("b:1000:0:1 g:2:1001:5") + .unwrap() + .to_string(), + "b:1000:0:1 g:2:1001:5" + ); + } +} diff --git a/src/fs/mount.rs b/src/fs/mount.rs index 2c54db6..b43f739 100644 --- a/src/fs/mount.rs +++ b/src/fs/mount.rs @@ -1,3 +1,4 @@ +use char_enum::FromStrError; use itertools::Itertools; use std::{ convert::Infallible, @@ -10,7 +11,7 @@ use std::{ }; use super::{ - id_mapping::{IdRange, ParseIdRangeError}, + id_mapping::{ParseUntypedIdRangeError, UntypedIdRange}, permission::Permissions, }; @@ -25,7 +26,7 @@ impl Display for ParseKeyValuePairError { impl Error for ParseKeyValuePairError {} -#[derive(Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct KeyValuePair { name: String, value: Option, @@ -104,13 +105,36 @@ impl Display for MountOptions { } } -#[derive(Debug, Default)] +#[derive(Debug, PartialEq, Eq)] +pub struct ParseAccessError; + +impl Display for ParseAccessError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "received invalid access type") + } +} + +impl Error for ParseAccessError {} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum Access { ReadOnly, #[default] ReadWrite, } +impl FromStr for Access { + type Err = ParseAccessError; + + fn from_str(s: &str) -> Result { + match s { + "read-only" => Ok(Self::ReadOnly), + "read-write" => Ok(Self::ReadWrite), + _ => Err(ParseAccessError), + } + } +} + impl Display for Access { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( @@ -135,7 +159,7 @@ impl Display for ParseDeviceIdTypeError { impl Error for ParseDeviceIdTypeError {} -#[derive(Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum DeviceId { Label(String), Uuid(String), @@ -192,7 +216,7 @@ impl From<&str> for DeviceId { } } -#[derive(Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct ParseOptionsModeError; impl Display for ParseOptionsModeError { @@ -203,7 +227,7 @@ impl Display for ParseOptionsModeError { impl Error for ParseOptionsModeError {} -#[derive(Debug)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum OptionsMode { Ignore, Append, @@ -240,7 +264,7 @@ impl Display for OptionsMode { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct ParseOptionsSourceError(String); impl Display for ParseOptionsSourceError { @@ -251,7 +275,7 @@ impl Display for ParseOptionsSourceError { impl Error for ParseOptionsSourceError {} -#[derive(Debug)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum OptionsSource { Mtab, Fstab, @@ -285,7 +309,7 @@ impl Display for OptionsSource { } } -#[derive(Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct ParseUserMapError; impl Display for ParseUserMapError { @@ -296,14 +320,14 @@ impl Display for ParseUserMapError { impl Error for ParseUserMapError {} -#[derive(Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum MapUsers { - Uid(IdRange), + Uid(UntypedIdRange), Namespace(PathBuf), } -impl From for MapUsers { - fn from(value: IdRange) -> Self { +impl From for MapUsers { + fn from(value: UntypedIdRange) -> Self { Self::Uid(value) } } @@ -318,7 +342,7 @@ impl FromStr for MapUsers { type Err = ParseUserMapError; fn from_str(s: &str) -> Result { - match IdRange::from_str(s) { + match UntypedIdRange::from_str(s) { Ok(range) => Ok(Self::from(range)), Err(_) => match PathBuf::from_str(s) { Ok(path) => Ok(Self::from(path)), @@ -341,7 +365,7 @@ impl Display for MapUsers { } } -#[derive(Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct ParseGidRangeError; impl Display for ParseGidRangeError { @@ -352,20 +376,20 @@ impl Display for ParseGidRangeError { impl Error for ParseGidRangeError {} -impl From for ParseGidRangeError { - fn from(_value: ParseIdRangeError) -> Self { +impl From for ParseGidRangeError { + fn from(_value: ParseUntypedIdRangeError) -> Self { Self } } -#[derive(Debug)] -pub struct GidRange(IdRange); +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct GidRange(UntypedIdRange); impl FromStr for GidRange { type Err = ParseGidRangeError; fn from_str(s: &str) -> Result { - Ok(IdRange::from_str(s).map(Self)?) + Ok(UntypedIdRange::from_str(s).map(Self)?) } } @@ -375,9 +399,29 @@ impl Display for GidRange { } } +impl From for GidRange { + fn from(value: UntypedIdRange) -> Self { + Self(value) + } +} + #[derive(Debug)] pub struct Options(Vec); +impl Clone for Options { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +impl PartialEq for Options { + fn eq(&self, other: &Self) -> bool { + self.0.eq(&other.0) + } +} + +impl Eq for Options {} + impl FromStr for Options { type Err = T::Err; @@ -409,7 +453,7 @@ impl From> for Options { } } -#[derive(Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Sources(Options); impl FromStr for Sources { @@ -420,11 +464,11 @@ impl FromStr for Sources { } } -#[derive(Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct ParseUncheckedOptionsError; impl Display for ParseUncheckedOptionsError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { unreachable!() } } @@ -437,7 +481,7 @@ impl From for ParseUncheckedOptionsError { } } -#[derive(Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct UncheckedOptions(Options); impl FromStr for UncheckedOptions { @@ -494,13 +538,15 @@ impl From> for UncheckedOptions { } } -#[derive(Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum ParseMountOptionError { DeviceId(ParseDeviceIdTypeError), MapUsers(ParseUserMapError), MapGroups(ParseGidRangeError), OptionsMode(ParseOptionsModeError), OptionsSource(ParseOptionsSourceError), + Operation(ParseMountOperationError), + Subtree(ParseSubtreeError), UnknownOption(String), } @@ -512,6 +558,8 @@ impl Display for ParseMountOptionError { Self::MapGroups(e) => write!(f, "{e}"), Self::OptionsMode(e) => write!(f, "{e}"), Self::OptionsSource(e) => write!(f, "{e}"), + Self::Operation(e) => write!(f, "{e}"), + Self::Subtree(e) => write!(f, "{e}"), Self::UnknownOption(opt) => write!(f, "unknown option: {opt}"), } } @@ -555,10 +603,22 @@ impl From for ParseMountOptionError { } } -#[derive(Debug)] +impl From for ParseMountOptionError { + fn from(value: ParseMountOperationError) -> Self { + Self::Operation(value) + } +} + +impl From for ParseMountOptionError { + fn from(value: ParseSubtreeError) -> Self { + Self::Subtree(value) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] pub enum MountOption { Label(DeviceId), - FsType(String), + FsType(UncheckedOptions), All, NoCanonicalize, Fake, @@ -576,8 +636,7 @@ pub enum MountOption { OnlyOnce, Options(UncheckedOptions), TestOptions(UncheckedOptions), - Types(UncheckedOptions), - Source(UncheckedOptions), + Source(DeviceId), Target(PathBuf), TargetPrefix(PathBuf), Verbose, @@ -600,7 +659,7 @@ impl FromStr for MountOption { match option { ("L" | "label", Some(id)) => Ok(Self::Label(DeviceId::from_str(id)?)), - ("t" | "types", Some(types)) => Ok(Self::FsType(types.to_string())), + ("t" | "types", Some(types)) => Ok(Self::FsType(UncheckedOptions::from_str(types)?)), ("a" | "all", None) => Ok(Self::All), ("c" | "no-canonicalize", None) => Ok(Self::NoCanonicalize), ("f" | "fake", None) => Ok(Self::Fake), @@ -625,7 +684,7 @@ impl FromStr for MountOption { Ok(Self::TestOptions(UncheckedOptions::from_str(options)?)) } ("r" | "read-only", None) => Ok(Self::Access(Access::ReadOnly)), - ("source", Some(src)) => Ok(Self::Source(UncheckedOptions::from_str(src)?)), + ("source", Some(src)) => Ok(Self::Source(DeviceId::from_str(src)?)), ("target", Some(target)) => Ok(Self::Target(target.into())), ("target-prefix", Some(path)) => Ok(Self::TargetPrefix(path.into())), ("w" | "rw" | "read-write", None) => Ok(Self::Access(Access::ReadWrite)), @@ -634,14 +693,9 @@ impl FromStr for MountOption { ("B" | "bind", None) => Ok(Self::Operation(MountOperation::Bind)), ("M" | "move", None) => Ok(Self::Operation(MountOperation::Move)), ("R" | "rbind", None) => Ok(Self::Operation(MountOperation::RecursiveBind)), - ("make-shared", None) => Ok(Self::Subtree(Subtree::Shared)), - ("make-slave", None) => Ok(Self::Subtree(Subtree::Replica)), - ("make-private", None) => Ok(Self::Subtree(Subtree::Private)), - ("make-unbindable", None) => Ok(Self::Subtree(Subtree::Unbindable)), - ("make-rshared", None) => Ok(Self::Subtree(Subtree::Shared.recursive())), - ("make-rslave", None) => Ok(Self::Subtree(Subtree::Replica.recursive())), - ("make-rprivate", None) => Ok(Self::Subtree(Subtree::Private.recursive())), - ("make-runbindable", None) => Ok(Self::Subtree(Subtree::Unbindable.recursive())), + (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())), } } @@ -677,7 +731,6 @@ impl Display for MountOption { Self::OnlyOnce => "--onlyonce".to_string(), Self::Options(opts) => format!("--options {opts}"), Self::TestOptions(opts) => format!("--test-opts {opts}"), - Self::Types(types) => format!("--types {types}"), Self::Source(src) => format!("--source {src}"), Self::Target(path) => format!("--target {}", path.to_string_lossy()), Self::TargetPrefix(path) => format!("--target-prefix {}", path.to_string_lossy()), @@ -691,7 +744,18 @@ impl Display for MountOption { } } -#[derive(Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ParseSubtreeError(String); + +impl Display for ParseSubtreeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "received invalid subtree: {}", self.0) + } +} + +impl Error for ParseSubtreeError {} + +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Subtree { Shared, Replica, @@ -706,6 +770,26 @@ impl Subtree { } } +impl FromStr for Subtree { + type Err = ParseSubtreeError; + + fn from_str(s: &str) -> Result { + match s { + "shared" => Ok(Self::Shared), + "slave" | "replica" => Ok(Self::Replica), + "private" => Ok(Self::Private), + "unbindable" => Ok(Self::Unbindable), + + "rshared" => Ok(Self::recursive(Self::Shared)), + "rslave" | "rreplica" => Ok(Self::recursive(Self::Replica)), + "rprivate" => Ok(Self::recursive(Self::Private)), + "runbindable" => Ok(Self::recursive(Self::Unbindable)), + + _ => Err(ParseSubtreeError(s.to_string())), + } + } +} + impl Display for Subtree { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( @@ -722,7 +806,7 @@ impl Display for Subtree { } } -#[derive(Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct ParseMountOperationError(String); impl Display for ParseMountOperationError { @@ -733,7 +817,7 @@ impl Display for ParseMountOperationError { impl Error for ParseMountOperationError {} -#[derive(Debug)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum MountOperation { Bind, RecursiveBind, @@ -794,25 +878,381 @@ impl Mount { .arg(&self.target) .output() } +} - pub fn test(&self) -> Command { - let mut cmd = Command::new("mount"); - cmd.arg(self.options.to_string()) - .arg(self.source.to_string()) - .arg(&self.target); - cmd +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() + ) } } #[cfg(test)] mod tests { - use super::{Mount, MountOption, MountOptions, Options}; + use std::{borrow::Cow, path::PathBuf, str::FromStr}; + + use enumflags2::BitFlags; + use itertools::Itertools; + + use crate::fs::{ + id_mapping::UntypedIdRange, + mount::{ParseDeviceIdTypeError, ParseOptionsSourceError}, + permission::Permissions, + }; + + use super::{ + Access, DeviceId, KeyValuePair, MapUsers, Mount, MountOperation, MountOption, MountOptions, + Options, OptionsMode, OptionsSource, ParseMountOptionError, Subtree, UncheckedOptions, + }; + + #[test] + fn key_value() { + let kv = KeyValuePair::from_str("test=value").unwrap(); + let k = KeyValuePair::from_str("keyword").unwrap(); + + assert_eq!(kv, KeyValuePair::new("test", Some("value"))); + assert_eq!(k, KeyValuePair::new("keyword", None::)); + + assert_eq!(kv.to_string(), "test=value"); + assert_eq!(k.to_string(), "keyword"); + } + + #[test] + fn access() { + let read = Access::from_str("read-only").unwrap(); + + let read_write = Access::from_str("read-write").unwrap(); + + assert_eq!(read, Access::ReadOnly); + assert_eq!(read_write, Access::ReadWrite); + + assert_eq!(read.to_string(), "read-only"); + assert_eq!(read_write.to_string(), "read-write"); + } + + #[test] + fn device_id() { + let label = DeviceId::from_str("LABEL=test").unwrap(); + let uuid = DeviceId::from_str("UUID=1234").unwrap(); + let pl = DeviceId::from_str("PARTLABEL=test").unwrap(); + let pu = DeviceId::from_str("PARTUUID=98776543").unwrap(); + let id = DeviceId::from_str("ID=awawa").unwrap(); + 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!( + should_err, + Err(ParseDeviceIdTypeError("GARBAGE".to_string())) + ); + + assert_eq!(label.to_string(), "LABEL=test"); + assert_eq!(uuid.to_string(), "UUID=1234"); + assert_eq!(pl.to_string(), "PARTLABEL=test"); + assert_eq!(pu.to_string(), "PARTUUID=98776543"); + assert_eq!(id.to_string(), "ID=awawa"); + assert_eq!(other.to_string(), "awawa"); + } + + #[test] + fn options_mode() { + let ignore = OptionsMode::from_str("ignore").unwrap(); + let append = OptionsMode::from_str("append").unwrap(); + let prepend = OptionsMode::from_str("prepend").unwrap(); + let replace = OptionsMode::from_str("replace").unwrap(); + + assert_eq!(ignore, OptionsMode::Ignore); + assert_eq!(append, OptionsMode::Append); + assert_eq!(prepend, OptionsMode::Prepend); + assert_eq!(replace, OptionsMode::Replace); + + assert_eq!(ignore.to_string(), "ignore"); + assert_eq!(append.to_string(), "append"); + assert_eq!(prepend.to_string(), "prepend"); + assert_eq!(replace.to_string(), "replace"); + } + + #[test] + fn options_source() { + let mtab = OptionsSource::from_str("mtab").unwrap(); + let fstab = OptionsSource::from_str("fstab").unwrap(); + let disable = OptionsSource::from_str("disable").unwrap(); + let should_err = OptionsSource::from_str("awawa"); + + assert_eq!(mtab, OptionsSource::Mtab); + assert_eq!(fstab, OptionsSource::Fstab); + assert_eq!(disable, OptionsSource::Disable); + assert_eq!( + should_err, + Err(ParseOptionsSourceError("awawa".to_string())) + ); + + assert_eq!(mtab.to_string(), "mtab"); + assert_eq!(fstab.to_string(), "fstab"); + assert_eq!(disable.to_string(), "disable"); + } + + #[test] + fn map_users() { + let uid_map = MapUsers::from_str("1000:1001:1").unwrap(); + let ns_map = MapUsers::from_str("/proc/1000/ns/user").unwrap(); + + assert_eq!(uid_map, MapUsers::Uid(UntypedIdRange(1000, 1001, 1))); + assert_eq!( + ns_map, + MapUsers::Namespace(PathBuf::from_str("/proc/1000/ns/user").unwrap()) + ); + + assert_eq!(uid_map.to_string(), "1000:1001:1"); + assert_eq!(ns_map.to_string(), "/proc/1000/ns/user"); + } + + #[test] + fn options() { + let opts = Options::::from_str("key=value,keyword").unwrap(); + + assert_eq!( + opts, + Options(vec![ + KeyValuePair::new("key", Some("value")), + KeyValuePair::new("keyword", None::<&str>) + ]) + ); + + assert_eq!(opts.to_string(), "key=value,keyword"); + } + + #[test] + fn operations() { + let bind = MountOperation::from_str("bind").unwrap(); + let mov = MountOperation::from_str("move").unwrap(); + let rbind = MountOperation::from_str("rbind").unwrap(); + + assert_eq!(bind, MountOperation::Bind); + assert_eq!(mov, MountOperation::Move); + assert_eq!(rbind, MountOperation::RecursiveBind); + + assert_eq!(bind.to_string(), "bind"); + assert_eq!(mov.to_string(), "move"); + assert_eq!(rbind.to_string(), "rbind"); + } + + #[test] + fn subtree() { + let shared = Subtree::from_str("shared").unwrap(); + let rshared = Subtree::from_str("rshared").unwrap(); + let private = Subtree::from_str("private").unwrap(); + let rprivate = Subtree::from_str("rprivate").unwrap(); + let replica = Subtree::from_str("replica").unwrap(); + let rreplica = Subtree::from_str("rreplica").unwrap(); + let unbind = Subtree::from_str("unbindable").unwrap(); + let runbind = Subtree::from_str("runbindable").unwrap(); + + assert_eq!(shared, Subtree::Shared); + assert_eq!(rshared, Subtree::recursive(Subtree::Shared)); + assert_eq!(private, Subtree::Private); + assert_eq!(rprivate, Subtree::recursive(Subtree::Private)); + assert_eq!(replica, Subtree::Replica); + assert_eq!(rreplica, Subtree::recursive(Subtree::Replica)); + assert_eq!(unbind, Subtree::Unbindable); + assert_eq!(runbind, Subtree::recursive(Subtree::Unbindable)); + + assert_eq!(shared.to_string(), "shared"); + assert_eq!(rshared.to_string(), "rshared"); + assert_eq!(private.to_string(), "private"); + assert_eq!(rprivate.to_string(), "rprivate"); + assert_eq!(replica.to_string(), "slave"); + assert_eq!(rreplica.to_string(), "rslave"); + assert_eq!(unbind.to_string(), "unbindable"); + assert_eq!(runbind.to_string(), "runbindable"); + } + + fn try_opts<'a, 'b>( + cmds: &[&'a str], + args: impl Into>, + ) -> Result + where + 'a: 'b, + { + let args = args.into(); + let to_args = |c: &'a str| -> Cow<'b, str> { + match args { + Some(x) => format!("{c} {x}").into(), + None => c.into(), + } + }; + + cmds.iter().map(|s| to_args(s)).fold( + Err(ParseMountOptionError::UnknownOption("".to_string())), + |_, c| MountOption::from_str(&c), + ) + } + + struct Opts<'a, 'b> { + options: Cow<'a, [&'a str]>, + args: Option<&'b str>, + } + + impl<'a, 'b> Opts<'a, 'b> { + fn assert(&self, expected: MountOption) { + let opt = try_opts(&self.options, self.args).unwrap(); + assert_eq!(opt, expected); + } + } + + impl<'a, 'b> From<&'a str> for Opts<'a, 'b> { + fn from(value: &'a str) -> Self { + Self { + options: vec![value].into(), + args: None, + } + } + } + + impl<'a, 'b> From<(&'a str, &'b str)> for Opts<'a, 'b> { + fn from(value: (&'a str, &'b str)) -> Self { + Self { + options: vec![value.0].into(), + args: Some(value.1), + } + } + } + + impl<'a, 'b> From<&'a [&'a str]> for Opts<'a, 'b> { + fn from(value: &'a [&'a str]) -> Self { + Self { + options: value.into(), + args: None, + } + } + } + + impl<'a, 'b> From<(&'a [&str], &'b str)> for Opts<'a, 'b> { + fn from(value: (&'a [&str], &'b str)) -> Self { + Self { + options: value.0.into(), + args: Some(value.1), + } + } + } + + #[test] + fn mount_options() { + 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("-a").assert(MountOption::All); + Opts::from(["-c", "--no-canonicalize"].as_slice()).assert(MountOption::NoCanonicalize); + Opts::from(["-f", "--fake"].as_slice()).assert(MountOption::Fake); + Opts::from(["-F", "--fork"].as_slice()).assert(MountOption::Fork); + + Opts::from((["-T", "--fstab"].as_slice(), "/etc/fstab")) + .assert(MountOption::Fstab("/etc/fstab".into())); + + Opts::from(["-i", "--internal-only"].as_slice()).assert(MountOption::InternalOnly); + Opts::from(["-l", "--show-labels"].as_slice()).assert(MountOption::ShowLabels); + + Opts::from(("--map-groups", "1000:1001:1")) + .assert(MountOption::MapGroups(UntypedIdRange(1000, 1001, 1).into())); + + Opts::from(("--map-users", "1000:1001:1")).assert(MountOption::MapUsers(MapUsers::Uid( + UntypedIdRange(1000, 1001, 1), + ))); + + Opts::from(("--map-users", "/proc/1000/ns/user")).assert(MountOption::MapUsers( + MapUsers::Namespace("/proc/1000/ns/user".into()), + )); + + Opts::from(["-m", "--mkdir"].as_slice()).assert(MountOption::MakeDir(None)); + + Opts::from((["-m", "--mkdir"].as_slice(), "7777")).assert(MountOption::MakeDir(Some( + Permissions::Octal(BitFlags::all().into()), + ))); + + Opts::from(["-n", "--no-mtab"].as_slice()).assert(MountOption::NoMtab); + + Opts::from(("--options-mode", "ignore")) + .assert(MountOption::OptionsMode(OptionsMode::Ignore)); + + Opts::from(("--options-source", "fstab")).assert(MountOption::OptionsSource( + vec![OptionsSource::Fstab].into(), + )); + + 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(); + + Opts::from((["-o", "--options"].as_slice(), "kbity,dogy,aaaaaa=awawa")) + .assert(MountOption::Options(opts.clone())); + + Opts::from((["-O", "--test-opts"].as_slice(), "kbity,dogy,aaaaaa=awawa")) + .assert(MountOption::TestOptions(opts)); + + Opts::from(["-r", "--read-only"].as_slice()).assert(MountOption::Access(Access::ReadOnly)); + + Opts::from(("--source", "LABEL=label")) + .assert(MountOption::Source(DeviceId::Label("label".into()))); + + Opts::from(("--target", "/mnt")).assert(MountOption::Target("/mnt".into())); + Opts::from(("--target-prefix", "/pfx")).assert(MountOption::TargetPrefix("/pfx".into())); + + Opts::from(["-w", "--rw", "--read-write"].as_slice()) + .assert(MountOption::Access(Access::ReadWrite)); + + Opts::from((["-N", "--namespace"].as_slice(), "1000")) + .assert(MountOption::Namespace("1000".into())); + + Opts::from((["-U", "--uuid"].as_slice(), "1234")) + .assert(MountOption::Label(DeviceId::Uuid("1234".into()))); + + Opts::from(["-B", "--bind"].as_slice()) + .assert(MountOption::Operation(MountOperation::Bind)); + Opts::from(["-M", "--move"].as_slice()) + .assert(MountOption::Operation(MountOperation::Move)); + Opts::from(["-R", "--rbind"].as_slice()) + .assert(MountOption::Operation(MountOperation::RecursiveBind)); + + Opts::from("--make-shared").assert(MountOption::Subtree(Subtree::Shared)); + Opts::from("--make-rshared") + .assert(MountOption::Subtree(Subtree::recursive(Subtree::Shared))); + + Opts::from("--make-private").assert(MountOption::Subtree(Subtree::Private)); + Opts::from("--make-rprivate") + .assert(MountOption::Subtree(Subtree::recursive(Subtree::Private))); + + Opts::from("--make-slave").assert(MountOption::Subtree(Subtree::Replica)); + Opts::from("--make-rslave") + .assert(MountOption::Subtree(Subtree::recursive(Subtree::Replica))); + + Opts::from("--make-unbindable").assert(MountOption::Subtree(Subtree::Unbindable)); + Opts::from("--make-runbindable").assert(MountOption::Subtree(Subtree::recursive( + Subtree::Unbindable, + ))); + } #[test] fn it_works() { let mut options = MountOptions::default(); - options.push(MountOption::Types(vec!["overlay"].into())); + options.push(MountOption::FsType(vec!["overlay"].into())); let mount = Mount::new("/test", "/target", options); - println!("{:?}", mount.test()); + println!("{mount}"); } } diff --git a/src/fs/permission.rs b/src/fs/permission.rs index ea70bfb..4b92422 100644 --- a/src/fs/permission.rs +++ b/src/fs/permission.rs @@ -37,7 +37,7 @@ impl From> for ParseModeError { #[bitflags] #[repr(u16)] -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Mode { IXOTH = 0x0001, IWOTH = 0x0002, @@ -59,14 +59,14 @@ impl Display for Mode { } } -#[derive(Clone, Copy, Debug, Default)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct ModeFlags(BitFlags); impl FromStr for ModeFlags { type Err = ParseModeError; fn from_str(s: &str) -> Result { - let bits = u16::from_str(s)?; + let bits = u16::from_str_radix(s, 16)?; Ok(Self(BitFlags::from_bits(bits)?)) } @@ -78,7 +78,21 @@ impl Display for ModeFlags { } } -#[derive(Clone, Copy, Debug, FromChar)] +impl TryFrom for ModeFlags { + type Error = FromBitsError; + + fn try_from(value: u16) -> Result { + Ok(Self(BitFlags::from_bits(value)?)) + } +} + +impl From> for ModeFlags { + fn from(value: BitFlags) -> Self { + Self(value) + } +} + +#[derive(Clone, Copy, Debug, FromChar, PartialEq, Eq)] pub enum Operator { #[value = "-"] Sub, @@ -105,7 +119,7 @@ impl Operator { #[bitflags] #[repr(u8)] -#[derive(Clone, Copy, Debug, FromChar)] +#[derive(Clone, Copy, Debug, FromChar, PartialEq, Eq)] pub enum SymbolicMode { #[value = 'r'] Read, @@ -123,7 +137,7 @@ pub enum SymbolicMode { #[bitflags] #[repr(u8)] -#[derive(Clone, Copy, Debug, FromChar)] +#[derive(Clone, Copy, Debug, FromChar, PartialEq, Eq)] pub enum GroupId { #[value = 'u'] User, @@ -133,7 +147,7 @@ pub enum GroupId { Other, } -#[derive(Clone, Debug)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Modes(BitFlags); impl Default for Modes { @@ -170,7 +184,7 @@ impl Display for Modes { } } -#[derive(Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct ParseSymbolicArgsError(FromStrError); impl Display for ParseSymbolicArgsError { @@ -193,7 +207,7 @@ impl From for ParseSymbolicArgsError { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum SymbolicArgs { Group(Modes), Mode(Modes), @@ -222,7 +236,7 @@ impl Display for SymbolicArgs { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum ParseSymbolicError { Group(String), Operator(String), @@ -251,7 +265,7 @@ impl From for ParseSymbolicError { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct SymbolicPermissions { groups: Modes, operator: Operator, @@ -297,7 +311,7 @@ impl Display for SymbolicPermissions { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct ParseOctalError(String); impl Display for ParseOctalError { @@ -314,7 +328,7 @@ impl From for ParseOctalError { } } -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct OctalPermissions(ModeFlags); impl Display for OctalPermissions { @@ -331,7 +345,21 @@ impl FromStr for OctalPermissions { } } -#[derive(Debug)] +impl TryFrom for OctalPermissions { + type Error = FromBitsError; + + fn try_from(value: u16) -> Result { + Ok(Self(ModeFlags::try_from(value)?)) + } +} + +impl From> for OctalPermissions { + fn from(value: BitFlags) -> Self { + Self(value.into()) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] pub enum ParsePermissionsError { Symbolic(ParseSymbolicError), Octal(ParseOctalError), @@ -360,7 +388,7 @@ impl From for ParsePermissionsError { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Permissions { Symbolic(SymbolicPermissions), Octal(OctalPermissions), diff --git a/src/fs/stackable/fuse_overlay.rs b/src/fs/stackable/fuse_overlay.rs index 48944d0..bfb4fdf 100644 --- a/src/fs/stackable/fuse_overlay.rs +++ b/src/fs/stackable/fuse_overlay.rs @@ -4,7 +4,7 @@ use crate::fs::{id_mapping::IdMapping, AsIter, FileSystem, Mountpoint}; use super::{Stack, Stackable}; -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum FuseOverlayOption { CloneFd, MaxIdleThreads(isize),