feat: unit testing,,,,

This commit is contained in:
Rowan 2025-03-01 04:24:25 -06:00
parent 2f1fc9ba90
commit 082d666fe0
5 changed files with 862 additions and 105 deletions

View file

@ -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,
}

View file

@ -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<FromStrError> 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<FromStrError> for ParseTypedIdRangeError {
fn from(value: FromStrError) -> Self {
Self(value.to_string())
Self::MissingType(value)
}
}
impl From<ParseIdRangeError> for TypedIdRangeError {
fn from(value: ParseIdRangeError) -> Self {
Self(value.to_string())
impl From<ParseUntypedIdRangeError> 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<Self, Self::Err> {
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<UntypedIdRange> 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<ParseIntError> for ParseIdRangeError {
impl From<ParseIntError> 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<Self, Self::Err> {
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<usize> = 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<IdRange>);
impl From<TypedIdRange> 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<ParseUntypedIdRangeError> for ParseIdRangeError {
fn from(value: ParseUntypedIdRangeError) -> Self {
Self(value.to_string())
}
}
impl From<ParseTypedIdRangeError> 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<Self, Self::Err> {
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<TypedIdRange> for IdRange {
fn from(value: TypedIdRange) -> Self {
Self::Typed(value)
}
}
impl From<UntypedIdRange> 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<char> for Delimiter {
fn from(value: char) -> Self {
Self(value)
}
}
fn try_split(s: &str, delim: char) -> Option<impl Iterator<Item = &str>> {
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<Item = &'a str>)> {
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<IdRange>,
delimiter: Delimiter,
}
impl IdMapping {
pub fn new(
ranges: impl Iterator<Item = impl Into<IdRange>>,
delimiter: impl Into<Delimiter>,
) -> Self {
Self {
ranges: ranges.map_into().collect(),
delimiter: delimiter.into(),
}
}
pub fn from_iter<'a>(s: impl IntoIterator<Item = &'a str>) -> Result<Self, ParseIdRangeError> {
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<Delimiter>) -> Self {
Self {
delimiter: delimiter.into(),
..self
}
}
}
impl FromStr for IdMapping {
type Err = ParseIdRangeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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"
);
}
}

View file

@ -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<String>,
@ -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<Self, Self::Err> {
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<IdRange> for MapUsers {
fn from(value: IdRange) -> Self {
impl From<UntypedIdRange> 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<Self, Self::Err> {
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<ParseIdRangeError> for ParseGidRangeError {
fn from(_value: ParseIdRangeError) -> Self {
impl From<ParseUntypedIdRangeError> 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<Self, Self::Err> {
Ok(IdRange::from_str(s).map(Self)?)
Ok(UntypedIdRange::from_str(s).map(Self)?)
}
}
@ -375,9 +399,29 @@ impl Display for GidRange {
}
}
impl From<UntypedIdRange> for GidRange {
fn from(value: UntypedIdRange) -> Self {
Self(value)
}
}
#[derive(Debug)]
pub struct Options<T>(Vec<T>);
impl<T: Clone> Clone for Options<T> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<T: PartialEq> PartialEq for Options<T> {
fn eq(&self, other: &Self) -> bool {
self.0.eq(&other.0)
}
}
impl<T: Eq> Eq for Options<T> {}
impl<T: FromStr> FromStr for Options<T> {
type Err = T::Err;
@ -409,7 +453,7 @@ impl<T> From<Vec<T>> for Options<T> {
}
}
#[derive(Debug)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Sources(Options<OptionsSource>);
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<Infallible> for ParseUncheckedOptionsError {
}
}
#[derive(Debug)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct UncheckedOptions(Options<String>);
impl FromStr for UncheckedOptions {
@ -494,13 +538,15 @@ impl From<Options<String>> 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<ParseOptionsSourceError> for ParseMountOptionError {
}
}
#[derive(Debug)]
impl From<ParseMountOperationError> for ParseMountOptionError {
fn from(value: ParseMountOperationError) -> Self {
Self::Operation(value)
}
}
impl From<ParseSubtreeError> 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<Self, Self::Err> {
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::<String>));
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::<KeyValuePair>::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<Option<&'b str>>,
) -> Result<MountOption, ParseMountOptionError>
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}");
}
}

View file

@ -37,7 +37,7 @@ impl From<FromBitsError<Mode>> 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<Mode>);
impl FromStr for ModeFlags {
type Err = ParseModeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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<u16> for ModeFlags {
type Error = FromBitsError<Mode>;
fn try_from(value: u16) -> Result<Self, Self::Error> {
Ok(Self(BitFlags::from_bits(value)?))
}
}
impl From<BitFlags<Mode>> for ModeFlags {
fn from(value: BitFlags<Mode>) -> 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<T: BitFlag>(BitFlags<T>);
impl<T: BitFlag> Default for Modes<T> {
@ -170,7 +184,7 @@ impl<T: BitFlag + Display> Display for Modes<T> {
}
}
#[derive(Debug)]
#[derive(Clone, Debug, PartialEq)]
pub struct ParseSymbolicArgsError(FromStrError);
impl Display for ParseSymbolicArgsError {
@ -193,7 +207,7 @@ impl From<FromStrError> for ParseSymbolicArgsError {
}
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SymbolicArgs {
Group(Modes<GroupId>),
Mode(Modes<SymbolicMode>),
@ -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<ParseSymbolicArgsError> for ParseSymbolicError {
}
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SymbolicPermissions {
groups: Modes<GroupId>,
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<ParseModeError> 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<u16> for OctalPermissions {
type Error = FromBitsError<Mode>;
fn try_from(value: u16) -> Result<Self, Self::Error> {
Ok(Self(ModeFlags::try_from(value)?))
}
}
impl From<BitFlags<Mode>> for OctalPermissions {
fn from(value: BitFlags<Mode>) -> Self {
Self(value.into())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ParsePermissionsError {
Symbolic(ParseSymbolicError),
Octal(ParseOctalError),
@ -360,7 +388,7 @@ impl From<ParseOctalError> for ParsePermissionsError {
}
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Permissions {
Symbolic(SymbolicPermissions),
Octal(OctalPermissions),

View file

@ -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),